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 +4 -4
- data/CHANGELOG.md +10 -0
- data/lib/carve/hexapdf/renderer.rb +80 -5
- data/lib/carve/hexapdf/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 998af42c6a5a9753c0874cbdd003da94d3433303de6051f1a784a7beb3f3d2a1
|
|
4
|
+
data.tar.gz: dfe6a046b7e1026ebe8c9ce6c1364872d58f9e407441049cba2a7dd9c63fce66
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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,
|
|
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"
|
|
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
|
-
|
|
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
|