rustpdf 0.1.0-arm64-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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ef3f9bc2679f676ea00cfb8d4fb687ecf50c0c233f017f5c6daab3d5935c20ef
4
+ data.tar.gz: 959fac07a3b61c2b32bc54b554b5a495ff18de869bbfa649bda54fdcbe3c7d6d
5
+ SHA512:
6
+ metadata.gz: 8d29f59f39265cd61b9664e952e73efa71be90257f6168e3cd3eeabb7a182de7e88be9682e48ab2a8c925d0eaf93ddfa663cf30393ddd03fb7190f2416c07d1a
7
+ data.tar.gz: 9f8e307466947685315bd384201b030768152dfb3b37818dede5c7cf2c71bef5793df52ea3baa4364d94b792c748d5219012d24517831547d7594a2940b0f717
data/README.md ADDED
@@ -0,0 +1,57 @@
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**, **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.
19
+
20
+ ## Loading the native library
21
+
22
+ `Native` finds `libpdf_ffi` via `RUSTPDF_LIB`, then by walking up from `lib/` to
23
+ `target/{debug,release}`. Build it from the repo root with
24
+ `cargo build -p pdf-ffi`. Requires Ruby ≥ 2.6 (Fiddle is bundled).
25
+
26
+ ## Quick start
27
+
28
+ ```ruby
29
+ require "rustpdf"
30
+
31
+ RustPdf.activate_license(token) # or set RUSTPDF_LICENSE (auto-activated)
32
+
33
+ doc = RustPdf::Document.new
34
+ doc.pdfa(RustPdf::Pdfa::A2A).info(title: "Report")
35
+ f = doc.add_font_file("assets/fonts/Roboto-Regular.ttf")
36
+ doc.add_page
37
+ .show_text(f, 20, 72, 760, "Title", heading_level: 1)
38
+ .paragraph(f, 12, 72, 720, 450, "A wrapping body…", align: RustPdf::Align::JUSTIFY)
39
+ data = doc.to_bytes
40
+
41
+ puts RustPdf.extract_text(data)
42
+
43
+ ed = RustPdf::EditableDoc.load(data)
44
+ ed.encrypt(method: RustPdf::Cipher::AES256, owner: "owner").save("secured.pdf")
45
+
46
+ signed = RustPdf.sign(data, key_der, cert_der, pades: true)
47
+ ```
48
+
49
+ Corporate features (PDF/A, signing, encryption, accessibility) require a license;
50
+ without one they raise `RustPdf::Error`. See [`docs/LICENSING.md`](../../docs/LICENSING.md).
51
+
52
+ ## Test
53
+
54
+ ```sh
55
+ cargo build -p pdf-ffi
56
+ ruby bindings/ruby/test/run.rb # or: make ruby-test
57
+ ```
@@ -0,0 +1,186 @@
1
+ module RustPdf
2
+ # A PDF document being authored. Call #close (or rely on GC) to free.
3
+ class Document
4
+ def initialize
5
+ @ptr = Native.call("pdf_document_new")
6
+ raise Error, "pdf_document_new returned NULL" if @ptr.null?
7
+ end
8
+
9
+ def close
10
+ return if @ptr.nil? || @ptr.null?
11
+
12
+ Native.call("pdf_document_free", @ptr)
13
+ @ptr = nil
14
+ end
15
+
16
+ # ---- configuration ------------------------------------------------------
17
+
18
+ def pdfa(level = nil)
19
+ RustPdf.check(
20
+ level ? Native.call("pdf_document_pdfa_level", ptr, level) : Native.call("pdf_document_pdfa", ptr)
21
+ )
22
+ self
23
+ end
24
+
25
+ def tagged
26
+ RustPdf.check(Native.call("pdf_document_tagged", ptr))
27
+ self
28
+ end
29
+
30
+ def version=(v)
31
+ RustPdf.check(Native.call("pdf_document_set_version", ptr, v))
32
+ end
33
+
34
+ def default_size(width, height)
35
+ RustPdf.check(Native.call("pdf_document_set_default_size", ptr, width, height))
36
+ self
37
+ end
38
+
39
+ def info(title: nil, author: nil, subject: nil, keywords: nil, creator: nil)
40
+ RustPdf.check(Native.call("pdf_document_set_info", ptr, title, author, subject, keywords, creator))
41
+ self
42
+ end
43
+
44
+ # ---- pages + graphics ---------------------------------------------------
45
+
46
+ def add_page(width: nil, height: nil)
47
+ RustPdf.check(
48
+ width && height ? Native.call("pdf_document_add_page_sized", ptr, width, height)
49
+ : Native.call("pdf_document_add_page", ptr)
50
+ )
51
+ self
52
+ end
53
+
54
+ def fill_rgb(r, g, b)
55
+ RustPdf.check(Native.call("pdf_page_set_fill_rgb", ptr, r, g, b))
56
+ self
57
+ end
58
+
59
+ def stroke_rgb(r, g, b)
60
+ RustPdf.check(Native.call("pdf_page_set_stroke_rgb", ptr, r, g, b))
61
+ self
62
+ end
63
+
64
+ def line_width(w)
65
+ RustPdf.check(Native.call("pdf_page_set_line_width", ptr, w))
66
+ self
67
+ end
68
+
69
+ def rect(x, y, w, h)
70
+ RustPdf.check(Native.call("pdf_page_rect", ptr, x, y, w, h))
71
+ self
72
+ end
73
+
74
+ def fill
75
+ RustPdf.check(Native.call("pdf_page_fill", ptr))
76
+ self
77
+ end
78
+
79
+ def stroke
80
+ RustPdf.check(Native.call("pdf_page_stroke", ptr))
81
+ self
82
+ end
83
+
84
+ # ---- fonts + text -------------------------------------------------------
85
+
86
+ def add_font_file(path)
87
+ RustPdf.out_int { |buf| Native.call("pdf_document_add_font_file", ptr, path, buf) }
88
+ end
89
+
90
+ def add_font(data)
91
+ RustPdf.out_int { |buf| Native.call("pdf_document_add_font", ptr, data, data.bytesize, buf) }
92
+ end
93
+
94
+ def show_text(font, size, x, y, text, heading_level: 0)
95
+ RustPdf.check(Native.call("pdf_page_show_text", ptr, font, size, x, y, text, heading_level))
96
+ self
97
+ end
98
+
99
+ def paragraph(font, size, x, y, width, text, align: Align::LEFT)
100
+ RustPdf.check(Native.call("pdf_page_paragraph", ptr, font, size, x, y, width, align, text))
101
+ self
102
+ end
103
+
104
+ # ---- images -------------------------------------------------------------
105
+
106
+ def add_image_file(path)
107
+ RustPdf.out_int { |buf| Native.call("pdf_document_add_image_file", ptr, path, buf) }
108
+ end
109
+
110
+ def add_image_png(data)
111
+ RustPdf.out_int { |buf| Native.call("pdf_document_add_image_png", ptr, data, data.bytesize, buf) }
112
+ end
113
+
114
+ def add_image_jpeg(data)
115
+ RustPdf.out_int { |buf| Native.call("pdf_document_add_image_jpeg", ptr, data, data.bytesize, buf) }
116
+ end
117
+
118
+ def draw_image(image, x, y, w, h)
119
+ RustPdf.check(Native.call("pdf_page_draw_image", ptr, image, x, y, w, h))
120
+ self
121
+ end
122
+
123
+ def figure(image, x, y, w, h, alt)
124
+ RustPdf.check(Native.call("pdf_page_figure", ptr, image, x, y, w, h, alt))
125
+ self
126
+ end
127
+
128
+ # ---- attachments + forms ------------------------------------------------
129
+
130
+ def attach_file(name, mime, data, relationship: Relationship::SOURCE, description: "")
131
+ RustPdf.check(Native.call("pdf_document_attach_file", ptr, name, mime, data, data.bytesize, relationship, description))
132
+ self
133
+ end
134
+
135
+ # rect = [x0, y0, x1, y1]
136
+ def text_field(name, page, rect, value: "", size: 0.0)
137
+ RustPdf.check(Native.call("pdf_document_text_field", ptr, name, page, rect[0], rect[1], rect[2], rect[3], value, size))
138
+ self
139
+ end
140
+
141
+ def checkbox(name, page, rect, checked)
142
+ RustPdf.check(Native.call("pdf_document_checkbox", ptr, name, page, rect[0], rect[1], rect[2], rect[3], checked ? 1 : 0))
143
+ self
144
+ end
145
+
146
+ # options: Array<String>; selected: index or nil
147
+ def dropdown(name, page, rect, options, selected: nil, size: 0.0)
148
+ RustPdf.check(Native.call("pdf_document_dropdown", ptr, name, page, rect[0], rect[1], rect[2], rect[3],
149
+ options.join("\n"), selected || -1, size))
150
+ self
151
+ end
152
+
153
+ # buttons: Array<[ [x0,y0,x1,y1], "export" ]>; selected: index or nil
154
+ def radio_group(name, page, buttons, selected: nil)
155
+ rects = buttons.flat_map { |(r, _)| r }.pack("d*")
156
+ cstrs = buttons.map { |(_, e)| Fiddle::Pointer[e.to_s] }
157
+ addrs = cstrs.map(&:to_i).pack("J*")
158
+ RustPdf.check(Native.call("pdf_document_radio_group", ptr, name, page, buttons.size, rects, addrs, selected || -1))
159
+ cstrs.clear # released after the call returned
160
+ self
161
+ end
162
+
163
+ # ---- output -------------------------------------------------------------
164
+
165
+ def page_count
166
+ Native.call("pdf_document_page_count", ptr)
167
+ end
168
+
169
+ def to_bytes
170
+ p = ptr
171
+ RustPdf.take_bytes { |pp, pn| Native.call("pdf_document_write", p, pp, pn) }
172
+ end
173
+
174
+ def save(path)
175
+ RustPdf.check(Native.call("pdf_document_save", ptr, path))
176
+ end
177
+
178
+ private
179
+
180
+ def ptr
181
+ raise Error, "operation on a closed Document" if @ptr.nil? || @ptr.null?
182
+
183
+ @ptr
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,136 @@
1
+ module RustPdf
2
+ # An existing PDF loaded for manipulation. Call #close (or rely on GC) to free.
3
+ class EditableDoc
4
+ # Load a PDF from bytes (optionally with a password).
5
+ def self.load(data, password: nil)
6
+ ptr = if password
7
+ Native.call("pdf_editable_load_password", data, data.bytesize, password)
8
+ else
9
+ Native.call("pdf_editable_load", data, data.bytesize)
10
+ end
11
+ raise Error, RustPdf.last_error if ptr.null?
12
+
13
+ new(ptr)
14
+ end
15
+
16
+ def self.load_file(path, password: nil)
17
+ load(File.binread(path), password: password)
18
+ end
19
+
20
+ def initialize(ptr)
21
+ @ptr = ptr
22
+ end
23
+ private_class_method :new
24
+
25
+ def close
26
+ return if @ptr.nil? || @ptr.null?
27
+
28
+ Native.call("pdf_editable_free", @ptr)
29
+ @ptr = nil
30
+ end
31
+
32
+ def page_count
33
+ Native.call("pdf_editable_page_count", ptr)
34
+ end
35
+
36
+ def merge(other)
37
+ RustPdf.check(Native.call("pdf_editable_merge", ptr, other.send(:ptr)))
38
+ self
39
+ end
40
+
41
+ def rotate_page(index, degrees)
42
+ RustPdf.check(Native.call("pdf_editable_rotate_page", ptr, index, degrees))
43
+ self
44
+ end
45
+
46
+ def delete_page(index)
47
+ RustPdf.check(Native.call("pdf_editable_delete_page", ptr, index))
48
+ self
49
+ end
50
+
51
+ def reorder_pages(order)
52
+ buf = order.pack("J*")
53
+ RustPdf.check(Native.call("pdf_editable_reorder_pages", ptr, buf, order.size))
54
+ self
55
+ end
56
+
57
+ def extract_pages(indices)
58
+ buf = indices.pack("J*")
59
+ out = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
60
+ RustPdf.check(Native.call("pdf_editable_extract_pages", ptr, buf, indices.size, out))
61
+ EditableDoc.send(:new, out.ptr)
62
+ end
63
+
64
+ def info=(pair)
65
+ key, value = pair
66
+ RustPdf.check(Native.call("pdf_editable_set_info", ptr, key, value))
67
+ end
68
+
69
+ def set_info(key, value)
70
+ RustPdf.check(Native.call("pdf_editable_set_info", ptr, key, value))
71
+ self
72
+ end
73
+
74
+ def get_info(key)
75
+ p = ptr
76
+ RustPdf.take_bytes { |pp, pn| Native.call("pdf_editable_get_info", p, key, pp, pn) }
77
+ .force_encoding(Encoding::UTF_8)
78
+ end
79
+
80
+ def set_xmp(xml)
81
+ RustPdf.check(Native.call("pdf_editable_set_xmp", ptr, xml, xml.bytesize))
82
+ self
83
+ end
84
+
85
+ def overlay_page(index, content)
86
+ RustPdf.check(Native.call("pdf_editable_overlay_page", ptr, index, content, content.bytesize))
87
+ self
88
+ end
89
+
90
+ # Returns whether the field existed.
91
+ def fill_text_field(name, value)
92
+ found = RustPdf.out_int { |buf| Native.call("pdf_editable_fill_text_field", ptr, name, value, buf) }
93
+ found != 0
94
+ end
95
+
96
+ def optimize
97
+ RustPdf.check(Native.call("pdf_editable_optimize", ptr))
98
+ self
99
+ end
100
+
101
+ def compact(on = true)
102
+ RustPdf.check(Native.call("pdf_editable_compact", ptr, on ? 1 : 0))
103
+ self
104
+ end
105
+
106
+ # Encrypt on save (requires a license).
107
+ def encrypt(method: Cipher::AES256, user: "", owner: "", read_only: false)
108
+ RustPdf.check(Native.call("pdf_editable_encrypt", ptr, method, user, owner, read_only ? 1 : 0))
109
+ self
110
+ end
111
+
112
+ def to_bytes
113
+ p = ptr
114
+ RustPdf.take_bytes { |pp, pn| Native.call("pdf_editable_to_bytes", p, pp, pn) }
115
+ end
116
+
117
+ def to_bytes_incremental(original)
118
+ p = ptr
119
+ RustPdf.take_bytes do |pp, pn|
120
+ Native.call("pdf_editable_to_bytes_incremental", p, original, original.bytesize, pp, pn)
121
+ end
122
+ end
123
+
124
+ def save(path)
125
+ RustPdf.check(Native.call("pdf_editable_save", ptr, path))
126
+ end
127
+
128
+ private
129
+
130
+ def ptr
131
+ raise Error, "operation on a closed EditableDoc" if @ptr.nil? || @ptr.null?
132
+
133
+ @ptr
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,143 @@
1
+ require "fiddle"
2
+
3
+ module RustPdf
4
+ # Internal: dlopen's libpdf_ffi and builds memoized Fiddle::Function objects
5
+ # from a signature table. Not part of the public API.
6
+ module Native
7
+ module_function
8
+
9
+ VP = Fiddle::TYPE_VOIDP
10
+ I = Fiddle::TYPE_INT
11
+ D = Fiddle::TYPE_DOUBLE
12
+ SZ = Fiddle::TYPE_SIZE_T
13
+ VOID = Fiddle::TYPE_VOID
14
+
15
+ # size_t / uintptr_t and int widths (for out-parameter buffers).
16
+ SIZEOF_SZ = Fiddle::SIZEOF_VOIDP
17
+ SIZEOF_INT = Fiddle::SIZEOF_INT
18
+
19
+ SIGS = {
20
+ "pdf_version" => [[], VP],
21
+ "pdf_last_error_message" => [[], VP],
22
+ "pdf_activate_license" => [[VP], I],
23
+ "pdf_buffer_free" => [[VP, SZ], VOID],
24
+
25
+ "pdf_document_new" => [[], VP],
26
+ "pdf_document_free" => [[VP], VOID],
27
+ "pdf_document_add_page" => [[VP], I],
28
+ "pdf_document_add_page_sized" => [[VP, D, D], I],
29
+ "pdf_document_page_count" => [[VP], I],
30
+ "pdf_page_set_fill_rgb" => [[VP, D, D, D], I],
31
+ "pdf_page_set_stroke_rgb" => [[VP, D, D, D], I],
32
+ "pdf_page_set_line_width" => [[VP, D], I],
33
+ "pdf_page_rect" => [[VP, D, D, D, D], I],
34
+ "pdf_page_fill" => [[VP], I],
35
+ "pdf_page_stroke" => [[VP], I],
36
+ "pdf_document_save" => [[VP, VP], I],
37
+ "pdf_document_write" => [[VP, VP, VP], I],
38
+ "pdf_document_pdfa" => [[VP], I],
39
+ "pdf_document_pdfa_level" => [[VP, I], I],
40
+ "pdf_document_tagged" => [[VP], I],
41
+ "pdf_document_set_version" => [[VP, I], I],
42
+ "pdf_document_set_default_size" => [[VP, D, D], I],
43
+ "pdf_document_set_info" => [[VP, VP, VP, VP, VP, VP], I],
44
+ "pdf_document_add_font_file" => [[VP, VP, VP], I],
45
+ "pdf_document_add_font" => [[VP, VP, SZ, VP], I],
46
+ "pdf_page_show_text" => [[VP, I, D, D, D, VP, I], I],
47
+ "pdf_page_paragraph" => [[VP, I, D, D, D, D, I, VP], I],
48
+ "pdf_document_add_image_file" => [[VP, VP, VP], I],
49
+ "pdf_document_add_image_png" => [[VP, VP, SZ, VP], I],
50
+ "pdf_document_add_image_jpeg" => [[VP, VP, SZ, VP], I],
51
+ "pdf_page_draw_image" => [[VP, I, D, D, D, D], I],
52
+ "pdf_page_figure" => [[VP, I, D, D, D, D, VP], I],
53
+ "pdf_document_attach_file" => [[VP, VP, VP, VP, SZ, I, VP], I],
54
+ "pdf_document_text_field" => [[VP, VP, SZ, D, D, D, D, VP, D], I],
55
+ "pdf_document_checkbox" => [[VP, VP, SZ, D, D, D, D, I], I],
56
+ "pdf_document_dropdown" => [[VP, VP, SZ, D, D, D, D, VP, I, D], I],
57
+ "pdf_document_radio_group" => [[VP, VP, SZ, SZ, VP, VP, I], I],
58
+
59
+ "pdf_editable_load" => [[VP, SZ], VP],
60
+ "pdf_editable_load_password" => [[VP, SZ, VP], VP],
61
+ "pdf_editable_free" => [[VP], VOID],
62
+ "pdf_editable_page_count" => [[VP], I],
63
+ "pdf_editable_merge" => [[VP, VP], I],
64
+ "pdf_editable_rotate_page" => [[VP, SZ, I], I],
65
+ "pdf_editable_delete_page" => [[VP, SZ], I],
66
+ "pdf_editable_reorder_pages" => [[VP, VP, SZ], I],
67
+ "pdf_editable_extract_pages" => [[VP, VP, SZ, VP], I],
68
+ "pdf_editable_set_info" => [[VP, VP, VP], I],
69
+ "pdf_editable_get_info" => [[VP, VP, VP, VP], I],
70
+ "pdf_editable_set_xmp" => [[VP, VP, SZ], I],
71
+ "pdf_editable_overlay_page" => [[VP, SZ, VP, SZ], I],
72
+ "pdf_editable_fill_text_field" => [[VP, VP, VP, VP], I],
73
+ "pdf_editable_optimize" => [[VP], I],
74
+ "pdf_editable_compact" => [[VP, I], I],
75
+ "pdf_editable_encrypt" => [[VP, I, VP, VP, I], I],
76
+ "pdf_editable_to_bytes" => [[VP, VP, VP], I],
77
+ "pdf_editable_to_bytes_incremental" => [[VP, VP, SZ, VP, VP], I],
78
+ "pdf_editable_save" => [[VP, VP], I],
79
+
80
+ "pdf_extract_text" => [[VP, SZ, VP, VP], I],
81
+ "pdf_sign" => [[VP, SZ, VP, SZ, VP, SZ, VP, VP, VP, I, VP, VP], I],
82
+ "pdf_timestamp" => [[VP, SZ, VP, SZ, VP, SZ, VP, VP, VP], I],
83
+ "pdf_add_dss" => [[VP, SZ, VP, VP, SZ, VP, VP, SZ, VP, VP], I],
84
+ }.freeze
85
+
86
+ def lib
87
+ @lib ||= Fiddle.dlopen(lib_path)
88
+ end
89
+
90
+ def [](name)
91
+ @fns ||= {}
92
+ @fns[name] ||= begin
93
+ args, ret = SIGS.fetch(name)
94
+ Fiddle::Function.new(lib[name], args, ret)
95
+ end
96
+ end
97
+
98
+ def call(name, *args)
99
+ self[name].call(*args)
100
+ end
101
+
102
+ def lib_path
103
+ env = ENV["RUSTPDF_LIB"]
104
+ return env if env && !env.empty? && File.file?(env)
105
+
106
+ file = lib_file_name
107
+
108
+ # Packaged gem: the platform-specific cdylib is vendored under
109
+ # vendor/<gem-platform>/ (staged by CI, see release-ruby.yml). Matches the
110
+ # per-platform wheel/npm-package layout used by the Python/Node bindings.
111
+ # A platform gem ships exactly one vendor/<plat>/ dir, so the first match
112
+ # for this OS's lib file name is the right one — no platform-string parsing.
113
+ vendored = Dir[File.join(gem_root, "vendor", "*", file)].find { |p| File.file?(p) }
114
+ return vendored if vendored
115
+
116
+ # Monorepo dev: walk up from lib/ to the Cargo build tree.
117
+ dir = __dir__
118
+ 10.times do
119
+ %w[debug release].each do |profile|
120
+ candidate = File.join(dir, "target", profile, file)
121
+ return candidate if File.file?(candidate)
122
+ end
123
+ parent = File.dirname(dir)
124
+ break if parent == dir
125
+ dir = parent
126
+ end
127
+ raise Error, "could not locate #{file}; build it with `cargo build -p pdf-ffi` or set RUSTPDF_LIB"
128
+ end
129
+
130
+ # Repo/gem root = two levels up from lib/rustpdf/.
131
+ def gem_root
132
+ File.expand_path("../..", __dir__)
133
+ end
134
+
135
+ def lib_file_name
136
+ case RbConfig::CONFIG["host_os"]
137
+ when /mswin|mingw|cygwin/ then "pdf_ffi.dll"
138
+ when /darwin/ then "libpdf_ffi.dylib"
139
+ else "libpdf_ffi.so"
140
+ end
141
+ end
142
+ end
143
+ end
data/lib/rustpdf.rb ADDED
@@ -0,0 +1,142 @@
1
+ require "fiddle"
2
+ require_relative "rustpdf/native"
3
+
4
+ # Idiomatic Ruby binding for the rust-pdf core over its C ABI (libpdf_ffi),
5
+ # using the built-in Fiddle stdlib. Covers the whole product surface: vector
6
+ # graphics, fonts/text, paragraphs, images, PDF/A (1b-3a), tagged/accessible
7
+ # output, attachments, AcroForm fields, manipulation, text extraction,
8
+ # encryption and digital signatures, plus feature licensing.
9
+ module RustPdf
10
+ # Raised when a native call returns a non-zero PdfStatus.
11
+ class Error < StandardError
12
+ attr_reader :status
13
+
14
+ def initialize(message, status = 0)
15
+ @status = status
16
+ super(status.zero? ? message : "PdfStatus=#{status}: #{message}")
17
+ end
18
+ end
19
+
20
+ # PDF/A conformance levels.
21
+ module Pdfa
22
+ A1B = 0
23
+ A2B = 1
24
+ A2A = 2
25
+ A3B = 3
26
+ A3A = 4
27
+ end
28
+
29
+ # Paragraph alignment.
30
+ module Align
31
+ LEFT = 0
32
+ RIGHT = 1
33
+ CENTER = 2
34
+ JUSTIFY = 3
35
+ end
36
+
37
+ # Embedded-file relationship (PDF/A-3).
38
+ module Relationship
39
+ SOURCE = 0
40
+ DATA = 1
41
+ ALTERNATIVE = 2
42
+ SUPPLEMENT = 3
43
+ UNSPECIFIED = 4
44
+ end
45
+
46
+ # Encryption ciphers.
47
+ module Cipher
48
+ RC4 = 0
49
+ AES128 = 1
50
+ AES256 = 2
51
+ end
52
+
53
+ module_function
54
+
55
+ # Native library version string.
56
+ def version
57
+ Native.call("pdf_version").to_s
58
+ end
59
+
60
+ # Activate a license token (unlocks PDF/A, signing, encryption,
61
+ # accessibility). Tokens may also be supplied via the RUSTPDF_LICENSE /
62
+ # RUSTPDF_LICENSE_FILE environment variables (auto-activated).
63
+ def activate_license(token)
64
+ check(Native.call("pdf_activate_license", token))
65
+ end
66
+
67
+ # Extract a document's text (Unicode via ToUnicode).
68
+ def extract_text(pdf)
69
+ take_bytes { |pp, pn| Native.call("pdf_extract_text", pdf, pdf.bytesize, pp, pn) }
70
+ .force_encoding(Encoding::UTF_8)
71
+ end
72
+
73
+ # Sign a PDF (PKCS#7 detached, incremental update). Requires a license.
74
+ def sign(pdf, key_der, cert_der, reason: nil, location: nil, name: nil, pades: false)
75
+ take_bytes do |pp, pn|
76
+ Native.call("pdf_sign", pdf, pdf.bytesize, key_der, key_der.bytesize,
77
+ cert_der, cert_der.bytesize, reason, location, name, pades ? 1 : 0, pp, pn)
78
+ end
79
+ end
80
+
81
+ # Append a document timestamp (/DocTimeStamp, PAdES-B-LTA).
82
+ def timestamp(pdf, tsa_key_der, tsa_cert_der, date: nil)
83
+ take_bytes do |pp, pn|
84
+ Native.call("pdf_timestamp", pdf, pdf.bytesize, tsa_key_der, tsa_key_der.bytesize,
85
+ tsa_cert_der, tsa_cert_der.bytesize, date, pp, pn)
86
+ end
87
+ end
88
+
89
+ # Append a Document Security Store (/DSS, PAdES-B-LT).
90
+ def add_dss(pdf, certs: [], crls: [])
91
+ cptrs = certs.map { |c| Fiddle::Pointer[c] }
92
+ rptrs = crls.map { |c| Fiddle::Pointer[c] }
93
+ cptr_buf = cptrs.map(&:to_i).pack("J*")
94
+ clen_buf = certs.map(&:bytesize).pack("J*")
95
+ rptr_buf = rptrs.map(&:to_i).pack("J*")
96
+ rlen_buf = crls.map(&:bytesize).pack("J*")
97
+ result = take_bytes do |pp, pn|
98
+ Native.call("pdf_add_dss", pdf, pdf.bytesize, cptr_buf, clen_buf, certs.size,
99
+ rptr_buf, rlen_buf, crls.size, pp, pn)
100
+ end
101
+ # keep the per-item pointers alive until the call has returned
102
+ cptrs.clear
103
+ rptrs.clear
104
+ result
105
+ end
106
+
107
+ # ---- internal helpers (used by Document/EditableDoc) ----------------------
108
+
109
+ def last_error
110
+ p = Native.call("pdf_last_error_message")
111
+ p.null? ? "unknown error" : p.to_s
112
+ end
113
+
114
+ def check(status)
115
+ raise Error.new(last_error, status) unless status.zero?
116
+ end
117
+
118
+ # Run an out-buffer producer { |out_ptr, out_len| status } and return the
119
+ # produced bytes, always freeing the native buffer.
120
+ def take_bytes
121
+ pp = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
122
+ pn = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
123
+ check(yield(pp, pn))
124
+ len = pn[0, Native::SIZEOF_SZ].unpack1("J")
125
+ return "".b if len.zero?
126
+
127
+ dptr = pp.ptr
128
+ bytes = dptr[0, len]
129
+ Native.call("pdf_buffer_free", dptr, len)
130
+ bytes
131
+ end
132
+
133
+ # Run a producer { |out_int| status } and return the written int.
134
+ def out_int
135
+ buf = Fiddle::Pointer.malloc(Native::SIZEOF_INT, Fiddle::RUBY_FREE)
136
+ check(yield(buf))
137
+ buf[0, Native::SIZEOF_INT].unpack1("i!")
138
+ end
139
+ end
140
+
141
+ require_relative "rustpdf/document"
142
+ require_relative "rustpdf/editable_doc"
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rustpdf
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: arm64-darwin
6
+ authors:
7
+ - rust-pdf
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-27 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Idiomatic Ruby binding over the rust-pdf C ABI (libpdf_ffi) using the
14
+ built-in Fiddle stdlib.
15
+ email:
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - lib/rustpdf.rb
22
+ - lib/rustpdf/document.rb
23
+ - lib/rustpdf/editable_doc.rb
24
+ - lib/rustpdf/native.rb
25
+ - vendor/arm64-darwin/libpdf_ffi.dylib
26
+ homepage:
27
+ licenses:
28
+ - Nonstandard
29
+ metadata: {}
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '2.6'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubygems_version: 3.5.22
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: Ruby binding for the rust-pdf core (generate, manipulate, sign and validate
49
+ PDFs).
50
+ test_files: []