rustpdf 0.4.2-x86_64-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 +7 -0
- data/README.md +57 -0
- data/lib/rustpdf/document.rb +224 -0
- data/lib/rustpdf/editable_doc.rb +199 -0
- data/lib/rustpdf/native.rb +166 -0
- data/lib/rustpdf.rb +215 -0
- data/vendor/x86_64-darwin/libpdf_ffi.dylib +0 -0
- metadata +50 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0ea684eb693a8ccd4cf2a1270ada8e16d5f61030b06a190a02d93073b34835fb
|
|
4
|
+
data.tar.gz: fe5ea298dc20db77fa6fd16be0624d8bbaab2cbea0ccf824f8f9b9192a0eef85
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3c7e1f74341ffe42e35a87cdd3e866c4bbd94c976544299e0c8bef05aeb9c1ab49698a9939d994a91961bd28bf5b6d5af798fac8ff42ceedbc25aeeeed6b06bf
|
|
7
|
+
data.tar.gz: 2bbe7ac930f28fbc4d8c4fa5a849d0a1df61766956d142e13ac495ce10eaf9e4eeab66d0009d3b0002dafd704e78608c5be578a8d57573ac7a9f2873c9f3b303
|
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**, **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.
|
|
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, page rendering — a **Pro** feature) 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,224 @@
|
|
|
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
|
+
# ---- hyperlinks + bookmarks (Tier 1) ------------------------------------
|
|
164
|
+
|
|
165
|
+
# rect = [x0, y0, x1, y1]; link to an external URI.
|
|
166
|
+
def link_uri(rect, uri)
|
|
167
|
+
RustPdf.check(Native.call("pdf_page_link_uri", ptr, rect[0], rect[1], rect[2], rect[3], uri))
|
|
168
|
+
self
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# rect = [x0, y0, x1, y1]; link to another page (optional +top+ y-offset).
|
|
172
|
+
def link_to_page(rect, page_index, top: nil)
|
|
173
|
+
RustPdf.check(Native.call("pdf_page_link_to_page", ptr, rect[0], rect[1], rect[2], rect[3],
|
|
174
|
+
page_index, top || 0.0, top.nil? ? 0 : 1))
|
|
175
|
+
self
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Append one outline tree (a RustPdf::Bookmark). Pre-order flattened into
|
|
179
|
+
# parallel arrays and emitted in a single native call.
|
|
180
|
+
def add_bookmark(bookmark)
|
|
181
|
+
entries = bookmark.flatten_into(0, [])
|
|
182
|
+
n = entries.size
|
|
183
|
+
levels = entries.map { |e| e[0] }.pack("i!*")
|
|
184
|
+
pages = entries.map { |e| e[2] }.pack("J*")
|
|
185
|
+
tops = entries.map { |e| e[3] || 0.0 }.pack("d*")
|
|
186
|
+
has = entries.map { |e| e[3].nil? ? 0 : 1 }.pack("i!*")
|
|
187
|
+
cstrs = entries.map { |e| Fiddle::Pointer[e[1].to_s] }
|
|
188
|
+
titles = cstrs.map(&:to_i).pack("J*")
|
|
189
|
+
RustPdf.check(Native.call("pdf_document_add_bookmarks", ptr, n, levels, titles, pages, tops, has))
|
|
190
|
+
cstrs.clear # released after the call returned
|
|
191
|
+
self
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# ---- ZUGFeRD / Factur-X (Tier 2) ----------------------------------------
|
|
195
|
+
|
|
196
|
+
def facturx(xml, profile: FacturxProfile::EN16931)
|
|
197
|
+
RustPdf.check(Native.call("pdf_document_facturx", ptr, xml, xml.bytesize, profile))
|
|
198
|
+
self
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# ---- output -------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
def page_count
|
|
204
|
+
Native.call("pdf_document_page_count", ptr)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def to_bytes
|
|
208
|
+
p = ptr
|
|
209
|
+
RustPdf.take_bytes { |pp, pn| Native.call("pdf_document_write", p, pp, pn) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def save(path)
|
|
213
|
+
RustPdf.check(Native.call("pdf_document_save", ptr, path))
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
def ptr
|
|
219
|
+
raise Error, "operation on a closed Document" if @ptr.nil? || @ptr.null?
|
|
220
|
+
|
|
221
|
+
@ptr
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
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
|
+
# ---- form fill + flatten + watermark (Tier 1) ---------------------------
|
|
97
|
+
|
|
98
|
+
# Set a checkbox by field name. Returns whether the field existed.
|
|
99
|
+
def set_checkbox(name, checked = true)
|
|
100
|
+
found = RustPdf.out_int { |buf| Native.call("pdf_editable_set_checkbox", ptr, name, checked ? 1 : 0, buf) }
|
|
101
|
+
found != 0
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Select a radio button by field name + export value. Returns whether found.
|
|
105
|
+
def set_radio(name, export_value)
|
|
106
|
+
found = RustPdf.out_int { |buf| Native.call("pdf_editable_set_radio", ptr, name, export_value, buf) }
|
|
107
|
+
found != 0
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Set a choice (dropdown/list) value by field name. Returns whether found.
|
|
111
|
+
def set_choice(name, value)
|
|
112
|
+
found = RustPdf.out_int { |buf| Native.call("pdf_editable_set_choice", ptr, name, value, buf) }
|
|
113
|
+
found != 0
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Flatten all AcroForm fields into page content (removes interactivity).
|
|
117
|
+
def flatten_forms
|
|
118
|
+
RustPdf.check(Native.call("pdf_editable_flatten_forms", ptr))
|
|
119
|
+
self
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Returns the list of AcroForm field names.
|
|
123
|
+
def field_names
|
|
124
|
+
p = ptr
|
|
125
|
+
text = RustPdf.take_bytes { |pp, pn| Native.call("pdf_editable_field_names", p, pp, pn) }
|
|
126
|
+
.force_encoding(Encoding::UTF_8)
|
|
127
|
+
text.split("\n").reject(&:empty?)
|
|
128
|
+
end
|
|
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)
|
|
132
|
+
r, g, b = color
|
|
133
|
+
RustPdf.check(Native.call("pdf_editable_watermark_text", ptr, text, size, r, g, b, opacity, rotation_deg))
|
|
134
|
+
self
|
|
135
|
+
end
|
|
136
|
+
|
|
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))
|
|
140
|
+
self
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# ---- redaction + PDF/A conversion (Tier 2) ------------------------------
|
|
144
|
+
|
|
145
|
+
# Black out rectangles on a page. rects = [[x0,y0,x1,y1], ...].
|
|
146
|
+
# Returns whether the page existed.
|
|
147
|
+
def redact(page_index, rects)
|
|
148
|
+
flat = rects.flatten.pack("d*")
|
|
149
|
+
found = RustPdf.out_int { |buf| Native.call("pdf_editable_redact", ptr, page_index, flat, rects.size, buf) }
|
|
150
|
+
found != 0
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Convert the document to PDF/A (B-levels only: A1B=0, A2B=1, A3B=3).
|
|
154
|
+
def convert_to_pdfa(level = Pdfa::A2B)
|
|
155
|
+
RustPdf.check(Native.call("pdf_editable_convert_to_pdfa", ptr, level))
|
|
156
|
+
self
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def optimize
|
|
160
|
+
RustPdf.check(Native.call("pdf_editable_optimize", ptr))
|
|
161
|
+
self
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def compact(on = true)
|
|
165
|
+
RustPdf.check(Native.call("pdf_editable_compact", ptr, on ? 1 : 0))
|
|
166
|
+
self
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Encrypt on save (requires a license).
|
|
170
|
+
def encrypt(method: Cipher::AES256, user: "", owner: "", read_only: false)
|
|
171
|
+
RustPdf.check(Native.call("pdf_editable_encrypt", ptr, method, user, owner, read_only ? 1 : 0))
|
|
172
|
+
self
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def to_bytes
|
|
176
|
+
p = ptr
|
|
177
|
+
RustPdf.take_bytes { |pp, pn| Native.call("pdf_editable_to_bytes", p, pp, pn) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def to_bytes_incremental(original)
|
|
181
|
+
p = ptr
|
|
182
|
+
RustPdf.take_bytes do |pp, pn|
|
|
183
|
+
Native.call("pdf_editable_to_bytes_incremental", p, original, original.bytesize, pp, pn)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def save(path)
|
|
188
|
+
RustPdf.check(Native.call("pdf_editable_save", ptr, path))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
def ptr
|
|
194
|
+
raise Error, "operation on a closed EditableDoc" if @ptr.nil? || @ptr.null?
|
|
195
|
+
|
|
196
|
+
@ptr
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
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_extract_images_to_dir" => [[VP, SZ, VP, VP], I],
|
|
82
|
+
"pdf_render_page_to_png" => [[VP, SZ, SZ, D, VP, VP], I],
|
|
83
|
+
"pdf_page_count" => [[VP, SZ, VP], I],
|
|
84
|
+
"pdf_sign" => [[VP, SZ, VP, SZ, VP, SZ, VP, VP, VP, I, VP, VP], I],
|
|
85
|
+
"pdf_timestamp" => [[VP, SZ, VP, SZ, VP, SZ, VP, VP, VP], I],
|
|
86
|
+
"pdf_add_dss" => [[VP, SZ, VP, VP, SZ, VP, VP, SZ, VP, VP], I],
|
|
87
|
+
|
|
88
|
+
# Tier 1: hyperlinks + bookmarks (Document)
|
|
89
|
+
"pdf_page_link_uri" => [[VP, D, D, D, D, VP], I],
|
|
90
|
+
"pdf_page_link_to_page" => [[VP, D, D, D, D, SZ, D, I], I],
|
|
91
|
+
"pdf_document_add_bookmarks" => [[VP, SZ, VP, VP, VP, VP, VP], I],
|
|
92
|
+
# Tier 2: ZUGFeRD / Factur-X (Document)
|
|
93
|
+
"pdf_document_facturx" => [[VP, VP, SZ, I], I],
|
|
94
|
+
# Tier 1: form fill + flatten + watermark (EditableDoc)
|
|
95
|
+
"pdf_editable_set_checkbox" => [[VP, VP, I, VP], I],
|
|
96
|
+
"pdf_editable_set_radio" => [[VP, VP, VP, VP], I],
|
|
97
|
+
"pdf_editable_set_choice" => [[VP, VP, VP, VP], I],
|
|
98
|
+
"pdf_editable_flatten_forms" => [[VP], I],
|
|
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],
|
|
102
|
+
# Tier 2: redaction + PDF/A conversion (EditableDoc)
|
|
103
|
+
"pdf_editable_redact" => [[VP, SZ, VP, SZ, VP], I],
|
|
104
|
+
"pdf_editable_convert_to_pdfa" => [[VP, I], I],
|
|
105
|
+
# Tier 2: signature validation (module-level)
|
|
106
|
+
"pdf_verify_signatures_json" => [[VP, SZ, VP, VP], I],
|
|
107
|
+
}.freeze
|
|
108
|
+
|
|
109
|
+
def lib
|
|
110
|
+
@lib ||= Fiddle.dlopen(lib_path)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def [](name)
|
|
114
|
+
@fns ||= {}
|
|
115
|
+
@fns[name] ||= begin
|
|
116
|
+
args, ret = SIGS.fetch(name)
|
|
117
|
+
Fiddle::Function.new(lib[name], args, ret)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def call(name, *args)
|
|
122
|
+
self[name].call(*args)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def lib_path
|
|
126
|
+
env = ENV["RUSTPDF_LIB"]
|
|
127
|
+
return env if env && !env.empty? && File.file?(env)
|
|
128
|
+
|
|
129
|
+
file = lib_file_name
|
|
130
|
+
|
|
131
|
+
# Packaged gem: the platform-specific cdylib is vendored under
|
|
132
|
+
# vendor/<gem-platform>/ (staged by CI, see release-ruby.yml). Matches the
|
|
133
|
+
# per-platform wheel/npm-package layout used by the Python/Node bindings.
|
|
134
|
+
# A platform gem ships exactly one vendor/<plat>/ dir, so the first match
|
|
135
|
+
# for this OS's lib file name is the right one — no platform-string parsing.
|
|
136
|
+
vendored = Dir[File.join(gem_root, "vendor", "*", file)].find { |p| File.file?(p) }
|
|
137
|
+
return vendored if vendored
|
|
138
|
+
|
|
139
|
+
# Monorepo dev: walk up from lib/ to the Cargo build tree.
|
|
140
|
+
dir = __dir__
|
|
141
|
+
10.times do
|
|
142
|
+
%w[debug release].each do |profile|
|
|
143
|
+
candidate = File.join(dir, "target", profile, file)
|
|
144
|
+
return candidate if File.file?(candidate)
|
|
145
|
+
end
|
|
146
|
+
parent = File.dirname(dir)
|
|
147
|
+
break if parent == dir
|
|
148
|
+
dir = parent
|
|
149
|
+
end
|
|
150
|
+
raise Error, "could not locate #{file}; build it with `cargo build -p pdf-ffi` or set RUSTPDF_LIB"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Repo/gem root = two levels up from lib/rustpdf/.
|
|
154
|
+
def gem_root
|
|
155
|
+
File.expand_path("../..", __dir__)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def lib_file_name
|
|
159
|
+
case RbConfig::CONFIG["host_os"]
|
|
160
|
+
when /mswin|mingw|cygwin/ then "pdf_ffi.dll"
|
|
161
|
+
when /darwin/ then "libpdf_ffi.dylib"
|
|
162
|
+
else "libpdf_ffi.so"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
data/lib/rustpdf.rb
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
require "fiddle"
|
|
2
|
+
require "json"
|
|
3
|
+
require_relative "rustpdf/native"
|
|
4
|
+
|
|
5
|
+
# Idiomatic Ruby binding for the rust-pdf core over its C ABI (libpdf_ffi),
|
|
6
|
+
# using the built-in Fiddle stdlib. Covers the whole product surface: vector
|
|
7
|
+
# graphics, fonts/text, paragraphs, images, PDF/A (1b-3a), tagged/accessible
|
|
8
|
+
# output, attachments, AcroForm fields, manipulation, text extraction,
|
|
9
|
+
# encryption and digital signatures, plus feature licensing.
|
|
10
|
+
module RustPdf
|
|
11
|
+
# Raised when a native call returns a non-zero PdfStatus.
|
|
12
|
+
class Error < StandardError
|
|
13
|
+
attr_reader :status
|
|
14
|
+
|
|
15
|
+
def initialize(message, status = 0)
|
|
16
|
+
@status = status
|
|
17
|
+
super(status.zero? ? message : "PdfStatus=#{status}: #{message}")
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# PDF/A conformance levels.
|
|
22
|
+
module Pdfa
|
|
23
|
+
A1B = 0
|
|
24
|
+
A2B = 1
|
|
25
|
+
A2A = 2
|
|
26
|
+
A3B = 3
|
|
27
|
+
A3A = 4
|
|
28
|
+
# PDF/A-4 (ISO 19005-4), based on PDF 2.0.
|
|
29
|
+
A4 = 5
|
|
30
|
+
A4E = 6
|
|
31
|
+
A4F = 7
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Paragraph alignment.
|
|
35
|
+
module Align
|
|
36
|
+
LEFT = 0
|
|
37
|
+
RIGHT = 1
|
|
38
|
+
CENTER = 2
|
|
39
|
+
JUSTIFY = 3
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Embedded-file relationship (PDF/A-3).
|
|
43
|
+
module Relationship
|
|
44
|
+
SOURCE = 0
|
|
45
|
+
DATA = 1
|
|
46
|
+
ALTERNATIVE = 2
|
|
47
|
+
SUPPLEMENT = 3
|
|
48
|
+
UNSPECIFIED = 4
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Encryption ciphers.
|
|
52
|
+
module Cipher
|
|
53
|
+
RC4 = 0
|
|
54
|
+
AES128 = 1
|
|
55
|
+
AES256 = 2
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# ZUGFeRD / Factur-X conformance profiles.
|
|
59
|
+
module FacturxProfile
|
|
60
|
+
MINIMUM = 0
|
|
61
|
+
BASIC_WL = 1
|
|
62
|
+
BASIC = 2
|
|
63
|
+
EN16931 = 3
|
|
64
|
+
EXTENDED = 4
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# A document outline (bookmark) entry. Nest with #child to build a tree.
|
|
68
|
+
# Each #add_bookmark call on a Document appends one root tree (pre-order
|
|
69
|
+
# flattened into parallel arrays).
|
|
70
|
+
class Bookmark
|
|
71
|
+
attr_accessor :title, :page, :top, :children
|
|
72
|
+
|
|
73
|
+
def initialize(title, page, top: nil, children: nil)
|
|
74
|
+
@title = title
|
|
75
|
+
@page = page
|
|
76
|
+
@top = top
|
|
77
|
+
@children = children || []
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Append a child bookmark; returns self for chaining.
|
|
81
|
+
def child(bookmark)
|
|
82
|
+
@children << bookmark
|
|
83
|
+
self
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Pre-order flatten into +out+ as [level, title, page, top] tuples.
|
|
87
|
+
def flatten_into(level, out)
|
|
88
|
+
out << [level, title, page, top]
|
|
89
|
+
children.each { |c| c.flatten_into(level + 1, out) }
|
|
90
|
+
out
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
module_function
|
|
95
|
+
|
|
96
|
+
# Native library version string.
|
|
97
|
+
def version
|
|
98
|
+
Native.call("pdf_version").to_s
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Activate a license token (unlocks PDF/A, signing, encryption,
|
|
102
|
+
# accessibility). Tokens may also be supplied via the RUSTPDF_LICENSE /
|
|
103
|
+
# RUSTPDF_LICENSE_FILE environment variables (auto-activated).
|
|
104
|
+
def activate_license(token)
|
|
105
|
+
check(Native.call("pdf_activate_license", token))
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Extract a document's text (Unicode via ToUnicode).
|
|
109
|
+
def extract_text(pdf)
|
|
110
|
+
take_bytes { |pp, pn| Native.call("pdf_extract_text", pdf, pdf.bytesize, pp, pn) }
|
|
111
|
+
.force_encoding(Encoding::UTF_8)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Extract every raster image into +dir+ (JPEG verbatim as .jpg, others as
|
|
115
|
+
# .png; files named page{N}_{name}.{ext}). Returns the number written.
|
|
116
|
+
def extract_images_to_dir(pdf, dir)
|
|
117
|
+
count = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
118
|
+
check(Native.call("pdf_extract_images_to_dir", pdf, pdf.bytesize, dir, count))
|
|
119
|
+
count[0, Native::SIZEOF_SZ].unpack1("J")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Render page +page+ (0-based) of +pdf+ to a PNG image at +dpi+
|
|
123
|
+
# dots-per-inch. Page rendering is a licensed Pro feature: raises unless a
|
|
124
|
+
# license granting it is active.
|
|
125
|
+
def render_page_to_png(pdf, page = 0, dpi = 150.0)
|
|
126
|
+
take_bytes { |pp, pn| Native.call("pdf_render_page_to_png", pdf, pdf.bytesize, page, dpi.to_f, pp, pn) }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Number of pages in +pdf+ (free — no license required).
|
|
130
|
+
def page_count(pdf)
|
|
131
|
+
count = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
132
|
+
check(Native.call("pdf_page_count", pdf, pdf.bytesize, count))
|
|
133
|
+
count[0, Native::SIZEOF_SZ].unpack1("J")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Validate every signature in +pdf+. Returns one Hash per signature with keys
|
|
137
|
+
# "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.
|
|
140
|
+
def verify_signatures(pdf)
|
|
141
|
+
js = take_bytes { |pp, pn| Native.call("pdf_verify_signatures_json", pdf, pdf.bytesize, pp, pn) }
|
|
142
|
+
.force_encoding(Encoding::UTF_8)
|
|
143
|
+
js.empty? ? [] : JSON.parse(js)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Sign a PDF (PKCS#7 detached, incremental update). Requires a license.
|
|
147
|
+
def sign(pdf, key_der, cert_der, reason: nil, location: nil, name: nil, pades: false)
|
|
148
|
+
take_bytes do |pp, pn|
|
|
149
|
+
Native.call("pdf_sign", pdf, pdf.bytesize, key_der, key_der.bytesize,
|
|
150
|
+
cert_der, cert_der.bytesize, reason, location, name, pades ? 1 : 0, pp, pn)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Append a document timestamp (/DocTimeStamp, PAdES-B-LTA).
|
|
155
|
+
def timestamp(pdf, tsa_key_der, tsa_cert_der, date: nil)
|
|
156
|
+
take_bytes do |pp, pn|
|
|
157
|
+
Native.call("pdf_timestamp", pdf, pdf.bytesize, tsa_key_der, tsa_key_der.bytesize,
|
|
158
|
+
tsa_cert_der, tsa_cert_der.bytesize, date, pp, pn)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Append a Document Security Store (/DSS, PAdES-B-LT).
|
|
163
|
+
def add_dss(pdf, certs: [], crls: [])
|
|
164
|
+
cptrs = certs.map { |c| Fiddle::Pointer[c] }
|
|
165
|
+
rptrs = crls.map { |c| Fiddle::Pointer[c] }
|
|
166
|
+
cptr_buf = cptrs.map(&:to_i).pack("J*")
|
|
167
|
+
clen_buf = certs.map(&:bytesize).pack("J*")
|
|
168
|
+
rptr_buf = rptrs.map(&:to_i).pack("J*")
|
|
169
|
+
rlen_buf = crls.map(&:bytesize).pack("J*")
|
|
170
|
+
result = take_bytes do |pp, pn|
|
|
171
|
+
Native.call("pdf_add_dss", pdf, pdf.bytesize, cptr_buf, clen_buf, certs.size,
|
|
172
|
+
rptr_buf, rlen_buf, crls.size, pp, pn)
|
|
173
|
+
end
|
|
174
|
+
# keep the per-item pointers alive until the call has returned
|
|
175
|
+
cptrs.clear
|
|
176
|
+
rptrs.clear
|
|
177
|
+
result
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# ---- internal helpers (used by Document/EditableDoc) ----------------------
|
|
181
|
+
|
|
182
|
+
def last_error
|
|
183
|
+
p = Native.call("pdf_last_error_message")
|
|
184
|
+
p.null? ? "unknown error" : p.to_s
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def check(status)
|
|
188
|
+
raise Error.new(last_error, status) unless status.zero?
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Run an out-buffer producer { |out_ptr, out_len| status } and return the
|
|
192
|
+
# produced bytes, always freeing the native buffer.
|
|
193
|
+
def take_bytes
|
|
194
|
+
pp = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
195
|
+
pn = Fiddle::Pointer.malloc(Native::SIZEOF_SZ, Fiddle::RUBY_FREE)
|
|
196
|
+
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
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Run a producer { |out_int| status } and return the written int.
|
|
207
|
+
def out_int
|
|
208
|
+
buf = Fiddle::Pointer.malloc(Native::SIZEOF_INT, Fiddle::RUBY_FREE)
|
|
209
|
+
check(yield(buf))
|
|
210
|
+
buf[0, Native::SIZEOF_INT].unpack1("i!")
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
require_relative "rustpdf/document"
|
|
215
|
+
require_relative "rustpdf/editable_doc"
|
|
Binary file
|
metadata
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rustpdf
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.4.2
|
|
5
|
+
platform: x86_64-darwin
|
|
6
|
+
authors:
|
|
7
|
+
- rust-pdf
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-06-29 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/x86_64-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: []
|