rustpdf 0.1.0-x86_64-linux
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 +186 -0
- data/lib/rustpdf/editable_doc.rb +136 -0
- data/lib/rustpdf/native.rb +143 -0
- data/lib/rustpdf.rb +142 -0
- data/vendor/x86_64-linux/libpdf_ffi.so +0 -0
- metadata +50 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 338ced9bc04f0b7ec32aaf50f6f3a6dae01b06330a635624af41c28ccaa255b2
|
|
4
|
+
data.tar.gz: a3d214e870c8e3c7a064235ad273c8637cd801a423ed32500589eab7e13407a0
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6e9c32134dff46cf7c803414833a6a17e79538a4de354c71c6185179a0310f951292546180b652b0f3d95fdf949e6bd43ff30974e2f21a93836b0e42709d8c5b
|
|
7
|
+
data.tar.gz: 8b0fc3c9289e2285535a6a58812e4dd99b2317b235bd4f309dc23399bb96ec9d466d8482efb02205d8eba5caf5bbbc5230eeddfbc60cac97f76672097480c9f1
|
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"
|
|
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.1.0
|
|
5
|
+
platform: x86_64-linux
|
|
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/x86_64-linux/libpdf_ffi.so
|
|
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: []
|