rustpdf 0.4.5-universal-darwin → 0.4.8-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: bf26136e96e0bb69576cf294f81bbe62b5b9974c4a0646fac69ed40e72ffa82d
4
- data.tar.gz: f60566e9fc77e467e8b29328769a86fd3ec013d5a93a00bc7453ba8a12e7cef7
3
+ metadata.gz: '082524dbee70acfb9ee95fc16b1f5892159a7f63da57ab77061956dc365c19d3'
4
+ data.tar.gz: 7cd26d42a12ecacb24f1526da4b5a642556c887769f1d6243d43def4e7b914ea
5
5
  SHA512:
6
- metadata.gz: c7330892e700726c8f1f8d4b3e8bd4876afc20b3d24909d8e3a3bc154f5a3130a9fe456aadbae0b1f31c8a3dd33046c17945cc6cf32bb0f3f1a55d16be67dd40
7
- data.tar.gz: ddaaf60885bdccea7dd06eb990d9af4b2e021b313f51a8f5e9dd035de833c1d15a8ba6981b6b8a0359dde450c8669bf977b221a3a18a45f7e6bcc8e8b8a06294
6
+ metadata.gz: aaf0e097415445a9340e833906ae8165596cd417aceba6aff60bd7ae8508c1a825ef9015dc1811b00938c94130eb0fdef33f4b18b314a687b72de69bf17c2233
7
+ data.tar.gz: 43b7c488e1479a1b792da82337430eed855d032dbd5e3bb6a9db0102c5fc25874ffaaad4e80c9a7b54d82ebdca7a996378decea7f85adc338360bea448df699e
data/README.md CHANGED
@@ -1,21 +1,16 @@
1
- # rustpdf (Ruby binding)
2
-
3
- Idiomatic Ruby binding for the `rust-pdf` core over its C ABI (`libpdf_ffi`),
4
- using the built-in **Fiddle** standard library — no native gem to compile. It
5
- covers the whole product surface: vector graphics, embedded/subsetted fonts and
6
- text, wrapping paragraphs, images, **PDF/A** (levels 1b–3a),
7
- **tagged/accessible** output, embedded-file attachments, **AcroForm** fields,
8
- manipulation (merge/split/rotate/optimize/incremental update), **text
9
- extraction**, **page rendering** (page to PNG image), **encryption** (RC4 / AES-128 / AES-256) and **digital signatures**
10
- (PKCS#7 / PAdES) — plus **feature licensing**.
11
-
12
- Files (module `RustPdf`):
13
-
14
- * `lib/rustpdf.rb` — module functions (`version`, `activate_license`,
15
- `extract_text`, `sign`, `timestamp`, `add_dss`), enums, error, helpers;
16
- * `lib/rustpdf/native.rb` — the Fiddle signature table + loader;
17
- * `lib/rustpdf/document.rb`, `editable_doc.rb` — the `Document` / `EditableDoc`
18
- classes.
1
+ # RustPdf for Ruby
2
+
3
+ Generate, edit, sign and process PDFs from Ruby: vector graphics, embedded fonts and Unicode text, wrapping paragraphs, images, **PDF/A** (1b-4f), **tagged/accessible** output, attachments, **AcroForm** fields, page manipulation (merge/split/stamp), watermarks, true **redaction**, **AES-256** encryption, **digital signatures (PAdES)** with HSM/deferred signing, timestamps/LTV, text extraction and search, and page **rendering to PNG**. Pure Ruby gem (stdlib Fiddle), no native extension to compile.
4
+
5
+ ## Documentation
6
+
7
+ - **Full API reference:** https://rustpdf.dev/docs/ruby
8
+ - **Interactive positioning guide** (coordinates, anchors, rotation): https://rustpdf.dev/positioning
9
+ - All product guides (PDF/A, signatures, encryption, redaction, rendering): https://rustpdf.dev/docs/
10
+
11
+ The public API is `RustPdf::Document` (create PDFs), `RustPdf::EditableDoc`
12
+ (load and edit existing PDFs) and module functions (`version`,
13
+ `activate_license`, `extract_text`, `sign`, `timestamp`, `add_dss`).
19
14
 
20
15
  ## Loading the native library
21
16
 
@@ -163,18 +163,149 @@ module RustPdf
163
163
  found != 0
164
164
  end
165
165
 
166
- # Draw a line of positioned +text+ with baseline at (+x+, +y+) on page
167
- # +page_index+ (0-based), using standard Helvetica at +size+ points in RGB
166
+ # Register a TrueType/OpenType font (from a file path) for text stamping;
167
+ # returns a font id usable with the +font_id+ keyword of #place_text /
168
+ # #masked_text / #place_paragraph. The font is embedded as a subset —
169
+ # stamped text renders with the real font's glyphs and metrics.
170
+ def add_font_file(path)
171
+ RustPdf.out_int { |buf| Native.call("pdf_editable_add_font_file", ptr, path, buf) }
172
+ end
173
+
174
+ # Register a stamping font from raw TrueType/OpenType bytes. See
175
+ # #add_font_file.
176
+ def add_font(data)
177
+ RustPdf.out_int { |buf| Native.call("pdf_editable_add_font", ptr, data, data.bytesize, buf) }
178
+ end
179
+
180
+ # Draw a line of positioned +text+ anchored at (+x+, +y+) on page
181
+ # +page_index+ (0-based), using standard Helvetica (or an embedded font —
182
+ # pass +font_id+ from #add_font_file / #add_font) at +size+ points in RGB
168
183
  # +color+ (each 0..1, default black). +rotation_deg+ rotates the text
169
184
  # counter-clockwise about its anchor (match the page rotation to follow a
170
- # rotated page). Coordinates are in the page's VISIBLE space (origin
171
- # lower-left, y up), regardless of the page's /Rotate. Returns whether the
172
- # page existed.
173
- def place_text(page_index, x, y, text, size = 12.0, color = [0.0, 0.0, 0.0], rotation_deg = 0.0)
185
+ # rotated page). +align+ (RustPdf::Align, default LEFT) shifts the start
186
+ # point along the baseline direction by the text width for RIGHT/CENTER
187
+ # alignment. +anchor+ (RustPdf::VerticalAnchor) says what +y+ means:
188
+ # BASELINE (default, historical behavior), TOP (text hangs from +y+
189
+ # baseline lands ascent x size below it, legacy fixed-position layout), BOTTOM
190
+ # (descender line rests on +y+), or LINE_TOP / LINE_BOTTOM (legacy layout engines line
191
+ # box). Coordinates are in the page's VISIBLE space (origin lower-left,
192
+ # y up) unless #stamp_space= chose MEDIA. Returns whether the page (and
193
+ # font) existed.
194
+ def place_text(page_index, x, y, text, size = 12.0, color = [0.0, 0.0, 0.0], rotation_deg = 0.0,
195
+ align: Align::LEFT, font_id: -1, anchor: VerticalAnchor::BASELINE)
174
196
  r, g, b = color
175
197
  found = RustPdf.out_int do |buf|
176
- Native.call("pdf_editable_place_text", ptr, page_index, x.to_f, y.to_f, text,
177
- size.to_f, r.to_f, g.to_f, b.to_f, rotation_deg.to_f, buf)
198
+ Native.call("pdf_editable_place_text_anchored", ptr, page_index, x.to_f, y.to_f, text,
199
+ size.to_f, r.to_f, g.to_f, b.to_f, rotation_deg.to_f, align, anchor, font_id, buf)
200
+ end
201
+ found != 0
202
+ end
203
+
204
+ # Choose the coordinate space of the positioned stamping primitives
205
+ # (#fill_rect, #place_text, #masked_text, #place_paragraph, #draw_image)
206
+ # for subsequent calls. StampSpace::VISIBLE (default) keeps the historical
207
+ # behavior — coordinates in the page's displayed space, compensating
208
+ # /Rotate. StampSpace::MEDIA interprets coordinates and +rotation_deg+ in
209
+ # the raw PDF user space (legacy layout semantics), never composing with the
210
+ # page's /Rotate — use it to reproduce legacy-engine placement on rotated/scanned
211
+ # pages. Watermarks and redaction are unaffected.
212
+ def stamp_space=(space)
213
+ RustPdf.check(Native.call("pdf_editable_set_stamp_space", ptr, space))
214
+ @stamp_space = space
215
+ end
216
+
217
+ # The current stamping coordinate space (see #stamp_space=).
218
+ def stamp_space
219
+ @stamp_space || StampSpace::VISIBLE
220
+ end
221
+
222
+ # Chainable form of #stamp_space=.
223
+ def set_stamp_space(space)
224
+ self.stamp_space = space
225
+ self
226
+ end
227
+
228
+ # Stamp a paragraph with automatic word wrapping on page +page_index+:
229
+ # +text+ is broken into lines that fit +width+ points (greedy, by word;
230
+ # "\n" forces a break) and drawn downward from (+x+, +y+). +anchor+
231
+ # (RustPdf::VerticalAnchor) says what +y+ means for the block: TOP
232
+ # (default) — top of the box, first baseline ascent x size below +y+
233
+ # (legacy fixed-position layout); BASELINE — the first line's baseline; BOTTOM /
234
+ # LINE_BOTTOM — bottom-pinned: the block's bottom rests on +y+ and grows
235
+ # upward by its real content height. +align+ lays lines out inside
236
+ # +[x, x+width]+ (JUSTIFY stretches the word gaps of every line but the
237
+ # last of each paragraph). +max_height+ (points, nil = unlimited) truncates
238
+ # overflowing lines (a ceiling for the bottom-pinned anchors — the last
239
+ # lines stay pinned). +line_height+ scales the default 1.2 x size leading.
240
+ # Pass +font_id+ from #add_font_file / #add_font to wrap and draw with an
241
+ # embedded font (its real metrics drive the break points); -1 uses the
242
+ # built-in Helvetica. +rotation_deg+ rotates the laid-out block
243
+ # counter-clockwise about the anchor. Returns whether the page (and font)
244
+ # existed and the box was valid.
245
+ def place_paragraph(page_index, x, y, width, text, size: 12.0, color: [0.0, 0.0, 0.0],
246
+ align: Align::LEFT, font_id: -1, max_height: nil, line_height: 1.0,
247
+ anchor: VerticalAnchor::TOP, rotation_deg: 0.0)
248
+ found, = paragraph_anchored(page_index, x, y, width, text, size, color, align,
249
+ font_id, max_height, line_height, anchor, rotation_deg)
250
+ found
251
+ end
252
+
253
+ # Like #place_paragraph but returns a Hash with the number of +:lines+
254
+ # drawn and the consumed block +:height+ in points (top of the first drawn
255
+ # line's box to the bottom of the last one's; 0 when nothing fit) — stack
256
+ # blocks without re-measuring.
257
+ def place_paragraph_measured(page_index, x, y, width, text, size: 12.0, color: [0.0, 0.0, 0.0],
258
+ align: Align::LEFT, font_id: -1, max_height: nil, line_height: 1.0,
259
+ anchor: VerticalAnchor::TOP, rotation_deg: 0.0)
260
+ _, lines, height = paragraph_anchored(page_index, x, y, width, text, size, color, align,
261
+ font_id, max_height, line_height, anchor, rotation_deg)
262
+ { lines: lines, height: height }
263
+ end
264
+
265
+ # Mask a placeholder: fill an opaque background box +[x, y, x+width,
266
+ # y+height]+ in +bg_color+ (each 0..1, default white), then write +text+
267
+ # over it using standard Helvetica (or an embedded font — pass +font_id+
268
+ # from #add_font_file / #add_font) at +size+ points in +text_color+ (each
269
+ # 0..1, default black), horizontally aligned per +align+ (RustPdf::Align,
270
+ # default LEFT). +valign+ (RustPdf::VerticalAlign) controls the vertical
271
+ # alignment of the line inside the box: MIDDLE (default, historical
272
+ # cap-height centering), TOP (line hangs from the top edge — legacy PDF libraries
273
+ # top line-alignment semantics) or BOTTOM (descender line rests on the
274
+ # bottom edge). +padding+ is the horizontal edge inset (points) for
275
+ # LEFT/RIGHT alignment: text starts at +x + padding+ (or ends at
276
+ # +x + width - padding+); nil keeps the historical
277
+ # min(0.15 x size, width / 4), 0 starts flush with the box edge like
278
+ # rectangle-based DrawString APIs. Coordinates are in the page's VISIBLE space
279
+ # (origin lower-left, y up). Returns whether the page (and font) existed.
280
+ def masked_text(page_index, x, y, width, height, text, size = 12.0,
281
+ text_color = [0.0, 0.0, 0.0], bg_color = [1.0, 1.0, 1.0],
282
+ align: Align::LEFT, font_id: -1, valign: VerticalAlign::MIDDLE, padding: nil)
283
+ tr, tg, tb = text_color
284
+ br, bg, bb = bg_color
285
+ found = RustPdf.out_int do |buf|
286
+ Native.call("pdf_editable_masked_text_pad", ptr, page_index, x.to_f, y.to_f,
287
+ width.to_f, height.to_f, text, size.to_f,
288
+ tr.to_f, tg.to_f, tb.to_f, br.to_f, bg.to_f, bb.to_f, align, valign,
289
+ (padding || -1.0).to_f, font_id, buf)
290
+ end
291
+ found != 0
292
+ end
293
+
294
+ # Stamp an +image+ (PNG or JPEG bytes; the core dispatches on the signature)
295
+ # onto page +page_index+ (0-based), with its lower-left corner at (+x+, +y+),
296
+ # scaled to +width+ x +height+ points and rotated +rotation_deg+ degrees
297
+ # counter-clockwise. +anchor+ (RustPdf::ImageAnchor) controls how a rotated
298
+ # image is anchored: CORNER (default) rotates the image about its own
299
+ # lower-left corner at (x, y); BOUNDING_BOX lands the rotated image's
300
+ # bounding box with its lower-left at (x, y) (bounding-box layout semantics —
301
+ # e.g. a 90-degree image occupies [x, x+height] x [y, y+width]).
302
+ # Coordinates are in the page's VISIBLE space (origin lower-left, y up),
303
+ # regardless of the page's /Rotate. Returns whether the page existed.
304
+ def draw_image(page_index, image, x, y, width, height, rotation_deg = 0.0,
305
+ anchor: ImageAnchor::CORNER)
306
+ found = RustPdf.out_int do |buf|
307
+ Native.call("pdf_editable_draw_image_anchored", ptr, page_index, image, image.bytesize,
308
+ x.to_f, y.to_f, width.to_f, height.to_f, rotation_deg.to_f, anchor, buf)
178
309
  end
179
310
  found != 0
180
311
  end
@@ -252,6 +383,24 @@ module RustPdf
252
383
 
253
384
  private
254
385
 
386
+ # Shared core of #place_paragraph / #place_paragraph_measured. Returns
387
+ # [found (bool), lines (Integer), height (Float)].
388
+ def paragraph_anchored(page_index, x, y, width, text, size, color, align,
389
+ font_id, max_height, line_height, anchor, rotation_deg)
390
+ r, g, b = color
391
+ height_buf = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DOUBLE, Fiddle::RUBY_FREE)
392
+ lines_buf = Fiddle::Pointer.malloc(Native::SIZEOF_INT, Fiddle::RUBY_FREE)
393
+ found_buf = Fiddle::Pointer.malloc(Native::SIZEOF_INT, Fiddle::RUBY_FREE)
394
+ RustPdf.check(Native.call("pdf_editable_place_paragraph_anchored", ptr, page_index,
395
+ x.to_f, y.to_f, width.to_f, text, size.to_f,
396
+ r.to_f, g.to_f, b.to_f, align, anchor, font_id,
397
+ (max_height || 0.0).to_f, line_height.to_f, rotation_deg.to_f,
398
+ height_buf, lines_buf, found_buf))
399
+ [found_buf[0, Native::SIZEOF_INT].unpack1("i!") != 0,
400
+ lines_buf[0, Native::SIZEOF_INT].unpack1("i!"),
401
+ height_buf[0, Fiddle::SIZEOF_DOUBLE].unpack1("d")]
402
+ end
403
+
255
404
  def ptr
256
405
  raise Error, "operation on a closed EditableDoc" if @ptr.nil? || @ptr.null?
257
406
 
@@ -78,6 +78,7 @@ module RustPdf
78
78
  "pdf_editable_save" => [[VP, VP], I],
79
79
 
80
80
  "pdf_extract_text" => [[VP, SZ, VP, VP], I],
81
+ "pdf_extract_page_text" => [[VP, SZ, SZ, VP, VP], I],
81
82
  "pdf_extract_images_to_dir" => [[VP, SZ, VP, VP], I],
82
83
  "pdf_render_page_to_png" => [[VP, SZ, SZ, D, VP, VP], I],
83
84
  "pdf_page_count" => [[VP, SZ, VP], I],
@@ -129,6 +130,22 @@ module RustPdf
129
130
  "pdf_inspect_json" => [[VP, SZ, VP, VP], I],
130
131
  "pdf_editable_fill_rect" => [[VP, I, D, D, D, D, D, D, D, D, VP], I],
131
132
  "pdf_editable_place_text" => [[VP, I, D, D, VP, D, D, D, D, D, VP], I],
133
+ "pdf_editable_place_text_aligned" => [[VP, I, D, D, VP, D, D, D, D, D, I, VP], I],
134
+ "pdf_editable_masked_text" => [[VP, I, D, D, D, D, VP, D, D, D, D, D, D, D, I, VP], I],
135
+ # Issue #50: stamp a PNG/JPEG image onto an existing page. The image bytes
136
+ # cross as the (uint8_t* data, uintptr_t len) pair, like pdf_editable_load.
137
+ "pdf_editable_draw_image" => [[VP, I, VP, SZ, D, D, D, D, D, VP], I],
138
+
139
+ # Stamping fonts + anchored stamping primitives. font_id -1 = built-in
140
+ # Helvetica; anchors/valign/space/image-anchor cross as small ints.
141
+ "pdf_editable_add_font_file" => [[VP, VP, VP], I],
142
+ "pdf_editable_add_font" => [[VP, VP, SZ, VP], I],
143
+ "pdf_editable_place_text_anchored" => [[VP, I, D, D, VP, D, D, D, D, D, I, I, I, VP], I],
144
+ "pdf_editable_masked_text_pad" => [[VP, I, D, D, D, D, VP, D, D, D, D, D, D, D, I, I, D, I, VP], I],
145
+ "pdf_editable_place_paragraph_anchored" =>
146
+ [[VP, I, D, D, D, VP, D, D, D, D, I, I, I, D, D, D, VP, VP, VP], I],
147
+ "pdf_editable_set_stamp_space" => [[VP, I], I],
148
+ "pdf_editable_draw_image_anchored" => [[VP, I, VP, SZ, D, D, D, D, D, I, VP], I],
132
149
  }.freeze
133
150
 
134
151
  def lib
data/lib/rustpdf.rb CHANGED
@@ -40,6 +40,48 @@ module RustPdf
40
40
  JUSTIFY = 3
41
41
  end
42
42
 
43
+ # What +y+ means for stamped text (EditableDoc#place_text /
44
+ # #place_paragraph). BASELINE is the historical place_text behavior; TOP
45
+ # hangs the text from +y+ (baseline lands ascent x size below it, legacy layout engines
46
+ # fixed-position layout semantics); BOTTOM rests the descender line on +y+.
47
+ # LINE_TOP / LINE_BOTTOM anchor via the layout line box (its height is also
48
+ # the leading basis, so a single line and a wrapped block agree vertically).
49
+ module VerticalAnchor
50
+ BASELINE = 0
51
+ TOP = 1
52
+ BOTTOM = 2
53
+ LINE_TOP = 3
54
+ LINE_BOTTOM = 4
55
+ end
56
+
57
+ # Vertical alignment of the line inside a masked_text box. MIDDLE is the
58
+ # historical cap-height centering; TOP hangs the line from the top edge
59
+ # (top line-alignment in rectangle-based text APIs semantics); BOTTOM rests the descender
60
+ # line on the bottom edge.
61
+ module VerticalAlign
62
+ TOP = 0
63
+ MIDDLE = 1
64
+ BOTTOM = 2
65
+ end
66
+
67
+ # Coordinate space of the positioned stamping primitives (EditableDoc).
68
+ # VISIBLE (default) compensates the page /Rotate so coordinates match what
69
+ # a viewer displays; MEDIA is the raw PDF user space (legacy layout engines
70
+ # fixed-position layout/rotation semantics — no /Rotate composition).
71
+ module StampSpace
72
+ VISIBLE = 0
73
+ MEDIA = 1
74
+ end
75
+
76
+ # How a rotated EditableDoc#draw_image is anchored. CORNER (default) rotates
77
+ # the image about its own lower-left corner at (x, y); BOUNDING_BOX lands
78
+ # the rotated image's bounding box with its lower-left at (x, y) (legacy layout engines
79
+ # layout semantics).
80
+ module ImageAnchor
81
+ CORNER = 0
82
+ BOUNDING_BOX = 1
83
+ end
84
+
43
85
  # Embedded-file relationship (PDF/A-3).
44
86
  module Relationship
45
87
  SOURCE = 0
@@ -228,6 +270,13 @@ module RustPdf
228
270
  .force_encoding(Encoding::UTF_8)
229
271
  end
230
272
 
273
+ # Extract the text of a single 0-based +page_index+ (Unicode via ToUnicode),
274
+ # without building an intermediate one-page document.
275
+ def extract_page_text(pdf, page_index)
276
+ take_bytes { |pp, pn| Native.call("pdf_extract_page_text", pdf, pdf.bytesize, page_index, pp, pn) }
277
+ .force_encoding(Encoding::UTF_8)
278
+ end
279
+
231
280
  # Extract every raster image into +dir+ (JPEG verbatim as .jpg, others as
232
281
  # .png; files named page{N}_{name}.{ext}). Returns the number written.
233
282
  def extract_images_to_dir(pdf, dir)
metadata CHANGED
@@ -1,17 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rustpdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.4.8
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-30 00:00:00.000000000 Z
11
+ date: 2026-07-02 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: Idiomatic Ruby binding over the rust-pdf C ABI (libpdf_ffi) using the
14
- built-in Fiddle stdlib.
13
+ description: 'Generate, edit, sign and process PDFs in Ruby: PDF/A, AES-256 encryption,
14
+ digital signatures (PAdES), forms, stamping, redaction, text extraction and page
15
+ rendering. Docs: https://rustpdf.dev/docs/ruby'
15
16
  email:
16
17
  executables: []
17
18
  extensions: []
@@ -45,6 +46,5 @@ requirements: []
45
46
  rubygems_version: 3.5.22
46
47
  signing_key:
47
48
  specification_version: 4
48
- summary: Ruby binding for the rust-pdf core (generate, manipulate, sign and validate
49
- PDFs).
49
+ summary: 'Generate, edit, sign and process PDFs in Ruby. Docs: https://rustpdf.dev/docs/ruby'
50
50
  test_files: []