carve-hexapdf 0.1.6 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e542a29c024a850009d9f9fa32d6fe69ba870a97eab23f595522878d7da1c367
4
- data.tar.gz: 05d84eaa30d5ad7c4d6f111dc442f88a113bf8a7aa817849df4ee7b58bb5942f
3
+ metadata.gz: 998af42c6a5a9753c0874cbdd003da94d3433303de6051f1a784a7beb3f3d2a1
4
+ data.tar.gz: dfe6a046b7e1026ebe8c9ce6c1364872d58f9e407441049cba2a7dd9c63fce66
5
5
  SHA512:
6
- metadata.gz: 1382a76d094f651b2cc1b6cbae0ce65409d27222e02f4355361fce93ef6c9cc873b1ebd58ea01b6f02003ebb112ee8228784bb4dddf0aea60ac7186fc4e48f74
7
- data.tar.gz: 25cd7967c9fe80a6c18df8a2d261c6d27d041a874029c056b52f43f621af5e97f939e3a19f9c141eaa51a87292829445ac58b8074a09a477c8afd61cff31b149
6
+ metadata.gz: c9a88ff35c2faaa48db5a26344b49837d53b00212db3bd9f3ba44a743d32a212c16cbabe1b8fe7c2a0469882a93c7bd3684faaeddf91c3329af371440783c7de
7
+ data.tar.gz: 203abfd74c94db24dc267e14c8ab93dce5acc49e6ff98312fa17fb0c211b40f4c87dea3eafd62f9e9c8b78e06cf2025ae8f38a9a01ad849a072eac5aa3deef32
data/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.7] - 2026-11-12
11
+
12
+ The default fonts no longer crash the render: `x <= 10` and CJK text now produce a PDF.
13
+
14
+ ### Fixes
15
+
16
+ - Text the font cannot draw no longer aborts the render. With the default Type1 fonts, `<=`, `>=` and `!=` print as written instead of `≤ ≥ ≠`, and other characters without a glyph print as `?` with a warning; a TrueType `base_font:` draws them (#70, #71).
17
+
18
+ **Full Changelog**: https://github.com/markup-carve/carve-hexapdf/compare/v0.1.6...v0.1.7
19
+
10
20
  ## [0.1.6] - 2026-11-12
11
21
 
12
22
  Definition lists and table cell alignment now reach the PDF, the gem loads on Ruby 3.4, and the engine moves to carve-lang 0.1.14.
@@ -61,6 +61,8 @@ module Carve
61
61
  highlight_color: highlight_color,
62
62
  styles: styles))
63
63
  @renderers = renderers || {}
64
+ @drawable = {}
65
+ @warned = {}
64
66
  end
65
67
 
66
68
  def render_document(doc)
@@ -186,7 +188,7 @@ module Carve
186
188
  content = resolve_nbsp(node[:content]).chomp
187
189
  style = style_for("code.block", inherited_font: true, with_box: true)
188
190
  box = style.delete(:box)
189
- target.text(content, **style, box_style: box)
191
+ target.text(drawable_text(content, style[:font] || code_font), **style, box_style: box)
190
192
  end
191
193
 
192
194
  def list(node, target)
@@ -386,7 +388,8 @@ module Carve
386
388
  else
387
389
  alt = resolve_nbsp(node[:alt])
388
390
  alt = "[image: #{node[:src]}]" if alt.empty?
389
- target.formatted_text([{ text: alt, font: [base_font, { variant: :italic }] }],
391
+ font = [base_font, { variant: :italic }]
392
+ target.formatted_text([{ text: drawable_text(alt, font), font: font }],
390
393
  margin: [0, 0, BLOCK_GAP])
391
394
  end
392
395
  end
@@ -399,7 +402,8 @@ module Carve
399
402
  style = style_for("math", with_box: true)
400
403
  box = style.delete(:box)
401
404
  style[:font] ||= code_font
402
- target.text(node[:content].to_s, **style, text_align: :center, box_style: box)
405
+ target.text(drawable_text(node[:content].to_s, style[:font]), **style,
406
+ text_align: :center, box_style: box)
403
407
  end
404
408
 
405
409
  def thematic_break(target)
@@ -649,7 +653,8 @@ module Carve
649
653
  Array(pair[:annotation]).each { |part| emit_inline(part, ctx, out) }
650
654
  out << run(")", ctx)
651
655
  end
652
- when "smart_punctuation" then out << run(smart_punctuation_text(node), ctx)
656
+ when "smart_punctuation"
657
+ out << run(smart_punctuation_text(node), ctx, fallback: node[:value])
653
658
  # A character the author escaped. The backslash is authoring syntax, so
654
659
  # the page shows the character - but the node has no children, so
655
660
  # without this arm it falls through to the branch that emits children
@@ -767,7 +772,9 @@ module Carve
767
772
  text.to_s.gsub(RESOLVED_NBSP, NBSP)
768
773
  end
769
774
 
770
- def run(text, ctx)
775
+ # +fallback+ is the author's spelling of the same text, drawn instead when
776
+ # the run font cannot draw +text+ (a smart-typography glyph).
777
+ def run(text, ctx, fallback: nil)
771
778
  item = { text: resolve_nbsp(text) }
772
779
  if ctx[:code]
773
780
  # :box is a block-level pseudo-property (e.g. inherited from a user
@@ -793,9 +800,77 @@ module Carve
793
800
  item[:link] = ctx[:link]
794
801
  item.merge!(run_style("link"))
795
802
  end
803
+ font = item[:font] || ctx[:font_family] || base_font
804
+ item[:text] = drawable_text(item[:text], font, fallback: fallback)
796
805
  item
797
806
  end
798
807
 
808
+ # A character the font cannot encode raises inside HexaPDF and loses the
809
+ # whole document (carve-hexapdf#70). That is either a missing glyph (CJK
810
+ # in Times) or a glyph the font's encoding cannot reach (standard Type1
811
+ # Times has `lessequal`, WinAnsi has no slot for it).
812
+ def drawable_text(text, font, fallback: nil)
813
+ wrapper = font_wrapper(font)
814
+ return text if wrapper.nil? || text.each_char.all? { |ch| drawable?(wrapper, ch) }
815
+
816
+ if fallback && fallback.to_s.each_char.all? { |ch| drawable?(wrapper, ch) }
817
+ return fallback.to_s
818
+ end
819
+
820
+ text.each_char.map do |ch|
821
+ next ch if drawable?(wrapper, ch)
822
+
823
+ warn_missing_glyph(wrapper, ch)
824
+ drawable?(wrapper, REPLACEMENT) ? REPLACEMENT : ""
825
+ end.join
826
+ end
827
+
828
+ REPLACEMENT = "?"
829
+
830
+ def font_wrapper(font)
831
+ return font if font.respond_to?(:decode_codepoint)
832
+
833
+ name, opts = font.is_a?(Array) ? font : [font, {}]
834
+ return name if name.respond_to?(:decode_codepoint)
835
+
836
+ document = @c.document
837
+ name ||= document.config["font.default"]
838
+ document.fonts.add(name, variant: (opts || {})[:variant] || :none)
839
+ rescue StandardError
840
+ nil
841
+ end
842
+
843
+ # Mirrors HexaPDF: decode, let `font.on_invalid_glyph` (and so any
844
+ # configured `font.fallback`) supply a missing glyph, then encode. Tabs,
845
+ # line breaks and the like are layout controls, never drawn.
846
+ def drawable?(wrapper, char)
847
+ key = [wrapper, char]
848
+ return @drawable[key] if @drawable.key?(key)
849
+
850
+ @drawable[key] = encodable?(wrapper, char)
851
+ end
852
+
853
+ def encodable?(wrapper, char)
854
+ glyphs = [wrapper.decode_codepoint(char.ord)]
855
+ if glyphs.first.is_a?(::HexaPDF::Font::InvalidGlyph)
856
+ return true if glyphs.first.control_char?
857
+
858
+ glyphs = @c.document.config["font.on_invalid_glyph"].call(char.ord, glyphs.first)
859
+ end
860
+ glyphs.all? { |g| !g.is_a?(::HexaPDF::Font::InvalidGlyph) && g.font_wrapper.encode(g) }
861
+ rescue ::HexaPDF::Error
862
+ false
863
+ end
864
+
865
+ def warn_missing_glyph(wrapper, char)
866
+ return if @warned[[wrapper, char]]
867
+
868
+ @warned[[wrapper, char]] = true
869
+ name = wrapper.wrapped_font.full_name rescue wrapper.to_s
870
+ warn format("carve-hexapdf: font '%s' has no glyph for U+%04X %p; drawing %p instead",
871
+ name, char.ord, char, REPLACEMENT)
872
+ end
873
+
799
874
  # Link/highlight styles are decoration overlays on a run that may already
800
875
  # carry a variant or code font; the font they inherit from +base+ must not
801
876
  # clobber it (only a font set explicitly on the key itself wins), and a
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Carve
4
4
  module Hexapdf
5
- VERSION = "0.1.6"
5
+ VERSION = "0.1.7"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carve-hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - markup-carve