docpdf 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52751516c7b9baebff20d8f3f9bfae0f9b50753497bedd37a0b690da45b5e22d
4
- data.tar.gz: 211a5c562468626013b8427963c28cd4d2b410b24b688fe7ad5dd994566b8aba
3
+ metadata.gz: 43cbb73b198ee9a4c207afbdf359ffa4ce79b2c66e372169049fe622cce4f562
4
+ data.tar.gz: f08f6d9e7fd5b82a9b69f4e5402b94c4ab8a627c7468ad36373d6842458f2299
5
5
  SHA512:
6
- metadata.gz: dd27677faa12f3ed9e21e66282e8efac2c8e2abbf232a9dc4fd5cc6927da1a7eb0c85ba992ac2672035f7eb01a9c4232fd060ff1113c3a2ebc66b0722c19ac0f
7
- data.tar.gz: 969269269d255c41d6364f6650154a3dfa6c4ba9919e4a5492b9c35e8c74d3277c3aa1d3c0dfd4a8cf4e02cd19cc7b8f895f16d2ff90121b6d342830a113456d
6
+ metadata.gz: 3612856e07798bbee41cd5a2dea00dd21ea67aadd2b4c8412fe2afd4c13dc9f91a334833373ba8f099b7bae1392e6756736da848085a2f2e3a3a04ef962568f0
7
+ data.tar.gz: 53b3025fab9d38dc1bf0d8b960b42088d49b73e4a05f79b6025b18bb9f912bde8da2d0dfacf571bc2b11f12e6c47b9266ef3f30d637e4c1fe1ae59ec937699e4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.5] - 2026-09-18
4
+
5
+ ### Added
6
+ - `config.converter` selects a preferred converter adapter, mirroring `config.stamper`. It only claims the mime types that adapter registered for, so everything else still resolves in registration order and `config.converter = :hexapdf` does not disturb Word or image conversion.
7
+
8
+ ### Fixed
9
+ - Watermarking a structurally valid PDF with an empty page tree raised `NoMethodError` instead of a `DocPDF::ConversionError`. Both stampers and the per-page watermark path now raise `ConversionError` with a clear message.
10
+ - Plain text containing U+2028 LINE SEPARATOR or U+2029 PARAGRAPH SEPARATOR crashed the Prawn converter, since Windows-1252 has no room for them. Both are now translated to a newline, which is what they mean, so text pasted out of word processors and web pages converts cleanly.
11
+ - Adapters no longer leak third-party exceptions to callers rescuing `DocPDF::Error`. The Prawn converter wraps every `Prawn::Errors` class plus `Encoding::UndefinedConversionError`, and both stampers wrap a missing watermark image file.
12
+
3
13
  ## [0.1.4] - 2026-04-08
4
14
 
5
15
  ### Fixed
data/README.md CHANGED
@@ -197,6 +197,11 @@ DocPDF.configure do |config|
197
197
  # Stamper adapter for watermarking (default: nil, auto-detects hexapdf then combine_pdf)
198
198
  config.stamper = :hexapdf # or :combine_pdf
199
199
 
200
+ # Preferred converter adapter (default: nil, uses registration order)
201
+ # Only applies to the MIME types that adapter registered for; everything else
202
+ # still resolves normally, so this will not affect Word or image conversion.
203
+ config.converter = :hexapdf # or :prawn, :rmagick, :mini_magick, ...
204
+
200
205
  # Page size (default: "LETTER")
201
206
  config.page_size = "A4"
202
207
 
@@ -220,7 +225,7 @@ end
220
225
 
221
226
  Converter adapters (for format-to-PDF conversion) are auto-detected based on MIME type and gem availability. The first available adapter wins, in registration order:
222
227
 
223
- - **text/plain**: Prawn, then HexaPDF
228
+ - **text/plain**: Prawn, then HexaPDF (set `config.converter` to reverse this)
224
229
  - **image/\***: RMagick, then MiniMagick
225
230
  - **Office formats**: LibreOffice (always available if installed)
226
231
  - **application/pdf**: Passthrough (returned unchanged)
@@ -235,8 +240,8 @@ DocPDF has two types of adapters:
235
240
  | Adapter | Gem | Formats |
236
241
  |---------|-----|---------|
237
242
  | Soffice | None (system) | Word, Excel, PowerPoint, ODF, CSV, HTML, RTF |
238
- | Prawn | `prawn` | Plain text |
239
- | HexaPDF | `hexapdf` | Plain text |
243
+ | Prawn | `prawn` | Plain text (wraps long lines; built-in fonts are Windows-1252 only) |
244
+ | HexaPDF | `hexapdf` | Plain text (wider character support; does not wrap long lines) |
240
245
  | RMagick | `rmagick` | JPEG, PNG, HEIC, WebP |
241
246
  | MiniMagick | `mini_magick` | JPEG, PNG, HEIC, WebP |
242
247
  | Passthrough | None | PDF (returned unchanged) |
@@ -1,5 +1,6 @@
1
1
  require "prawn"
2
2
  require_relative "base"
3
+ require_relative "../../prawn_errors"
3
4
 
4
5
  module DocPDF
5
6
  module Adapters
@@ -7,18 +8,30 @@ module DocPDF
7
8
  class Prawn < Base
8
9
  MIME_TYPES = %w[text/plain].freeze
9
10
 
11
+ # U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR mean "break the
12
+ # line", but Windows-1252 has no room for them, so Prawn's built-in fonts
13
+ # reject them. Text pasted out of word processors and web pages carries
14
+ # them routinely, so translate to a newline instead of failing on it.
15
+ LINE_SEPARATORS = /[

]/
16
+
10
17
  class << self
11
18
  def convert(data, _source_filename)
12
19
  config = DocPDF.configuration
13
20
  opts = config.text_options
14
- content = data.dup.force_encoding("UTF-8")
21
+ content = normalize(data)
15
22
  pdf = ::Prawn::Document.new(page_size: config.page_size, margin: opts[:margins])
16
23
  pdf.font(opts[:font], size: opts[:font_size])
17
24
  pdf.text content, color: opts[:color]
18
25
  pdf.render
19
- rescue ::Prawn::Errors::UnknownFont, ::Prawn::Errors::CannotFit => e
26
+ rescue *PRAWN_ERRORS, ::Encoding::UndefinedConversionError => e
20
27
  raise ConversionError, "Prawn failed to render text to PDF: #{e.message}"
21
28
  end
29
+
30
+ private
31
+
32
+ def normalize(data)
33
+ data.dup.force_encoding("UTF-8").gsub(LINE_SEPARATORS, "\n")
34
+ end
22
35
  end
23
36
  end
24
37
  end
@@ -1,6 +1,7 @@
1
1
  require "combine_pdf"
2
2
  require "prawn"
3
3
  require_relative "base"
4
+ require_relative "../../prawn_errors"
4
5
 
5
6
  module DocPDF
6
7
  module Adapters
@@ -10,6 +11,7 @@ module DocPDF
10
11
  def stamp(data, stamps, page_indices: nil)
11
12
  source = ::CombinePDF.parse(data)
12
13
  source_page = source.pages.first
14
+ raise ConversionError, "Cannot stamp a PDF with no pages" unless source_page
13
15
  page_w = source_page[:MediaBox][2].to_f
14
16
  page_h = source_page[:MediaBox][3].to_f
15
17
 
@@ -20,7 +22,7 @@ module DocPDF
20
22
  page << stamp_page if page_indices.nil? || page_indices.include?(idx)
21
23
  end
22
24
  source.to_pdf
23
- rescue ::CombinePDF::ParsingError => e
25
+ rescue ::CombinePDF::ParsingError, *PRAWN_ERRORS, Errno::ENOENT => e
24
26
  raise ConversionError, "CombinePDF failed to stamp PDF: #{e.message}"
25
27
  end
26
28
 
@@ -9,6 +9,7 @@ module DocPDF
9
9
  def stamp(data, stamps, page_indices: nil)
10
10
  doc = HexaPDF::Document.new(io: StringIO.new(data))
11
11
  source_page = doc.pages[0]
12
+ raise ConversionError, "Cannot stamp a PDF with no pages" unless source_page
12
13
  page_w = source_page.box.width
13
14
  page_h = source_page.box.height
14
15
 
@@ -21,7 +22,7 @@ module DocPDF
21
22
  end
22
23
 
23
24
  write_to_string(doc)
24
- rescue HexaPDF::Error => e
25
+ rescue HexaPDF::Error, Errno::ENOENT => e
25
26
  raise ConversionError, "HexaPDF failed to stamp PDF: #{e.message}"
26
27
  end
27
28
 
@@ -1,6 +1,7 @@
1
1
  module DocPDF
2
2
  class Configuration
3
3
  attr_accessor :soffice_path,
4
+ :converter,
4
5
  :stamper,
5
6
  :page_size,
6
7
  :text_options,
@@ -22,6 +23,7 @@ module DocPDF
22
23
 
23
24
  def initialize
24
25
  @soffice_path = "soffice"
26
+ @converter = nil
25
27
  @stamper = nil
26
28
  @page_size = "LETTER"
27
29
  @text_options = TEXT_DEFAULTS.dup
@@ -8,12 +8,44 @@ module DocPDF
8
8
  end
9
9
 
10
10
  def resolve(mime_type)
11
+ configured = DocPDF.configuration.converter
12
+ return resolve_configured(configured, mime_type) if configured
13
+
14
+ resolve_registered(mime_type)
15
+ end
16
+
17
+ private
18
+
19
+ def load_adapter(entry)
20
+ require entry[:require_name] if entry[:require_name]
21
+ entry[:loader].call
22
+ end
23
+
24
+ # A configured converter only claims the mime types it registered for, so
25
+ # anything else still falls through to the normal registration order. That
26
+ # keeps config.converter = :hexapdf from breaking Word conversion.
27
+ def resolve_configured(name, mime_type)
28
+ entry = @adapters.find { |e| e[:name] == name }
29
+ unless entry
30
+ valid_names = @adapters.map { |e| e[:name].inspect }.join(", ")
31
+ raise AdapterNotFoundError, "Unknown converter: #{name.inspect}. Valid converters are: #{valid_names}."
32
+ end
33
+
34
+ return resolve_registered(mime_type) unless entry[:mime_types].include?(mime_type)
35
+
36
+ begin
37
+ load_adapter(entry)
38
+ rescue LoadError => e
39
+ raise AdapterNotFoundError, "Converter #{name.inspect} requires gems that are not installed: #{e.message}"
40
+ end
41
+ end
42
+
43
+ def resolve_registered(mime_type)
11
44
  missing_gems = []
12
45
 
13
46
  @adapters.each do |entry|
14
47
  next unless entry[:mime_types].include?(mime_type)
15
- require entry[:require_name] if entry[:require_name]
16
- return entry[:loader].call
48
+ return load_adapter(entry)
17
49
  rescue LoadError
18
50
  missing_gems << entry[:require_name]
19
51
  next
@@ -27,8 +59,6 @@ module DocPDF
27
59
  resolve_fallback
28
60
  end
29
61
 
30
- private
31
-
32
62
  def resolve_fallback
33
63
  entry = @adapters.find { |e| e[:name] == :fallback }
34
64
  raise AdapterNotFoundError, "No converter found and no fallback registered." unless entry
@@ -0,0 +1,14 @@
1
+ require "prawn"
2
+
3
+ module DocPDF
4
+ # Prawn defines every error class directly under StandardError with no shared
5
+ # ancestor, so there is no namespace to rescue. Collecting them lets adapters
6
+ # wrap the whole family in DocPDF::ConversionError rather than leaking Prawn's
7
+ # exceptions to callers who only rescue DocPDF::Error.
8
+ #
9
+ # Only require this from files that already require "prawn"; prawn is optional.
10
+ PRAWN_ERRORS = ::Prawn::Errors.constants
11
+ .map { |name| ::Prawn::Errors.const_get(name) }
12
+ .select { |const| const.is_a?(Class) && const <= StandardError }
13
+ .freeze
14
+ end
@@ -1,3 +1,3 @@
1
1
  module DocPDF
2
- VERSION = "0.1.4"
2
+ VERSION = "0.1.5"
3
3
  end
@@ -138,6 +138,8 @@ module DocPDF
138
138
 
139
139
  def stamp_per_page(stamper)
140
140
  page_count = pdf_page_count
141
+ raise ConversionError, "Cannot stamp a PDF with no pages" if page_count.zero?
142
+
141
143
  result = @pdf_bytes
142
144
 
143
145
  build_page_stamp_map(page_count).each do |page_indices, stamps_for_pages|
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
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Velocity Labs, LLC
@@ -40,6 +40,7 @@ files:
40
40
  - lib/docpdf/errors.rb
41
41
  - lib/docpdf/input_normalizer.rb
42
42
  - lib/docpdf/mime_detector.rb
43
+ - lib/docpdf/prawn_errors.rb
43
44
  - lib/docpdf/result.rb
44
45
  - lib/docpdf/stamper_resolver.rb
45
46
  - lib/docpdf/version.rb