Module: RustPdf
- Defined in:
- lib/rustpdf.rb,
lib/rustpdf/native.rb,
lib/rustpdf/document.rb,
lib/rustpdf/editable_doc.rb
Overview
Idiomatic Ruby binding for the rust-pdf core over its C ABI (libpdf_ffi), using the built-in Fiddle stdlib. Covers the whole product surface: vector graphics, fonts/text, paragraphs, images, PDF/A (1b-3a), tagged/accessible output, attachments, AcroForm fields, manipulation, text extraction, encryption and digital signatures. Free and open source (MIT).
Defined Under Namespace
Modules: Align, Certify, Cipher, FacturxProfile, ImageAnchor, Native, Pdfa, Relationship, StampSpace, Version, VerticalAlign, VerticalAnchor Classes: Bookmark, Document, EditableDoc, Error, PageGeometry, PdfOverview, PdfRect, SignatureField, SignaturePolicy, SigningOptions, SigningSession, TextHit
Class Method Summary collapse
-
.add_dss(pdf, certs: [], crls: []) ⇒ Object
Append a Document Security Store (/DSS, PAdES-B-LT).
-
.begin_signing(pdf, options: nil) ⇒ Object
Model B — two-phase signing, phase 1.
-
.begin_timestamp(pdf) ⇒ Object
Phase 1 of a network-TSA document timestamp (/DocTimeStamp).
-
.build_signing_options(options) ⇒ Object
Marshal a SigningOptions (or nil) into the C PdfSigningOptions struct bytes, returning [packed_struct, keepalive] where
keepaliveholds the Fiddle pointers backing the struct's string/byte fields (keep it referenced until the native call returns). - .check(status) ⇒ Object
-
.complete_signature(document, container) ⇒ Object
Model B — phase 2.
-
.extract_images_to_dir(pdf, dir) ⇒ Object
Extract every raster image into
dir(JPEG verbatim as .jpg, others as .png; files named pageN_name.ext). -
.extract_page_text(pdf, page_index) ⇒ Object
Extract the text of a single 0-based
page_index(Unicode via ToUnicode), without building an intermediate one-page document. -
.extract_text(pdf) ⇒ Object
Extract a document's text (Unicode via ToUnicode).
-
.find_text(pdf, query, case_sensitive: false) ⇒ Object
Find every occurrence of
queryinpdf, returning positional boxes. -
.inspect_pdf(pdf) ⇒ Object
Inspect
pdfwithout mutating it: PDF version, PDF/A level (if any), encryption posture and page count. - .last_error ⇒ Object
-
.list_signatures(pdf) ⇒ Object
List the signature fields in
pdf(detect existing signatures before signing). -
.measure_page(pdf, index) ⇒ Object
Read the geometry of a single 0-based page of
pdf. -
.measure_pages(pdf) ⇒ Object
Read the geometry (size, rotation, MediaBox, CropBox) of every page in
pdf, in page order, without mutating it. -
.out_int ⇒ Object
Run a producer { |out_int| status } and return the written int.
-
.page_count(pdf) ⇒ Object
Number of pages in
pdf(free). -
.read_buffer(pp, pn) ⇒ Object
Read an out-buffer (pointer-to-pointer
pp, pointer-to-lenpn) into a binary String, freeing the native buffer. -
.render_page_to_png(pdf, page = 0, dpi = 150.0) ⇒ Object
Render page
page(0-based) ofpdfto a PNG image atdpidots-per-inch. -
.sign(pdf, key_der, cert_der, reason: nil, location: nil, name: nil, pades: false) ⇒ Object
Sign a PDF (PKCS#7 detached, incremental update).
-
.sign_with(pdf, cert_der, chain: [], options: nil, &block) ⇒ Object
Model A — remote signer.
-
.take_bytes ⇒ Object
Run an out-buffer producer { |out_ptr, out_len| status } and return the produced bytes, always freeing the native buffer.
-
.timestamp(pdf, tsa_key_der, tsa_cert_der, date: nil) ⇒ Object
Append a document timestamp (/DocTimeStamp, PAdES-B-LTA).
-
.timestamp_request(imprint, nonce: nil, cert_req: true) ⇒ Object
Build an RFC 3161 TimeStampReq (DER) for
imprint(the SHA-256 of the bytes to timestamp, e.g. thetbs_bytesfrom #begin_timestamp). -
.timestamp_token_from_response(response) ⇒ Object
Extract the TimeStampToken (a CMS ContentInfo) from a TSA's RFC 3161 TimeStampResp
responsebytes. -
.verify_signatures(pdf) ⇒ Object
Validate every signature in
pdf. -
.version ⇒ Object
Native library version string.
Class Method Details
.add_dss(pdf, certs: [], crls: []) ⇒ Object
Append a Document Security Store (/DSS, PAdES-B-LT).
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 |
# File 'lib/rustpdf.rb', line 378 def add_dss(pdf, certs: [], crls: []) cptrs = certs.map { |c| Fiddle::Pointer[c] } rptrs = crls.map { |c| Fiddle::Pointer[c] } cptr_buf = cptrs.map(&:to_i).pack("J*") clen_buf = certs.map(&:bytesize).pack("J*") rptr_buf = rptrs.map(&:to_i).pack("J*") rlen_buf = crls.map(&:bytesize).pack("J*") result = take_bytes do |pp, pn| Native.call("pdf_add_dss", pdf, pdf.bytesize, cptr_buf, clen_buf, certs.size, rptr_buf, rlen_buf, crls.size, pp, pn) end # keep the per-item pointers alive until the call has returned cptrs.clear rptrs.clear result end |
.begin_signing(pdf, options: nil) ⇒ Object
Model B — two-phase signing, phase 1. Prepare pdf for deferred signing:
returns a SigningSession whose #hash you send to a remote HSM. Build the CMS
container, then call SigningSession#complete (or .complete_signature). The
key never reaches this library.
453 454 455 456 457 458 459 460 461 462 |
# File 'lib/rustpdf.rb', line 453 def begin_signing(pdf, options: nil) opts_bytes, keep = () doc_p = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE) doc_n = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE) tbs_p = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE) tbs_n = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE) check(Native.call("pdf_sign_begin", pdf, pdf.bytesize, opts_bytes, doc_p, doc_n, tbs_p, tbs_n)) keep.clear SigningSession.new(read_buffer(doc_p, doc_n), read_buffer(tbs_p, tbs_n)) end |
.begin_timestamp(pdf) ⇒ Object
Phase 1 of a network-TSA document timestamp (/DocTimeStamp). Prepares pdf
and returns [document_bytes, tbs_bytes]: tbs_bytes are the bytes whose
SHA-256 forms the RFC 3161 message imprint. Build a TimeStampReq with
#timestamp_request, POST it to the TSA, extract the token with
#timestamp_token_from_response, then embed it via #complete_signature.
479 480 481 482 483 484 485 486 |
# File 'lib/rustpdf.rb', line 479 def (pdf) doc_p = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE) doc_n = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE) tbs_p = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE) tbs_n = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE) check(Native.call("pdf_timestamp_begin", pdf, pdf.bytesize, doc_p, doc_n, tbs_p, tbs_n)) [read_buffer(doc_p, doc_n), read_buffer(tbs_p, tbs_n)] end |
.build_signing_options(options) ⇒ Object
Marshal a SigningOptions (or nil) into the C PdfSigningOptions struct bytes,
returning [packed_struct, keepalive] where keepalive holds the Fiddle
pointers backing the struct's string/byte fields (keep it referenced until
the native call returns). 64-bit layout: 3 ptr, 2 int (8 bytes together),
size_t, ptr, ptr, size_t, ptr, ptr, then the visible-signature tail:
int (+ 4 pad), size_t, double, ptr, ptr, size_t.
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 |
# File 'lib/rustpdf.rb', line 533 def () keep = [] cstr = lambda do |s| return 0 if s.nil? p = Fiddle::Pointer[s.to_s] keep << p p.to_i end reason = cstr.call(&.reason) location = cstr.call(&.location) name = cstr.call(&.name) pades = &.pades ? 1 : 0 cert = (&.certify || Certify::NONE).to_i est = (&.container_size || 0).to_i policy_oid = 0 policy_hash = 0 policy_hash_len = 0 policy_alg = 0 policy_uri = 0 if (pol = &.policy) policy_oid = cstr.call(pol.oid) if pol.hash && !pol.hash.empty? hp = Fiddle::Pointer[pol.hash] keep << hp policy_hash = hp.to_i policy_hash_len = pol.hash.bytesize end policy_alg = cstr.call(pol.hash_algorithm_oid) policy_uri = cstr.call(pol.uri) end visible = &.visible ? 1 : 0 vis_page = (&.visible_page || 0).to_i vis_rect = Array(&.visible_rect || [0.0, 0.0, 0.0, 0.0]).map(&:to_f)[0, 4] vis_rect += [0.0] * (4 - vis_rect.size) vis_text = cstr.call(&.visible_text) vis_image = 0 vis_image_len = 0 if (img = &.visible_image) && !img.empty? ip = Fiddle::Pointer[img] keep << ip vis_image = ip.to_i vis_image_len = img.bytesize end bytes = [reason, location, name].pack("J3") + [pades, cert].pack("l2") + [est, policy_oid, policy_hash, policy_hash_len, policy_alg, policy_uri].pack("J6") + [visible].pack("l") + "\x00\x00\x00\x00".b + # int visible + 4-byte pad [vis_page].pack("J") + vis_rect.pack("d4") + [vis_text, vis_image, vis_image_len].pack("J3") [bytes, keep] end |
.check(status) ⇒ Object
607 608 609 |
# File 'lib/rustpdf.rb', line 607 def check(status) raise Error.new(last_error, status) unless status.zero? end |
.complete_signature(document, container) ⇒ Object
Model B — phase 2. Embed a complete DER CMS / PKCS#7 container into a
prepared document (from #begin_signing), returning the final signed PDF.
466 467 468 469 470 |
# File 'lib/rustpdf.rb', line 466 def complete_signature(document, container) take_bytes do |pp, pn| Native.call("pdf_sign_complete", document, document.bytesize, container, container.bytesize, pp, pn) end end |
.extract_images_to_dir(pdf, dir) ⇒ Object
Extract every raster image into dir (JPEG verbatim as .jpg, others as
.png; files named pageN_name.ext). Returns the number written.
275 276 277 278 279 |
# File 'lib/rustpdf.rb', line 275 def extract_images_to_dir(pdf, dir) count = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE) check(Native.call("pdf_extract_images_to_dir", pdf, pdf.bytesize, dir, count)) count[0, Native::SIZEOF_SZ].unpack1("J") end |
.extract_page_text(pdf, page_index) ⇒ Object
Extract the text of a single 0-based page_index (Unicode via ToUnicode),
without building an intermediate one-page document.
268 269 270 271 |
# File 'lib/rustpdf.rb', line 268 def extract_page_text(pdf, page_index) take_bytes { |pp, pn| Native.call("pdf_extract_page_text", pdf, pdf.bytesize, page_index, pp, pn) } .force_encoding(Encoding::UTF_8) end |
.extract_text(pdf) ⇒ Object
Extract a document's text (Unicode via ToUnicode).
261 262 263 264 |
# File 'lib/rustpdf.rb', line 261 def extract_text(pdf) take_bytes { |pp, pn| Native.call("pdf_extract_text", pdf, pdf.bytesize, pp, pn) } .force_encoding(Encoding::UTF_8) end |
.find_text(pdf, query, case_sensitive: false) ⇒ Object
Find every occurrence of query in pdf, returning positional boxes.
Returns an Array of TextHit (page, text, x, y, width, height) in PDF points
(origin lower-left). case_sensitive defaults to false. An empty Array
means no match.
298 299 300 301 302 303 304 305 306 307 |
# File 'lib/rustpdf.rb', line 298 def find_text(pdf, query, case_sensitive: false) js = take_bytes do |pp, pn| Native.call("pdf_find_text_json", pdf, pdf.bytesize, query, case_sensitive ? 1 : 0, pp, pn) end.force_encoding(Encoding::UTF_8) return [] if js.empty? JSON.parse(js).map do |h| TextHit.new(h["page"], h["text"], h["x"], h["y"], h["width"], h["height"]) end end |
.inspect_pdf(pdf) ⇒ Object
Inspect pdf without mutating it: PDF version, PDF/A level (if any),
encryption posture and page count. Works even on password-protected files
(the encryption fields are still reported). Returns a PdfOverview. Named
inspect_pdf to avoid shadowing Object#inspect.
341 342 343 344 345 346 347 |
# File 'lib/rustpdf.rb', line 341 def inspect_pdf(pdf) js = take_bytes { |pp, pn| Native.call("pdf_inspect_json", pdf, pdf.bytesize, pp, pn) } .force_encoding(Encoding::UTF_8) o = JSON.parse(js) PdfOverview.new(o["version"], o["pdfaLevel"], o["encrypted"] ? true : false, o["encryption"], o["requiresPassword"] ? true : false, o["pageCount"]) end |
.last_error ⇒ Object
602 603 604 605 |
# File 'lib/rustpdf.rb', line 602 def last_error p = Native.call("pdf_last_error_message") p.null? ? "unknown error" : p.to_s end |
.list_signatures(pdf) ⇒ Object
List the signature fields in pdf (detect existing signatures before
signing). Returns an Array of SignatureField; an empty Array means there are
no signature fields.
511 512 513 514 515 516 517 518 519 520 521 522 523 |
# File 'lib/rustpdf.rb', line 511 def list_signatures(pdf) text = take_bytes { |pp, pn| Native.call("pdf_list_signatures", pdf, pdf.bytesize, pp, pn) } .force_encoding(Encoding::UTF_8) fields = [] text.each_line do |line| line = line.chomp tab = line.index("\t") next unless tab fields << SignatureField.new(line[(tab + 1)..-1], line[0...tab] == "1") end fields end |
.measure_page(pdf, index) ⇒ Object
Read the geometry of a single 0-based page of pdf. Raises IndexError if
index is out of range.
330 331 332 333 334 335 |
# File 'lib/rustpdf.rb', line 330 def measure_page(pdf, index) pages = measure_pages(pdf) raise IndexError, "page index #{index} out of range (#{pages.size} pages)" if index.negative? || index >= pages.size pages[index] end |
.measure_pages(pdf) ⇒ Object
Read the geometry (size, rotation, MediaBox, CropBox) of every page in pdf,
in page order, without mutating it. Returns an Array of PageGeometry. Sizes
are in PDF points; +rotated_width+/+rotated_height+ swap for 90/270 pages.
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 |
# File 'lib/rustpdf.rb', line 312 def measure_pages(pdf) js = take_bytes { |pp, pn| Native.call("pdf_measure_pages_json", pdf, pdf.bytesize, pp, pn) } .force_encoding(Encoding::UTF_8) return [] if js.empty? rect = lambda do |arr| a = Array(arr) a.size == 4 ? PdfRect.new(a[0], a[1], a[2], a[3]) : PdfRect.new(0, 0, 0, 0) end JSON.parse(js).map do |g| PageGeometry.new(g["page"], g["width"], g["height"], g["rotation"], g["rotatedWidth"], g["rotatedHeight"], rect.call(g["mediaBox"]), rect.call(g["cropBox"])) end end |
.out_int ⇒ Object
Run a producer { |out_int| status } and return the written int.
621 622 623 624 625 |
# File 'lib/rustpdf.rb', line 621 def out_int buf = Fiddle::Pointer.malloc(Native::SIZEOF_INT, Fiddle::RUBY_FREE) check(yield(buf)) buf[0, Native::SIZEOF_INT].unpack1("i!") end |
.page_count(pdf) ⇒ Object
Number of pages in pdf (free).
288 289 290 291 292 |
# File 'lib/rustpdf.rb', line 288 def page_count(pdf) count = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE) check(Native.call("pdf_page_count", pdf, pdf.bytesize, count)) count[0, Native::SIZEOF_SZ].unpack1("J") end |
.read_buffer(pp, pn) ⇒ Object
Read an out-buffer (pointer-to-pointer pp, pointer-to-len pn) into a
binary String, freeing the native buffer.
592 593 594 595 596 597 598 599 600 |
# File 'lib/rustpdf.rb', line 592 def read_buffer(pp, pn) len = pn[0, Native::SIZEOF_SZ].unpack1("J") return "".b if len.zero? dptr = pp.ptr bytes = dptr[0, len] Native.call("pdf_buffer_free", dptr, len) bytes end |
.render_page_to_png(pdf, page = 0, dpi = 150.0) ⇒ Object
Render page page (0-based) of pdf to a PNG image at dpi
dots-per-inch.
283 284 285 |
# File 'lib/rustpdf.rb', line 283 def render_page_to_png(pdf, page = 0, dpi = 150.0) take_bytes { |pp, pn| Native.call("pdf_render_page_to_png", pdf, pdf.bytesize, page, dpi.to_f, pp, pn) } end |
.sign(pdf, key_der, cert_der, reason: nil, location: nil, name: nil, pades: false) ⇒ Object
Sign a PDF (PKCS#7 detached, incremental update).
362 363 364 365 366 367 |
# File 'lib/rustpdf.rb', line 362 def sign(pdf, key_der, cert_der, reason: nil, location: nil, name: nil, pades: false) take_bytes do |pp, pn| Native.call("pdf_sign", pdf, pdf.bytesize, key_der, key_der.bytesize, cert_der, cert_der.bytesize, reason, location, name, pades ? 1 : 0, pp, pn) end end |
.sign_with(pdf, cert_der, chain: [], options: nil, &block) ⇒ Object
Model A — remote signer. Sign pdf without handing this library a key: it
builds the CMS signed attributes and calls the given block for the raw RSA
PKCS#1 v1.5 signature (over SHA-256 of the block's argument), then assembles
and embeds the CMS. cert_der is the signer certificate (DER); chain are
intermediate certificates (DER), supplied independently of the key. The
private key never reaches this library. Returns the signed PDF bytes.
403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 |
# File 'lib/rustpdf.rb', line 403 def sign_with(pdf, cert_der, chain: [], options: nil, &block) raise Error, "sign_with requires a block that produces the signature" unless block opts_bytes, keep = () cptrs = chain.map { |c| Fiddle::Pointer[c] } cptr_buf = cptrs.map(&:to_i).pack("J*") clen_buf = chain.map(&:bytesize).pack("J*") signer_error = nil closure = Fiddle::Closure::BlockCaller.new( Fiddle::TYPE_INT, [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T, Fiddle::TYPE_VOIDP] ) do |_ctx, data, data_len, sig_buf, sig_cap, sig_len| begin sig = block.call(data[0, data_len]).to_s if sig.bytesize > sig_cap signer_error = Error.new("signature (#{sig.bytesize} bytes) exceeds buffer capacity #{sig_cap}") next 2 end sig_buf[0, sig.bytesize] = sig sig_len[0, Native::SIZEOF_SZ] = [sig.bytesize].pack("J") 0 rescue StandardError => e signer_error = e 1 end end begin result = take_bytes do |pp, pn| Native.call("pdf_sign_with", pdf, pdf.bytesize, cert_der, cert_der.bytesize, cptr_buf, clen_buf, chain.size, opts_bytes, closure, Fiddle::NULL, pp, pn) end rescue Error raise signer_error if signer_error raise end # keep the callback, struct and per-cert pointers alive until the call returned cptrs.clear keep.clear closure.to_i # touch to keep it referenced past the native call result end |
.take_bytes ⇒ Object
Run an out-buffer producer { |out_ptr, out_len| status } and return the produced bytes, always freeing the native buffer.
613 614 615 616 617 618 |
# File 'lib/rustpdf.rb', line 613 def take_bytes pp = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE) pn = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE) check(yield(pp, pn)) read_buffer(pp, pn) end |
.timestamp(pdf, tsa_key_der, tsa_cert_der, date: nil) ⇒ Object
Append a document timestamp (/DocTimeStamp, PAdES-B-LTA).
370 371 372 373 374 375 |
# File 'lib/rustpdf.rb', line 370 def (pdf, tsa_key_der, tsa_cert_der, date: nil) take_bytes do |pp, pn| Native.call("pdf_timestamp", pdf, pdf.bytesize, tsa_key_der, tsa_key_der.bytesize, tsa_cert_der, tsa_cert_der.bytesize, date, pp, pn) end end |
.timestamp_request(imprint, nonce: nil, cert_req: true) ⇒ Object
Build an RFC 3161 TimeStampReq (DER) for imprint (the SHA-256 of the bytes
to timestamp, e.g. the tbs_bytes from #begin_timestamp). nonce is
optional; cert_req asks the TSA to embed its certificate. Returns the
request bytes to POST to the TSA.
492 493 494 495 496 497 |
# File 'lib/rustpdf.rb', line 492 def (imprint, nonce: nil, cert_req: true) take_bytes do |pp, pn| Native.call("pdf_timestamp_request", imprint, imprint.bytesize, nonce, nonce ? nonce.bytesize : 0, cert_req ? 1 : 0, pp, pn) end end |
.timestamp_token_from_response(response) ⇒ Object
Extract the TimeStampToken (a CMS ContentInfo) from a TSA's RFC 3161
TimeStampResp response bytes. The returned token is embedded via
#complete_signature(document, token).
502 503 504 505 506 |
# File 'lib/rustpdf.rb', line 502 def (response) take_bytes do |pp, pn| Native.call("pdf_timestamp_token_from_response", response, response.bytesize, pp, pn) end end |
.verify_signatures(pdf) ⇒ Object
Validate every signature in pdf. Returns one Hash per signature with keys
"field_name", "sub_filter", "signer", "covers_whole_document",
"digest_valid", "signature_valid", "is_valid" and "byte_range", plus the
richer certificate fields (any may be null): "issuer", "serial_number",
"valid_from", "valid_to", "algorithm", "signing_time", "cert_count" and
"has_timestamp". An empty array means the document is unsigned.
355 356 357 358 359 |
# File 'lib/rustpdf.rb', line 355 def verify_signatures(pdf) js = take_bytes { |pp, pn| Native.call("pdf_verify_signatures_json", pdf, pdf.bytesize, pp, pn) } .force_encoding(Encoding::UTF_8) js.empty? ? [] : JSON.parse(js) end |