docpdf 0.1.5 → 0.1.7
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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +40 -1
- data/lib/docpdf/adapters/converters/hexapdf.rb +9 -0
- data/lib/docpdf/adapters/converters/prawn.rb +9 -0
- data/lib/docpdf/adapters/stampers/combine_pdf.rb +9 -0
- data/lib/docpdf/adapters/stampers/hexapdf.rb +24 -3
- data/lib/docpdf/configuration.rb +2 -0
- data/lib/docpdf/font_files.rb +37 -0
- data/lib/docpdf/input_normalizer.rb +2 -0
- data/lib/docpdf/version.rb +1 -1
- data/lib/docpdf/watermarker.rb +3 -1
- data/lib/docpdf.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4fa01d67dead11a1f62eb4ff7f6f387599b97ec99d0c91bd5644e60e98fd26a6
|
|
4
|
+
data.tar.gz: 1dc447f55133af1a3ff94ad3aaaeca8014a1560cdd6db5cf2d85ab30220afa1e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3cfa330a65d41f8c24866b04da28d15a795df5c826f2459a9ae1c46c86c470147e92c6ff75952579d3bfd3b6fd4804747a712b868fc829102fab3ece963d0a2d
|
|
7
|
+
data.tar.gz: aa22481e4dfbb0966c9f95b1ecd5924680c491729a3c0ac8b873d2aca03c49b4889a936ce57400a3681594c5ac18078da66c8fa78b20cbed9da211316115f883
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.7] - 2026-10-08
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- The HexaPDF stamper raised `ConversionError` on PDFs with spec violations HexaPDF cannot correct, even though viewers render them fine. One example is a tagged PDF exported from Canva with a structure element missing its parent. The stamper now corrects what HexaPDF can and writes the rest as it came in, the way the `hexapdf` CLI does.
|
|
7
|
+
- `DocPDF.convert` raised `NameError: uninitialized constant DocPDF::InputNormalizer::Pathname` on Ruby 3.3 and 3.4 when nothing else had loaded `pathname`. Bundler and Rails both load it, so only plain Ruby scripts were affected. `DocPDF.watermark` with a file path and `font_file` given as a hash hit the same error. DocPDF now requires `pathname` itself.
|
|
8
|
+
|
|
9
|
+
## [0.1.6] - 2026-09-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `font_file` registers a TrueType font for text conversion (`config.text_options`) and text watermarks (`config.watermark_options`, or per stamp). PDF's built-in fonts are limited to Windows-1252, so Cyrillic, Greek and other non-Latin text previously raised `ConversionError` with no way around it. Takes a path for the regular weight or a hash of `normal`/`bold`/`italic`/`bold_italic`, and is supported by both text converters and both stampers.
|
|
13
|
+
|
|
3
14
|
## [0.1.5] - 2026-09-18
|
|
4
15
|
|
|
5
16
|
### Added
|
data/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
Convert documents from any common format to PDF, with optional watermarking. Zero hard dependencies; bring your own PDF library.
|
|
10
10
|
|
|
11
11
|
[](https://github.com/velocity-labs/docpdf/actions/workflows/ci.yml)
|
|
12
|
-
[](https://rubygems.org/gems/docpdf)
|
|
13
13
|
|
|
14
14
|
<br clear="left">
|
|
15
15
|
|
|
@@ -208,6 +208,7 @@ DocPDF.configure do |config|
|
|
|
208
208
|
# Plain text file conversion defaults
|
|
209
209
|
config.text_options = {
|
|
210
210
|
font: "Helvetica", # default: "Courier"
|
|
211
|
+
font_file: nil, # default: nil (see "Non-Latin text" below)
|
|
211
212
|
font_size: 12, # default: 10
|
|
212
213
|
margins: [72, 72, 72, 72], # default: [50, 50, 50, 50] (points: top, right, bottom, left)
|
|
213
214
|
color: "000000", # default: "333333" (hex)
|
|
@@ -216,6 +217,7 @@ DocPDF.configure do |config|
|
|
|
216
217
|
# Text watermark defaults (per-stamp options override these)
|
|
217
218
|
config.watermark_options = {
|
|
218
219
|
font: "Times", # default: "Helvetica"
|
|
220
|
+
font_file: nil, # default: nil (see "Non-Latin text" below)
|
|
219
221
|
font_size: 96, # default: 72
|
|
220
222
|
color: "FF0000", # default: "AAAAAA" (hex)
|
|
221
223
|
rotation: 30, # default: 45 (degrees counter-clockwise)
|
|
@@ -231,6 +233,43 @@ Converter adapters (for format-to-PDF conversion) are auto-detected based on MIM
|
|
|
231
233
|
- **application/pdf**: Passthrough (returned unchanged)
|
|
232
234
|
- **Unknown formats**: Fallback (tries LibreOffice, then returns raw data)
|
|
233
235
|
|
|
236
|
+
### Non-Latin text
|
|
237
|
+
|
|
238
|
+
PDF's built-in fonts only cover the Windows-1252 character set, so text outside it
|
|
239
|
+
(Cyrillic, Greek, CJK, emoji, and many accented forms) cannot be rendered and raises
|
|
240
|
+
`DocPDF::ConversionError`. Point `font_file` at a TrueType font to lift that limit:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
DocPDF.configure do |config|
|
|
244
|
+
config.text_options = config.text_options.merge(
|
|
245
|
+
font: "DejaVuSans",
|
|
246
|
+
font_file: "/path/to/DejaVuSans.ttf"
|
|
247
|
+
)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
DocPDF.convert("resume.txt") # renders Cyrillic, Greek, and more
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`font_file` takes a path for the regular weight, or a hash to register several styles:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
font_file: {
|
|
257
|
+
normal: "/fonts/DejaVuSans.ttf",
|
|
258
|
+
bold: "/fonts/DejaVuSans-Bold.ttf",
|
|
259
|
+
italic: "/fonts/DejaVuSans-Oblique.ttf",
|
|
260
|
+
bold_italic: "/fonts/DejaVuSans-BoldOblique.ttf"
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
The same option works for text watermarks, via `config.watermark_options` or per stamp:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
DocPDF.watermark("doc.pdf", { text: "ЧЕРНОВИК", font: "DejaVuSans", font_file: "/fonts/DejaVuSans.ttf" })
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
A font only renders the characters it actually contains. DejaVu covers Latin, Cyrillic
|
|
271
|
+
and Greek but not CJK, so pick a font that covers the scripts you expect.
|
|
272
|
+
|
|
234
273
|
## Adapters
|
|
235
274
|
|
|
236
275
|
DocPDF has two types of adapters:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "hexapdf"
|
|
2
2
|
require_relative "base"
|
|
3
|
+
require_relative "../../font_files"
|
|
3
4
|
|
|
4
5
|
module DocPDF
|
|
5
6
|
module Adapters
|
|
@@ -22,6 +23,7 @@ module DocPDF
|
|
|
22
23
|
page_size = normalize_page_size(config.page_size)
|
|
23
24
|
|
|
24
25
|
doc = HexaPDF::Document.new
|
|
26
|
+
register_font(doc, opts[:font], opts[:font_file])
|
|
25
27
|
page = doc.pages.add(page_size)
|
|
26
28
|
canvas = page.canvas
|
|
27
29
|
|
|
@@ -51,6 +53,13 @@ module DocPDF
|
|
|
51
53
|
|
|
52
54
|
private
|
|
53
55
|
|
|
56
|
+
def register_font(doc, name, font_file)
|
|
57
|
+
map = FontFiles.for_hexapdf(font_file)
|
|
58
|
+
return unless map
|
|
59
|
+
|
|
60
|
+
doc.config["font.map"] = (doc.config["font.map"] || {}).merge(name => map)
|
|
61
|
+
end
|
|
62
|
+
|
|
54
63
|
def normalize_page_size(size)
|
|
55
64
|
return size if size.is_a?(Symbol)
|
|
56
65
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require "prawn"
|
|
2
2
|
require_relative "base"
|
|
3
3
|
require_relative "../../prawn_errors"
|
|
4
|
+
require_relative "../../font_files"
|
|
4
5
|
|
|
5
6
|
module DocPDF
|
|
6
7
|
module Adapters
|
|
@@ -20,6 +21,7 @@ module DocPDF
|
|
|
20
21
|
opts = config.text_options
|
|
21
22
|
content = normalize(data)
|
|
22
23
|
pdf = ::Prawn::Document.new(page_size: config.page_size, margin: opts[:margins])
|
|
24
|
+
register_font(pdf, opts[:font], opts[:font_file])
|
|
23
25
|
pdf.font(opts[:font], size: opts[:font_size])
|
|
24
26
|
pdf.text content, color: opts[:color]
|
|
25
27
|
pdf.render
|
|
@@ -29,6 +31,13 @@ module DocPDF
|
|
|
29
31
|
|
|
30
32
|
private
|
|
31
33
|
|
|
34
|
+
def register_font(pdf, name, font_file)
|
|
35
|
+
families = FontFiles.for_prawn(font_file)
|
|
36
|
+
return unless families
|
|
37
|
+
|
|
38
|
+
pdf.font_families.update(name => families)
|
|
39
|
+
end
|
|
40
|
+
|
|
32
41
|
def normalize(data)
|
|
33
42
|
data.dup.force_encoding("UTF-8").gsub(LINE_SEPARATORS, "\n")
|
|
34
43
|
end
|
|
@@ -2,6 +2,7 @@ require "combine_pdf"
|
|
|
2
2
|
require "prawn"
|
|
3
3
|
require_relative "base"
|
|
4
4
|
require_relative "../../prawn_errors"
|
|
5
|
+
require_relative "../../font_files"
|
|
5
6
|
|
|
6
7
|
module DocPDF
|
|
7
8
|
module Adapters
|
|
@@ -62,6 +63,13 @@ module DocPDF
|
|
|
62
63
|
[img_w, img_h]
|
|
63
64
|
end
|
|
64
65
|
|
|
66
|
+
def register_font(pdf, name, font_file)
|
|
67
|
+
families = FontFiles.for_prawn(font_file)
|
|
68
|
+
return unless families
|
|
69
|
+
|
|
70
|
+
pdf.font_families.update(name => families)
|
|
71
|
+
end
|
|
72
|
+
|
|
65
73
|
def render_image_stamp(pdf, stamp)
|
|
66
74
|
page_w = pdf.bounds.width
|
|
67
75
|
page_h = pdf.bounds.height
|
|
@@ -88,6 +96,7 @@ module DocPDF
|
|
|
88
96
|
page_w = pdf.bounds.width
|
|
89
97
|
page_h = pdf.bounds.height
|
|
90
98
|
|
|
99
|
+
register_font(pdf, stamp[:font], stamp[:font_file])
|
|
91
100
|
pdf.font(stamp[:font])
|
|
92
101
|
font_size = fit_font_size(stamp[:font_size], stamp[:text], pdf, page_w, page_h)
|
|
93
102
|
text_w = pdf.width_of(stamp[:text], size: font_size)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "hexapdf"
|
|
2
2
|
require_relative "base"
|
|
3
|
+
require_relative "../../font_files"
|
|
3
4
|
|
|
4
5
|
module DocPDF
|
|
5
6
|
module Adapters
|
|
@@ -21,7 +22,7 @@ module DocPDF
|
|
|
21
22
|
page.canvas(type: :overlay).xobject(stamp_form, at: [0, 0]) if page_indices.nil? || page_indices.include?(idx)
|
|
22
23
|
end
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
write_to_string_leniently(doc)
|
|
25
26
|
rescue HexaPDF::Error, Errno::ENOENT => e
|
|
26
27
|
raise ConversionError, "HexaPDF failed to stamp PDF: #{e.message}"
|
|
27
28
|
end
|
|
@@ -88,6 +89,7 @@ module DocPDF
|
|
|
88
89
|
page_w = page.box.width
|
|
89
90
|
page_h = page.box.height
|
|
90
91
|
|
|
92
|
+
register_font(doc, stamp[:font], stamp[:font_file])
|
|
91
93
|
font = doc.fonts.add(stamp[:font])
|
|
92
94
|
glyph_units = font.decode_utf8(stamp[:text]).sum { |g| g.width }
|
|
93
95
|
font_size = fit_font_size(stamp[:font_size], glyph_units, page_w, page_h)
|
|
@@ -116,11 +118,30 @@ module DocPDF
|
|
|
116
118
|
end
|
|
117
119
|
end
|
|
118
120
|
|
|
119
|
-
def
|
|
121
|
+
def register_font(doc, name, font_file)
|
|
122
|
+
map = FontFiles.for_hexapdf(font_file)
|
|
123
|
+
return unless map
|
|
124
|
+
|
|
125
|
+
doc.config["font.map"] = (doc.config["font.map"] || {}).merge(name => map)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def write_to_string(doc, **write_options)
|
|
120
129
|
io = StringIO.new
|
|
121
|
-
doc.write(io)
|
|
130
|
+
doc.write(io, **write_options)
|
|
122
131
|
io.string
|
|
123
132
|
end
|
|
133
|
+
|
|
134
|
+
# The caller's PDF may break the spec in ways HexaPDF cannot repair, like a
|
|
135
|
+
# tagged PDF whose structure elements have no parent. Viewers open those
|
|
136
|
+
# fine, so correct what can be corrected and write the rest as it came in,
|
|
137
|
+
# the way the hexapdf CLI does. Objects are validated one at a time because
|
|
138
|
+
# Document#validate stops at the first one it cannot correct, and the stamp
|
|
139
|
+
# form only becomes an indirect object when validation reaches its page.
|
|
140
|
+
def write_to_string_leniently(doc)
|
|
141
|
+
doc.trailer.validate(auto_correct: true)
|
|
142
|
+
doc.each { |obj| obj.validate(auto_correct: true) }
|
|
143
|
+
write_to_string(doc, validate: false)
|
|
144
|
+
end
|
|
124
145
|
end
|
|
125
146
|
end
|
|
126
147
|
end
|
data/lib/docpdf/configuration.rb
CHANGED
|
@@ -9,6 +9,7 @@ module DocPDF
|
|
|
9
9
|
|
|
10
10
|
TEXT_DEFAULTS = {
|
|
11
11
|
font: "Courier",
|
|
12
|
+
font_file: nil,
|
|
12
13
|
font_size: 10,
|
|
13
14
|
margins: [50, 50, 50, 50],
|
|
14
15
|
color: "333333",
|
|
@@ -16,6 +17,7 @@ module DocPDF
|
|
|
16
17
|
|
|
17
18
|
WATERMARK_DEFAULTS = {
|
|
18
19
|
font: "Helvetica",
|
|
20
|
+
font_file: nil,
|
|
19
21
|
font_size: 72,
|
|
20
22
|
color: "AAAAAA",
|
|
21
23
|
rotation: 45,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
3
|
+
module DocPDF
|
|
4
|
+
# Prawn's built-in AFM fonts only cover Windows-1252, so any text outside it
|
|
5
|
+
# fails to render. Pointing an adapter at a TrueType file lifts that limit.
|
|
6
|
+
# Each backend wants the styles keyed differently, so normalize once here.
|
|
7
|
+
module FontFiles
|
|
8
|
+
STYLES = %i[normal bold italic bold_italic].freeze
|
|
9
|
+
|
|
10
|
+
# HexaPDF calls the unstyled variant :none where Prawn calls it :normal.
|
|
11
|
+
HEXAPDF_STYLES = { normal: :none, bold: :bold, italic: :italic, bold_italic: :bold_italic }.freeze
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
# Accepts a path for the regular weight, or a hash of styles:
|
|
15
|
+
# "DejaVuSans.ttf"
|
|
16
|
+
# { normal: "...", bold: "...", italic: "...", bold_italic: "..." }
|
|
17
|
+
def normalize(font_file)
|
|
18
|
+
return if font_file.nil?
|
|
19
|
+
return { normal: font_file.to_s } if font_file.is_a?(String) || font_file.is_a?(Pathname)
|
|
20
|
+
|
|
21
|
+
styles = font_file.to_h.transform_keys(&:to_sym).slice(*STYLES)
|
|
22
|
+
styles.empty? ? nil : styles.transform_values(&:to_s)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def for_hexapdf(font_file)
|
|
26
|
+
styles = normalize(font_file)
|
|
27
|
+
return unless styles
|
|
28
|
+
|
|
29
|
+
styles.each_with_object({}) { |(style, path), map| map[HEXAPDF_STYLES.fetch(style)] = path }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def for_prawn(font_file)
|
|
33
|
+
normalize(font_file)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/docpdf/version.rb
CHANGED
data/lib/docpdf/watermarker.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
|
|
1
3
|
module DocPDF
|
|
2
4
|
class Watermarker
|
|
3
5
|
POSITIONS = %i[center top bottom left right top_left top_right bottom_left bottom_right].freeze
|
|
@@ -46,7 +48,7 @@ module DocPDF
|
|
|
46
48
|
merged = STAMP_DEFAULTS.merge(stamp)
|
|
47
49
|
if merged[:text]
|
|
48
50
|
wm = DocPDF.configuration.watermark_options
|
|
49
|
-
{ font: wm[:font], font_size: wm[:font_size], color: wm[:color], rotation: wm[:rotation] }.merge(merged)
|
|
51
|
+
{ font: wm[:font], font_file: wm[:font_file], font_size: wm[:font_size], color: wm[:color], rotation: wm[:rotation] }.merge(merged)
|
|
50
52
|
else
|
|
51
53
|
merged
|
|
52
54
|
end
|
data/lib/docpdf.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: docpdf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Velocity Labs, LLC
|
|
@@ -38,6 +38,7 @@ files:
|
|
|
38
38
|
- lib/docpdf/converter.rb
|
|
39
39
|
- lib/docpdf/converter_resolver.rb
|
|
40
40
|
- lib/docpdf/errors.rb
|
|
41
|
+
- lib/docpdf/font_files.rb
|
|
41
42
|
- lib/docpdf/input_normalizer.rb
|
|
42
43
|
- lib/docpdf/mime_detector.rb
|
|
43
44
|
- lib/docpdf/prawn_errors.rb
|