rustpdf 0.4.3-universal-darwin → 0.4.4-universal-darwin

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8555af56c718d5e47642664e2823beaccb27b5bc5f1474080cebffa741033610
4
- data.tar.gz: 0537bc4fbcf67a7ca00d5f6feba51908545e7597397ce2a1e7c867c408a350a5
3
+ metadata.gz: 26be79c246143805efc4c3a4551e4afc5a189bc808f4554285b13dd533b90b6f
4
+ data.tar.gz: 3bd71a7b39e767292d447821de06952d60f0b79c2222aa77f7f775390b0c09dc
5
5
  SHA512:
6
- metadata.gz: 9d02734ec42fe90c43c9cf736f1114d4a8d5f12bfade04e8647ef79adacc6799fdf876322b0604befb13c0a8813db45e1f612fec5255eaaf01668c8ef23fdb3f
7
- data.tar.gz: 179e5dca40c02e68651660c13e174d7d3b926eee61a8d4cac8e8e0f8bbe4c5a8aab8b216ad582244a3027f4be12649061190d4376e176d9aded3cf8929c0a07a
6
+ metadata.gz: b196d1f22eff0fe24fb7ca9b4d61bf30670a1f7e847e7c5bd847790b5dc6913ad87c1adffa123f9b41c51548a3be421747da752c342a5d06beeec8f2368b8a27
7
+ data.tar.gz: f5a0d5b8ee82a2ab459a632058ada47f4c36a54474fe2e62fe33504cb6b5c4991b8cc0bd0cb9b9614d298006c338c88491da32a898c169c164e4b961fb90c6b7
data/README.md CHANGED
@@ -49,6 +49,38 @@ signed = RustPdf.sign(data, key_der, cert_der, pades: true)
49
49
  Corporate features (PDF/A, signing, encryption, accessibility, page rendering — a **Pro** feature) require a license;
50
50
  without one they raise `RustPdf::Error`. See [`docs/LICENSING.md`](../../docs/LICENSING.md).
51
51
 
52
+ ## Deferred / HSM signing
53
+
54
+ Sign without the private key ever entering this library — the key stays in an
55
+ HSM, cloud KMS, smartcard or PKI token. Works with any PKI (eIDAS, AATL, or
56
+ your own CA). The library builds the CMS and asks your code only for the raw
57
+ RSA signature over the bytes it hands you.
58
+
59
+ ```ruby
60
+ require "rustpdf"
61
+
62
+ pdf = File.binread("contract.pdf")
63
+ cert_der = File.binread("signing-cert.der") # X.509 signer certificate (DER)
64
+
65
+ # Model A — your block returns the raw RSA PKCS#1 v1.5 signature; the key
66
+ # never reaches the library. Call your HSM / cloud KMS / token here.
67
+ signed = RustPdf.sign_with(pdf, cert_der,
68
+ chain: [issuer_der],
69
+ options: RustPdf::SigningOptions.new(reason: "Approved")) do |to_sign|
70
+ hsm.sign_rsa_sha256(to_sign) # bytes in, raw signature out
71
+ end
72
+ File.binwrite("contract.signed.pdf", signed)
73
+
74
+ # Model B — two-phase: prepare, sign the hash out of band, then complete.
75
+ session = RustPdf.begin_signing(pdf)
76
+ container = remote.build_cms(session.hash) # send #hash to a remote signer
77
+ final = session.complete(container) # embed the DER CMS / PKCS#7
78
+ ```
79
+
80
+ `RustPdf.list_signatures(pdf)` inventories existing signature fields before you
81
+ sign. See [`docs/ruby.html`](../../site/public/docs/ruby.html#sign) for the full
82
+ deferred-signing API (`SigningOptions`, `Certify`, `SignaturePolicy`).
83
+
52
84
  ## Test
53
85
 
54
86
  ```sh
@@ -127,16 +127,22 @@ module RustPdf
127
127
  text.split("\n").reject(&:empty?)
128
128
  end
129
129
 
130
- # Stamp a diagonal text watermark across every page.
131
- def watermark_text(text, size: 64.0, color: [0.5, 0.5, 0.5], opacity: 0.30, rotation_deg: 45.0)
130
+ # Stamp a diagonal text watermark across every page. When
131
+ # +opaque_background+ is true, the text is drawn over an opaque white box
132
+ # (otherwise it is blended into the page content).
133
+ def watermark_text(text, size: 64.0, color: [0.5, 0.5, 0.5], opacity: 0.30,
134
+ rotation_deg: 45.0, opaque_background: false)
132
135
  r, g, b = color
133
- RustPdf.check(Native.call("pdf_editable_watermark_text", ptr, text, size, r, g, b, opacity, rotation_deg))
136
+ RustPdf.check(Native.call("pdf_editable_watermark_text", ptr, text, size, r, g, b,
137
+ opacity, rotation_deg, opaque_background ? 1 : 0))
134
138
  self
135
139
  end
136
140
 
137
- # Stamp an image watermark (from a file) across every page.
138
- def watermark_image_file(path, width, height, opacity: 0.30)
139
- RustPdf.check(Native.call("pdf_editable_watermark_image_file", ptr, path, width, height, opacity))
141
+ # Stamp an image watermark (from a file) across every page, rotated
142
+ # +rotation_deg+ degrees counter-clockwise.
143
+ def watermark_image_file(path, width, height, opacity: 0.30, rotation_deg: 0.0)
144
+ RustPdf.check(Native.call("pdf_editable_watermark_image_file", ptr, path, width, height,
145
+ opacity, rotation_deg))
140
146
  self
141
147
  end
142
148
 
@@ -156,6 +162,29 @@ module RustPdf
156
162
  self
157
163
  end
158
164
 
165
+ # ---- version normalization (issue #41 P1) -------------------------------
166
+
167
+ # Set the output PDF version (downgrade/normalize). +version+ uses the
168
+ # RustPdf::Version codes (V1_4=0, V1_5=1, V1_7=2, V2_0=3).
169
+ def set_version(version)
170
+ RustPdf.check(Native.call("pdf_editable_set_version", ptr, version))
171
+ self
172
+ end
173
+
174
+ # Strip PDF/A conformance (OutputIntents, XMP pdfaid, /Version) so the file
175
+ # is a plain PDF.
176
+ def strip_pdfa
177
+ RustPdf.check(Native.call("pdf_editable_strip_pdfa", ptr))
178
+ self
179
+ end
180
+
181
+ # Normalize to a plain PDF at +version+ (strip PDF/A + set version). Codes as
182
+ # in #set_version.
183
+ def normalize(version = Version::V1_7)
184
+ RustPdf.check(Native.call("pdf_editable_normalize", ptr, version))
185
+ self
186
+ end
187
+
159
188
  def optimize
160
189
  RustPdf.check(Native.call("pdf_editable_optimize", ptr))
161
190
  self
@@ -97,13 +97,32 @@ module RustPdf
97
97
  "pdf_editable_set_choice" => [[VP, VP, VP, VP], I],
98
98
  "pdf_editable_flatten_forms" => [[VP], I],
99
99
  "pdf_editable_field_names" => [[VP, VP, VP], I],
100
- "pdf_editable_watermark_text" => [[VP, VP, D, D, D, D, D, D], I],
101
- "pdf_editable_watermark_image_file" => [[VP, VP, D, D, D], I],
100
+ # watermark_text gained a trailing int opaque_background; watermark_image
101
+ # gained a trailing double rotation_deg (issue #41 P1).
102
+ "pdf_editable_watermark_text" => [[VP, VP, D, D, D, D, D, D, I], I],
103
+ "pdf_editable_watermark_image_file" => [[VP, VP, D, D, D, D], I],
102
104
  # Tier 2: redaction + PDF/A conversion (EditableDoc)
103
105
  "pdf_editable_redact" => [[VP, SZ, VP, SZ, VP], I],
104
106
  "pdf_editable_convert_to_pdfa" => [[VP, I], I],
105
107
  # Tier 2: signature validation (module-level)
106
108
  "pdf_verify_signatures_json" => [[VP, SZ, VP, VP], I],
109
+
110
+ # Deferred / external (HSM) signing — issue #41 P0. The PdfSigningOptions
111
+ # struct crosses as an opaque pointer (VP); the Model-A callback is a
112
+ # Fiddle::Closure passed as a function pointer (VP).
113
+ "pdf_sign_begin" => [[VP, SZ, VP, VP, VP, VP, VP], I],
114
+ "pdf_sign_complete" => [[VP, SZ, VP, SZ, VP, VP], I],
115
+ "pdf_sign_with" => [[VP, SZ, VP, SZ, VP, VP, SZ, VP, VP, VP, VP, VP], I],
116
+ "pdf_list_signatures" => [[VP, SZ, VP, VP], I],
117
+
118
+ # Issue #41 P1: positional text search, version normalization, network TSA.
119
+ "pdf_find_text_json" => [[VP, SZ, VP, I, VP, VP], I],
120
+ "pdf_editable_set_version" => [[VP, I], I],
121
+ "pdf_editable_strip_pdfa" => [[VP], I],
122
+ "pdf_editable_normalize" => [[VP, I], I],
123
+ "pdf_timestamp_begin" => [[VP, SZ, VP, VP, VP, VP], I],
124
+ "pdf_timestamp_request" => [[VP, SZ, VP, SZ, I, VP, VP], I],
125
+ "pdf_timestamp_token_from_response" => [[VP, SZ, VP, VP], I],
107
126
  }.freeze
108
127
 
109
128
  def lib
data/lib/rustpdf.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "fiddle"
2
2
  require "json"
3
+ require "digest"
3
4
  require_relative "rustpdf/native"
4
5
 
5
6
  # Idiomatic Ruby binding for the rust-pdf core over its C ABI (libpdf_ffi),
@@ -55,6 +56,14 @@ module RustPdf
55
56
  AES256 = 2
56
57
  end
57
58
 
59
+ # Output PDF version (for set_version / normalize).
60
+ module Version
61
+ V1_4 = 0
62
+ V1_5 = 1
63
+ V1_7 = 2
64
+ V2_0 = 3
65
+ end
66
+
58
67
  # ZUGFeRD / Factur-X conformance profiles.
59
68
  module FacturxProfile
60
69
  MINIMUM = 0
@@ -91,6 +100,89 @@ module RustPdf
91
100
  end
92
101
  end
93
102
 
103
+ # DocMDP certification level applied by the first (certifying) signature.
104
+ module Certify
105
+ NONE = 0 # not a certifying signature
106
+ LOCKED = 1 # /P 1 — no changes permitted after signing
107
+ FORMS = 2 # /P 2 — form-filling and signing permitted
108
+ FORMS_AND_ANNOTATIONS = 3 # /P 3 — also annotations permitted
109
+ end
110
+
111
+ # A signature-policy identifier (PAdES-EPES / ICP-Brasil AD-RB).
112
+ class SignaturePolicy
113
+ attr_accessor :oid, :hash, :hash_algorithm_oid, :uri
114
+
115
+ # +oid+: dotted-decimal policy OID; +hash+: policy document hash bytes
116
+ # (under +hash_algorithm_oid+, nil = SHA-256); +uri+: optional SPURI.
117
+ def initialize(oid:, hash:, hash_algorithm_oid: nil, uri: nil)
118
+ @oid = oid
119
+ @hash = hash
120
+ @hash_algorithm_oid = hash_algorithm_oid
121
+ @uri = uri
122
+ end
123
+ end
124
+
125
+ # Options for deferred / external signing (issue #41). The +visible_*+ fields
126
+ # request a visible signature appearance: +visible_rect+ is [x0,y0,x1,y1] in
127
+ # points, +visible_text+ is newline-separated appearance lines, and
128
+ # +visible_image+ is PNG/JPEG bytes of a handwritten-signature image.
129
+ class SigningOptions
130
+ attr_accessor :reason, :location, :name, :pades, :certify, :container_size, :policy,
131
+ :visible, :visible_page, :visible_rect, :visible_text, :visible_image
132
+
133
+ def initialize(reason: nil, location: nil, name: nil, pades: false,
134
+ certify: Certify::NONE, container_size: 0, policy: nil,
135
+ visible: false, visible_page: 0, visible_rect: nil,
136
+ visible_text: nil, visible_image: nil)
137
+ @reason = reason
138
+ @location = location
139
+ @name = name
140
+ @pades = pades
141
+ @certify = certify
142
+ @container_size = container_size
143
+ @policy = policy
144
+ @visible = visible
145
+ @visible_page = visible_page
146
+ @visible_rect = visible_rect
147
+ @visible_text = visible_text
148
+ @visible_image = visible_image
149
+ end
150
+ end
151
+
152
+ # A signature field discovered in a PDF (pre-signing inventory). +signed+ is
153
+ # true when the field already carries a signature.
154
+ SignatureField = Struct.new(:name, :signed)
155
+
156
+ # One positional text match from #find_text. Coordinates are in PDF points,
157
+ # origin lower-left; +x+/+y+ is the lower-left corner of the box.
158
+ TextHit = Struct.new(:page, :text, :x, :y, :width, :height)
159
+
160
+ # An in-progress two-phase signature: #document holds the placeholder PDF and
161
+ # #bytes the exact bytes the signature covers. Hand #hash to a remote signer,
162
+ # build the CMS container, then call #complete.
163
+ class SigningSession
164
+ # The prepared PDF (with a zero-filled /Contents placeholder).
165
+ attr_reader :document
166
+ # The exact bytes covered by the signature (the two ByteRange segments).
167
+ attr_reader :bytes
168
+
169
+ def initialize(document, bytes)
170
+ @document = document
171
+ @bytes = bytes
172
+ end
173
+
174
+ # SHA-256 of #bytes — the 32-byte value an HSM signs.
175
+ def hash
176
+ Digest::SHA256.digest(@bytes)
177
+ end
178
+
179
+ # Phase 2: complete the signature by embedding a finished DER CMS / PKCS#7
180
+ # +container+, returning the final signed PDF.
181
+ def complete(container)
182
+ RustPdf.complete_signature(@document, container)
183
+ end
184
+ end
185
+
94
186
  module_function
95
187
 
96
188
  # Native library version string.
@@ -133,10 +225,27 @@ module RustPdf
133
225
  count[0, Native::SIZEOF_SZ].unpack1("J")
134
226
  end
135
227
 
228
+ # Find every occurrence of +query+ in +pdf+, returning positional boxes.
229
+ # Returns an Array of TextHit (page, text, x, y, width, height) in PDF points
230
+ # (origin lower-left). +case_sensitive+ defaults to false. An empty Array
231
+ # means no match.
232
+ def find_text(pdf, query, case_sensitive: false)
233
+ js = take_bytes do |pp, pn|
234
+ Native.call("pdf_find_text_json", pdf, pdf.bytesize, query, case_sensitive ? 1 : 0, pp, pn)
235
+ end.force_encoding(Encoding::UTF_8)
236
+ return [] if js.empty?
237
+
238
+ JSON.parse(js).map do |h|
239
+ TextHit.new(h["page"], h["text"], h["x"], h["y"], h["width"], h["height"])
240
+ end
241
+ end
242
+
136
243
  # Validate every signature in +pdf+. Returns one Hash per signature with keys
137
244
  # "field_name", "sub_filter", "signer", "covers_whole_document",
138
- # "digest_valid", "signature_valid", "is_valid" and "byte_range". An empty
139
- # array means the document is unsigned.
245
+ # "digest_valid", "signature_valid", "is_valid" and "byte_range", plus the
246
+ # richer certificate fields (any may be null): "issuer", "serial_number",
247
+ # "valid_from", "valid_to", "algorithm", "signing_time", "cert_count" and
248
+ # "has_timestamp". An empty array means the document is unsigned.
140
249
  def verify_signatures(pdf)
141
250
  js = take_bytes { |pp, pn| Native.call("pdf_verify_signatures_json", pdf, pdf.bytesize, pp, pn) }
142
251
  .force_encoding(Encoding::UTF_8)
@@ -177,8 +286,213 @@ module RustPdf
177
286
  result
178
287
  end
179
288
 
289
+ # ---- deferred / external (HSM) signing — issue #41 ------------------------
290
+
291
+ # Model A — remote signer. Sign +pdf+ without handing this library a key: it
292
+ # builds the CMS signed attributes and calls the given block for the raw RSA
293
+ # PKCS#1 v1.5 signature (over SHA-256 of the block's argument), then assembles
294
+ # and embeds the CMS. +cert_der+ is the signer certificate (DER); +chain+ are
295
+ # intermediate certificates (DER), supplied independently of the key. The
296
+ # private key never reaches this library. Returns the signed PDF bytes.
297
+ def sign_with(pdf, cert_der, chain: [], options: nil, &block)
298
+ raise Error, "sign_with requires a block that produces the signature" unless block
299
+
300
+ opts_bytes, keep = build_signing_options(options)
301
+ cptrs = chain.map { |c| Fiddle::Pointer[c] }
302
+ cptr_buf = cptrs.map(&:to_i).pack("J*")
303
+ clen_buf = chain.map(&:bytesize).pack("J*")
304
+
305
+ signer_error = nil
306
+ closure = Fiddle::Closure::BlockCaller.new(
307
+ Fiddle::TYPE_INT,
308
+ [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T,
309
+ Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T, Fiddle::TYPE_VOIDP]
310
+ ) do |_ctx, data, data_len, sig_buf, sig_cap, sig_len|
311
+ begin
312
+ sig = block.call(data[0, data_len]).to_s
313
+ if sig.bytesize > sig_cap
314
+ signer_error = Error.new("signature (#{sig.bytesize} bytes) exceeds buffer capacity #{sig_cap}")
315
+ next 2
316
+ end
317
+ sig_buf[0, sig.bytesize] = sig
318
+ sig_len[0, Native::SIZEOF_SZ] = [sig.bytesize].pack("J")
319
+ 0
320
+ rescue StandardError => e
321
+ signer_error = e
322
+ 1
323
+ end
324
+ end
325
+
326
+ begin
327
+ result = take_bytes do |pp, pn|
328
+ Native.call("pdf_sign_with", pdf, pdf.bytesize, cert_der, cert_der.bytesize,
329
+ cptr_buf, clen_buf, chain.size, opts_bytes, closure, Fiddle::NULL, pp, pn)
330
+ end
331
+ rescue Error
332
+ raise signer_error if signer_error
333
+
334
+ raise
335
+ end
336
+ # keep the callback, struct and per-cert pointers alive until the call returned
337
+ cptrs.clear
338
+ keep.clear
339
+ closure.to_i # touch to keep it referenced past the native call
340
+ result
341
+ end
342
+
343
+ # Model B — two-phase signing, phase 1. Prepare +pdf+ for deferred signing:
344
+ # returns a SigningSession whose #hash you send to a remote HSM. Build the CMS
345
+ # container, then call SigningSession#complete (or .complete_signature). The
346
+ # key never reaches this library.
347
+ def begin_signing(pdf, options: nil)
348
+ opts_bytes, keep = build_signing_options(options)
349
+ doc_p = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
350
+ doc_n = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
351
+ tbs_p = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
352
+ tbs_n = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
353
+ check(Native.call("pdf_sign_begin", pdf, pdf.bytesize, opts_bytes, doc_p, doc_n, tbs_p, tbs_n))
354
+ keep.clear
355
+ SigningSession.new(read_buffer(doc_p, doc_n), read_buffer(tbs_p, tbs_n))
356
+ end
357
+
358
+ # Model B — phase 2. Embed a complete DER CMS / PKCS#7 +container+ into a
359
+ # prepared +document+ (from #begin_signing), returning the final signed PDF.
360
+ def complete_signature(document, container)
361
+ take_bytes do |pp, pn|
362
+ Native.call("pdf_sign_complete", document, document.bytesize, container, container.bytesize, pp, pn)
363
+ end
364
+ end
365
+
366
+ # ---- network TSA (AD-RT) document timestamp — issue #41 -------------------
367
+
368
+ # Phase 1 of a network-TSA document timestamp (/DocTimeStamp). Prepares +pdf+
369
+ # and returns [document_bytes, tbs_bytes]: +tbs_bytes+ are the bytes whose
370
+ # SHA-256 forms the RFC 3161 message imprint. Build a TimeStampReq with
371
+ # #timestamp_request, POST it to the TSA, extract the token with
372
+ # #timestamp_token_from_response, then embed it via #complete_signature.
373
+ def begin_timestamp(pdf)
374
+ doc_p = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
375
+ doc_n = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
376
+ tbs_p = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
377
+ tbs_n = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
378
+ check(Native.call("pdf_timestamp_begin", pdf, pdf.bytesize, doc_p, doc_n, tbs_p, tbs_n))
379
+ [read_buffer(doc_p, doc_n), read_buffer(tbs_p, tbs_n)]
380
+ end
381
+
382
+ # Build an RFC 3161 TimeStampReq (DER) for +imprint+ (the SHA-256 of the bytes
383
+ # to timestamp, e.g. the +tbs_bytes+ from #begin_timestamp). +nonce+ is
384
+ # optional; +cert_req+ asks the TSA to embed its certificate. Returns the
385
+ # request bytes to POST to the TSA.
386
+ def timestamp_request(imprint, nonce: nil, cert_req: true)
387
+ take_bytes do |pp, pn|
388
+ Native.call("pdf_timestamp_request", imprint, imprint.bytesize,
389
+ nonce, nonce ? nonce.bytesize : 0, cert_req ? 1 : 0, pp, pn)
390
+ end
391
+ end
392
+
393
+ # Extract the TimeStampToken (a CMS ContentInfo) from a TSA's RFC 3161
394
+ # TimeStampResp +response+ bytes. The returned token is embedded via
395
+ # #complete_signature(document, token).
396
+ def timestamp_token_from_response(response)
397
+ take_bytes do |pp, pn|
398
+ Native.call("pdf_timestamp_token_from_response", response, response.bytesize, pp, pn)
399
+ end
400
+ end
401
+
402
+ # List the signature fields in +pdf+ (detect existing signatures before
403
+ # signing). Returns an Array of SignatureField; an empty Array means there are
404
+ # no signature fields.
405
+ def list_signatures(pdf)
406
+ text = take_bytes { |pp, pn| Native.call("pdf_list_signatures", pdf, pdf.bytesize, pp, pn) }
407
+ .force_encoding(Encoding::UTF_8)
408
+ fields = []
409
+ text.each_line do |line|
410
+ line = line.chomp
411
+ tab = line.index("\t")
412
+ next unless tab
413
+
414
+ fields << SignatureField.new(line[(tab + 1)..-1], line[0...tab] == "1")
415
+ end
416
+ fields
417
+ end
418
+
180
419
  # ---- internal helpers (used by Document/EditableDoc) ----------------------
181
420
 
421
+ # Marshal a SigningOptions (or nil) into the C PdfSigningOptions struct bytes,
422
+ # returning [packed_struct, keepalive] where +keepalive+ holds the Fiddle
423
+ # pointers backing the struct's string/byte fields (keep it referenced until
424
+ # the native call returns). 64-bit layout: 3 ptr, 2 int (8 bytes together),
425
+ # size_t, ptr, ptr, size_t, ptr, ptr, then the visible-signature tail:
426
+ # int (+ 4 pad), size_t, double[4], ptr, ptr, size_t.
427
+ def build_signing_options(options)
428
+ keep = []
429
+ cstr = lambda do |s|
430
+ return 0 if s.nil?
431
+
432
+ p = Fiddle::Pointer[s.to_s]
433
+ keep << p
434
+ p.to_i
435
+ end
436
+
437
+ reason = cstr.call(options&.reason)
438
+ location = cstr.call(options&.location)
439
+ name = cstr.call(options&.name)
440
+ pades = options&.pades ? 1 : 0
441
+ cert = (options&.certify || Certify::NONE).to_i
442
+ est = (options&.container_size || 0).to_i
443
+
444
+ policy_oid = 0
445
+ policy_hash = 0
446
+ policy_hash_len = 0
447
+ policy_alg = 0
448
+ policy_uri = 0
449
+ if (pol = options&.policy)
450
+ policy_oid = cstr.call(pol.oid)
451
+ if pol.hash && !pol.hash.empty?
452
+ hp = Fiddle::Pointer[pol.hash]
453
+ keep << hp
454
+ policy_hash = hp.to_i
455
+ policy_hash_len = pol.hash.bytesize
456
+ end
457
+ policy_alg = cstr.call(pol.hash_algorithm_oid)
458
+ policy_uri = cstr.call(pol.uri)
459
+ end
460
+
461
+ visible = options&.visible ? 1 : 0
462
+ vis_page = (options&.visible_page || 0).to_i
463
+ vis_rect = Array(options&.visible_rect || [0.0, 0.0, 0.0, 0.0]).map(&:to_f)[0, 4]
464
+ vis_rect += [0.0] * (4 - vis_rect.size)
465
+ vis_text = cstr.call(options&.visible_text)
466
+ vis_image = 0
467
+ vis_image_len = 0
468
+ if (img = options&.visible_image) && !img.empty?
469
+ ip = Fiddle::Pointer[img]
470
+ keep << ip
471
+ vis_image = ip.to_i
472
+ vis_image_len = img.bytesize
473
+ end
474
+
475
+ bytes = [reason, location, name].pack("J3") +
476
+ [pades, cert].pack("l2") +
477
+ [est, policy_oid, policy_hash, policy_hash_len, policy_alg, policy_uri].pack("J6") +
478
+ [visible].pack("l") + "\x00\x00\x00\x00".b + # int visible + 4-byte pad
479
+ [vis_page].pack("J") + vis_rect.pack("d4") +
480
+ [vis_text, vis_image, vis_image_len].pack("J3")
481
+ [bytes, keep]
482
+ end
483
+
484
+ # Read an out-buffer (pointer-to-pointer +pp+, pointer-to-len +pn+) into a
485
+ # binary String, freeing the native buffer.
486
+ def read_buffer(pp, pn)
487
+ len = pn[0, Native::SIZEOF_SZ].unpack1("J")
488
+ return "".b if len.zero?
489
+
490
+ dptr = pp.ptr
491
+ bytes = dptr[0, len]
492
+ Native.call("pdf_buffer_free", dptr, len)
493
+ bytes
494
+ end
495
+
182
496
  def last_error
183
497
  p = Native.call("pdf_last_error_message")
184
498
  p.null? ? "unknown error" : p.to_s
@@ -194,13 +508,7 @@ module RustPdf
194
508
  pp = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
195
509
  pn = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
196
510
  check(yield(pp, pn))
197
- len = pn[0, Native::SIZEOF_SZ].unpack1("J")
198
- return "".b if len.zero?
199
-
200
- dptr = pp.ptr
201
- bytes = dptr[0, len]
202
- Native.call("pdf_buffer_free", dptr, len)
203
- bytes
511
+ read_buffer(pp, pn)
204
512
  end
205
513
 
206
514
  # Run a producer { |out_int| status } and return the written int.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rustpdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.4.4
5
5
  platform: universal-darwin
6
6
  authors:
7
7
  - rust-pdf
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-29 00:00:00.000000000 Z
11
+ date: 2026-06-30 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Idiomatic Ruby binding over the rust-pdf C ABI (libpdf_ffi) using the
14
14
  built-in Fiddle stdlib.