idml 0.10.6 → 0.10.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/TODO.pdf/139-hyphen-break-opportunities.md +24 -0
- data/TODO.pdf/140-cap-xheight-first-baselines.md +29 -0
- data/TODO.pdf/141-perf-hundred-pages.md +22 -0
- data/TODO.pdf/61-known-limitations.md +10 -6
- data/lib/idml/render/renderers/text_frame_renderer.rb +49 -11
- data/lib/idml/text_engine/line_breaker.rb +24 -13
- data/lib/idml/text_engine/pdfrb_font_metrics.rb +6 -0
- data/lib/idml/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 943028d5bf17ca306e3cce0d23c4b042ddaa9473295f2fa7f4af43009a7423a0
|
|
4
|
+
data.tar.gz: b9c3c1e5421ffa4de44396dbc94124434dda470c3f10fabd5ddd81146c3991c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86717ea81b1ede4576d3d41e7026cd2731ec40024ea887d33a03c4ff7771384cb0ae9fd04f5948a322d48986dae2946adcebf167a7c51ae3ce7890a28726e7ee
|
|
7
|
+
data.tar.gz: 12a2089f4600a04fe5cd1d74e409c0933dc6f691156ffca4eeb87fcb25fd5d89e2a1ec58261c8a087ea00b93cf42abee0bb54d64f72032dc3b6bbe919f6cc6b4
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# TODO PDF 139: Hyphen break opportunities in the LineBreaker
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-27
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
The greedy line breaker only treated spaces as break opportunities.
|
|
8
|
+
Long compound words ("state-of-the-art", "e-mail") overflowed the
|
|
9
|
+
frame as one unbreakable run instead of wrapping after a hyphen.
|
|
10
|
+
|
|
11
|
+
## Solution
|
|
12
|
+
|
|
13
|
+
`LineBreaker#break_after?` treats hyphen-minus (U+002D), Unicode
|
|
14
|
+
hyphen (U+2010), non-breaking hyphen (U+2011), and soft hyphen
|
|
15
|
+
(U+00AD) as break-after points: the hyphen stays on the first line
|
|
16
|
+
and the remainder wraps. Soft hyphens break without adding a
|
|
17
|
+
visible hyphen. Dictionary-based hyphenation (Knuth-Liang) remains
|
|
18
|
+
out of scope — the Hyphenation* PSR attributes stay parsed but
|
|
19
|
+
unused.
|
|
20
|
+
|
|
21
|
+
## Files
|
|
22
|
+
|
|
23
|
+
- `lib/idml/text_engine/line_breaker.rb`
|
|
24
|
+
- `spec/idml/text_engine/text_engine_spec.rb`
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# TODO PDF 140: CapHeight / XHeight / EmboxHeight first baselines
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-27
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
FirstBaselineOffset honored AscentOffset and FixedHeight (TODO 128)
|
|
8
|
+
but approximated CapHeight, XHeight, and EmboxHeight as leading —
|
|
9
|
+
text sat noticeably lower than InDesign places it.
|
|
10
|
+
|
|
11
|
+
## Solution
|
|
12
|
+
|
|
13
|
+
`TextFrameRenderer#baseline_target` now handles all five modes:
|
|
14
|
+
|
|
15
|
+
- **CapHeight** — the font's cap-height metric when the provider
|
|
16
|
+
fills it; pdfrb 0.7.1 never does, so the fallback is the standard
|
|
17
|
+
0.72 em proportion (Arial and Helvetica both sit within 1%).
|
|
18
|
+
- **XHeight** — 0.52 em (no x-height metric exists in pdfrb).
|
|
19
|
+
- **EmboxHeight** — the em box itself: the first paragraph's point
|
|
20
|
+
size.
|
|
21
|
+
|
|
22
|
+
`PdfrbFontMetrics#cap_height` exposes the metric for when pdfrb
|
|
23
|
+
starts populating it.
|
|
24
|
+
|
|
25
|
+
## Files
|
|
26
|
+
|
|
27
|
+
- `lib/idml/render/renderers/text_frame_renderer.rb`
|
|
28
|
+
- `lib/idml/text_engine/pdfrb_font_metrics.rb`
|
|
29
|
+
- `spec/idml/render/first_baseline_spec.rb`
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# TODO PDF 141: 100+ page performance regression guard
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-27
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
No test guarded against per-page complexity blowups — a regression
|
|
8
|
+
to O(document) work per page would only surface as slow user
|
|
9
|
+
renders.
|
|
10
|
+
|
|
11
|
+
## Solution
|
|
12
|
+
|
|
13
|
+
`perf_hundred_pages_spec.rb` builds a synthetic 120-spread package
|
|
14
|
+
(one page + one text frame each, all sharing a wordy story) and
|
|
15
|
+
renders it end-to-end through `Idml::Render.render`. Asserts all
|
|
16
|
+
120 PDF pages emit and the render completes under a generous 60s
|
|
17
|
+
ceiling — sized to trip only on pathological blowups, not machine
|
|
18
|
+
noise. Baseline on this branch: ~2s for 120 pages.
|
|
19
|
+
|
|
20
|
+
## Files
|
|
21
|
+
|
|
22
|
+
- `spec/idml/render/perf_hundred_pages_spec.rb`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TODO PDF 61: Known limitations and future work
|
|
2
2
|
|
|
3
|
-
## Status: DOCUMENTED — refreshed 2026-08-
|
|
3
|
+
## Status: DOCUMENTED — refreshed 2026-08-27
|
|
4
4
|
|
|
5
5
|
Previously listed limitations (font embedding, tagged PDF, per-run
|
|
6
6
|
styling, dead code) were resolved by TODOs 52/53/55/67/76 long ago;
|
|
@@ -9,8 +9,9 @@ this list reflects the current state.
|
|
|
9
9
|
## Known limitations
|
|
10
10
|
|
|
11
11
|
### Text layout
|
|
12
|
-
- **Hyphenation
|
|
13
|
-
parsed but unused);
|
|
12
|
+
- **Hyphenation**: dictionary-based hyphenation is not implemented
|
|
13
|
+
(Hyphenation* attributes are parsed but unused); compound words
|
|
14
|
+
wrap after explicit hyphens (TODO 139).
|
|
14
15
|
- **Justification**: word/letter spacing caps apply (TODO 119),
|
|
15
16
|
MaximumGlyphScaling stretches (TODO 129) and MinimumGlyphScaling
|
|
16
17
|
compresses overlong lines (TODO 135); ToBinding / RTL binding
|
|
@@ -18,8 +19,9 @@ this list reflects the current state.
|
|
|
18
19
|
- **Keep options**: KeepAllLinesTogether (TODO 123), KeepWithNext
|
|
19
20
|
(TODO 127), and the KeepFirstLines / KeepLastLines windows
|
|
20
21
|
(TODO 133, line-count approximation) are honored.
|
|
21
|
-
- **First baseline**:
|
|
22
|
-
|
|
22
|
+
- **First baseline**: all five modes honor their metrics (TODOs
|
|
23
|
+
128/140); CapHeight / XHeight use standard font proportions
|
|
24
|
+
(0.72 / 0.52 em) until a metrics provider fills them.
|
|
23
25
|
- **StartParagraph** breaks act at frame/column granularity — no
|
|
24
26
|
odd/even page parity.
|
|
25
27
|
- **Text wrap**: BoundingBoxTextWrap, Contour, and Inverse modes
|
|
@@ -54,4 +56,6 @@ this list reflects the current state.
|
|
|
54
56
|
|
|
55
57
|
- Hyphenation dictionary integration
|
|
56
58
|
- Shape-mode text wrap contours (side-aware)
|
|
57
|
-
-
|
|
59
|
+
- Named mojikumi sets (詳細 etc.)
|
|
60
|
+
- ToBinding / RTL binding alignment
|
|
61
|
+
- Vertical-mode keep options and run-grouped Latin rotation
|
|
@@ -629,12 +629,19 @@ module Idml
|
|
|
629
629
|
# the first line's baseline sits. The layout's default is
|
|
630
630
|
# leading-based; AscentOffset shifts by (ascent − leading)
|
|
631
631
|
# and FixedHeight by (MinimumFirstBaselineOffset − leading).
|
|
632
|
-
# CapHeight
|
|
633
|
-
#
|
|
632
|
+
# CapHeight uses the font's cap-height metric (leading
|
|
633
|
+
# fallback when the font reports none); XHeight approximates
|
|
634
|
+
# as 70% of cap height; EmboxHeight uses the em box (the
|
|
635
|
+
# first paragraph's point size). Returns the extra shift to
|
|
636
|
+
# subtract from the cursor.
|
|
637
|
+
FIRST_BASELINE_MODES = %w[
|
|
638
|
+
AscentOffset FixedHeight CapHeight XHeight EmboxHeight
|
|
639
|
+
].freeze
|
|
640
|
+
|
|
634
641
|
def self.first_baseline_offset(context, state, font)
|
|
635
642
|
pref = context.item&.text_frame_preference&.first
|
|
636
643
|
mode = pref&.first_baseline_offset
|
|
637
|
-
return 0.0 unless
|
|
644
|
+
return 0.0 unless FIRST_BASELINE_MODES.include?(mode)
|
|
638
645
|
|
|
639
646
|
baseline_target(pref, mode, state, font) -
|
|
640
647
|
first_paragraph_leading(state)
|
|
@@ -643,21 +650,52 @@ module Idml
|
|
|
643
650
|
|
|
644
651
|
# Distance from the top inset to the first baseline under
|
|
645
652
|
# the given mode.
|
|
653
|
+
# pdfrb 0.7.1 never fills the cap_height metric, so
|
|
654
|
+
# CapHeight / XHeight fall back to standard font
|
|
655
|
+
# proportions (cap ≈ 0.72 em, x ≈ 0.52 em — Arial and
|
|
656
|
+
# Helvetica both sit within 1% of these).
|
|
657
|
+
CAP_HEIGHT_RATIO = 0.72
|
|
658
|
+
X_HEIGHT_RATIO = 0.52
|
|
659
|
+
|
|
646
660
|
def self.baseline_target(pref, mode, state, font)
|
|
647
|
-
|
|
648
|
-
|
|
661
|
+
leading = first_paragraph_leading(state)
|
|
662
|
+
case mode
|
|
663
|
+
when "AscentOffset"
|
|
664
|
+
ratio_scaled(font.ascent, font) * leading
|
|
665
|
+
when "CapHeight"
|
|
666
|
+
cap_ratio(font) * leading
|
|
667
|
+
when "XHeight"
|
|
668
|
+
X_HEIGHT_RATIO * leading
|
|
669
|
+
when "EmboxHeight"
|
|
670
|
+
first_paragraph_size(state)
|
|
649
671
|
else
|
|
650
|
-
pref.minimum_first_baseline_offset ||
|
|
651
|
-
first_paragraph_leading(state)
|
|
672
|
+
pref.minimum_first_baseline_offset || leading
|
|
652
673
|
end
|
|
653
674
|
end
|
|
654
675
|
private_class_method :baseline_target
|
|
655
676
|
|
|
656
|
-
def self.
|
|
657
|
-
|
|
658
|
-
|
|
677
|
+
def self.cap_ratio(font)
|
|
678
|
+
cap = font.cap_height
|
|
679
|
+
return CAP_HEIGHT_RATIO unless cap
|
|
680
|
+
|
|
681
|
+
ratio_scaled(cap, font)
|
|
682
|
+
end
|
|
683
|
+
private_class_method :cap_ratio
|
|
684
|
+
|
|
685
|
+
def self.ratio_scaled(metric, font)
|
|
686
|
+
upem = font.units_per_em
|
|
687
|
+
return 1.0 if upem.zero?
|
|
688
|
+
|
|
689
|
+
metric / upem
|
|
690
|
+
end
|
|
691
|
+
private_class_method :ratio_scaled
|
|
692
|
+
|
|
693
|
+
def self.first_paragraph_size(state)
|
|
694
|
+
paragraph = state.current_paragraph || state.paragraphs.first
|
|
695
|
+
first_run = paragraph&.runs&.first
|
|
696
|
+
first_run&.point_size || DEFAULT_SIZE
|
|
659
697
|
end
|
|
660
|
-
private_class_method :
|
|
698
|
+
private_class_method :first_paragraph_size
|
|
661
699
|
|
|
662
700
|
def self.first_paragraph_leading(state)
|
|
663
701
|
paragraph = state.current_paragraph || state.paragraphs.first
|
|
@@ -6,10 +6,16 @@ module Idml
|
|
|
6
6
|
Line = Struct.new(:glyphs, :width, :x_offset)
|
|
7
7
|
|
|
8
8
|
# Greedy word-wrap line breaker. Accumulates glyphs until
|
|
9
|
-
# exceeding the frame width, then breaks at the last
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# exceeding the frame width, then breaks at the last break
|
|
10
|
+
# opportunity: a space, or a hyphen (compound words wrap after
|
|
11
|
+
# the hyphen, which stays on the first line). CJK runs get a
|
|
12
|
+
# kinsoku shori post-pass (no line starts or ends with a
|
|
13
|
+
# forbidden character).
|
|
12
14
|
class LineBreaker
|
|
15
|
+
# Break-after opportunities besides spaces: hyphen-minus,
|
|
16
|
+
# the Unicode hyphens, and the soft hyphen (which breaks
|
|
17
|
+
# without adding a visible hyphen).
|
|
18
|
+
HYPHEN_CODEPOINTS = [0x2D, 0x2010, 0x2011, 0x00AD].freeze
|
|
13
19
|
def self.break(glyphs:, frame_width:)
|
|
14
20
|
lines = new(frame_width).break(glyphs)
|
|
15
21
|
return lines unless cjk_run?(glyphs)
|
|
@@ -44,26 +50,26 @@ module Idml
|
|
|
44
50
|
lines = []
|
|
45
51
|
current = []
|
|
46
52
|
current_width = 0
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
break_idx = -1
|
|
54
|
+
width_at_break = 0
|
|
49
55
|
|
|
50
56
|
glyphs.each_with_index do |glyph, _idx|
|
|
51
57
|
current << glyph
|
|
52
58
|
current_width += glyph.width
|
|
53
59
|
|
|
54
|
-
if glyph
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
if break_after?(glyph)
|
|
61
|
+
break_idx = current.length - 1
|
|
62
|
+
width_at_break = current_width
|
|
57
63
|
end
|
|
58
64
|
|
|
59
65
|
next unless current_width > @frame_width && current.length > 1
|
|
60
66
|
|
|
61
|
-
if
|
|
62
|
-
line_glyphs = current[0..
|
|
63
|
-
lines << Line.new(line_glyphs,
|
|
64
|
-
current = current[(
|
|
67
|
+
if break_idx >= 0
|
|
68
|
+
line_glyphs = current[0..break_idx]
|
|
69
|
+
lines << Line.new(line_glyphs, width_at_break, 0)
|
|
70
|
+
current = current[(break_idx + 1)..]
|
|
65
71
|
current_width = current.sum(&:width)
|
|
66
|
-
|
|
72
|
+
break_idx = -1
|
|
67
73
|
elsif cjk_break?(current)
|
|
68
74
|
lines << Line.new(current[0..-2], current_width - glyph.width, 0)
|
|
69
75
|
current = [glyph]
|
|
@@ -78,6 +84,11 @@ module Idml
|
|
|
78
84
|
lines << Line.new(current, current_width, 0) if current.any?
|
|
79
85
|
lines
|
|
80
86
|
end
|
|
87
|
+
|
|
88
|
+
def break_after?(glyph)
|
|
89
|
+
glyph.is_space || HYPHEN_CODEPOINTS.include?(glyph.codepoint)
|
|
90
|
+
end
|
|
91
|
+
private :break_after?
|
|
81
92
|
end
|
|
82
93
|
end
|
|
83
94
|
end
|
|
@@ -37,6 +37,12 @@ module Idml
|
|
|
37
37
|
0
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Cap height in font units; nil when the font tables don't
|
|
41
|
+
# report one (some pdfrb-loaded fonts).
|
|
42
|
+
def cap_height
|
|
43
|
+
metrics_data[:cap_height]
|
|
44
|
+
end
|
|
45
|
+
|
|
40
46
|
# Returns raw advance width in font units (not scaled by size).
|
|
41
47
|
def glyph_width(codepoint)
|
|
42
48
|
cp = codepoint.is_a?(String) ? codepoint.each_codepoint.first : codepoint
|
data/lib/idml/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: idml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.10.
|
|
4
|
+
version: 0.10.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|
|
@@ -201,7 +201,10 @@ files:
|
|
|
201
201
|
- TODO.pdf/136-diagonal-cell-strokes.md
|
|
202
202
|
- TODO.pdf/137-simple-render-footnote-text.md
|
|
203
203
|
- TODO.pdf/138-text-wrap-sides.md
|
|
204
|
+
- TODO.pdf/139-hyphen-break-opportunities.md
|
|
204
205
|
- TODO.pdf/14-page-item-element-models.md
|
|
206
|
+
- TODO.pdf/140-cap-xheight-first-baselines.md
|
|
207
|
+
- TODO.pdf/141-perf-hundred-pages.md
|
|
205
208
|
- TODO.pdf/15-wire-spread-children.md
|
|
206
209
|
- TODO.pdf/16-typed-model-pipeline.md
|
|
207
210
|
- TODO.pdf/17-shape-rendering.md
|