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, plus feature licensing.
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
-
.activate_license(token) ⇒ Object
Activate a license token (unlocks PDF/A, signing, encryption, accessibility).
-
.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 — no license required). -
.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
.activate_license(token) ⇒ Object
Activate a license token (unlocks PDF/A, signing, encryption, accessibility). Tokens may also be supplied via the RUSTPDF_LICENSE / RUSTPDF_LICENSE_FILE environment variables (auto-activated).
263 264 265 |
# File 'lib/rustpdf.rb', line 263 def activate_license(token) check(Native.call("pdf_activate_license", token)) end |
.add_dss(pdf, certs: [], crls: []) ⇒ Object
Append a Document Security Store (/DSS, PAdES-B-LT).
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 |
# File 'lib/rustpdf.rb', line 386 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.
461 462 463 464 465 466 467 468 469 470 |
# File 'lib/rustpdf.rb', line 461 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.
487 488 489 490 491 492 493 494 |
# File 'lib/rustpdf.rb', line 487 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.
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 589 590 591 592 593 594 595 596 |
# File 'lib/rustpdf.rb', line 541 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
615 616 617 |
# File 'lib/rustpdf.rb', line 615 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.
474 475 476 477 478 |
# File 'lib/rustpdf.rb', line 474 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.
282 283 284 285 286 |
# File 'lib/rustpdf.rb', line 282 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.
275 276 277 278 |
# File 'lib/rustpdf.rb', line 275 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).
268 269 270 271 |
# File 'lib/rustpdf.rb', line 268 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.
306 307 308 309 310 311 312 313 314 315 |
# File 'lib/rustpdf.rb', line 306 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.
349 350 351 352 353 354 355 |
# File 'lib/rustpdf.rb', line 349 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
610 611 612 613 |
# File 'lib/rustpdf.rb', line 610 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.
519 520 521 522 523 524 525 526 527 528 529 530 531 |
# File 'lib/rustpdf.rb', line 519 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.
338 339 340 341 342 343 |
# File 'lib/rustpdf.rb', line 338 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.
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 |
# File 'lib/rustpdf.rb', line 320 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.
629 630 631 632 633 |
# File 'lib/rustpdf.rb', line 629 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 — no license required).
296 297 298 299 300 |
# File 'lib/rustpdf.rb', line 296 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.
600 601 602 603 604 605 606 607 608 |
# File 'lib/rustpdf.rb', line 600 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. Page rendering is a licensed Pro feature: raises unless a
license granting it is active.
291 292 293 |
# File 'lib/rustpdf.rb', line 291 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). Requires a license.
370 371 372 373 374 375 |
# File 'lib/rustpdf.rb', line 370 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.
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 448 449 450 451 452 453 454 455 |
# File 'lib/rustpdf.rb', line 411 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.
621 622 623 624 625 626 |
# File 'lib/rustpdf.rb', line 621 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).
378 379 380 381 382 383 |
# File 'lib/rustpdf.rb', line 378 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.
500 501 502 503 504 505 |
# File 'lib/rustpdf.rb', line 500 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).
510 511 512 513 514 |
# File 'lib/rustpdf.rb', line 510 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.
363 364 365 366 367 |
# File 'lib/rustpdf.rb', line 363 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 |