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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f60b5f9020e44d5131e9f50e2ddfb673f9caca65bbdee8428b4670baaf96108f
4
- data.tar.gz: 1f19b80511a14cea5fab0b76404be809a8f28e680deb91ee1b4caf9353a1b756
3
+ metadata.gz: 943028d5bf17ca306e3cce0d23c4b042ddaa9473295f2fa7f4af43009a7423a0
4
+ data.tar.gz: b9c3c1e5421ffa4de44396dbc94124434dda470c3f10fabd5ddd81146c3991c1
5
5
  SHA512:
6
- metadata.gz: 9bea97bee388cae4ab403c3bea6cae233c3a378c569c0c678620bdb59a3ed5534ab9aa560866fad103d8a724351e396c72e9a533671fd44189719958f83d811e
7
- data.tar.gz: 151917cdee9265410c788c85fb6f28d45d07ffc37cc3c4e3fc14b4578f2be57adee09393c284a53a883f1f1dbe105920ecd745a264f0311cc38c0c8f92d94a05
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-26
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** is not implemented (Hyphenation* attributes are
13
- parsed but unused); a hyphenation dictionary would be required.
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**: AscentOffset and FixedHeight are honored
22
- (TODO 128); CapHeight / XHeight approximate as leading.
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
- - Performance benchmark for 100+ page documents
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 / XHeight / EmboxHeight approximate as leading.
633
- # Returns the extra shift to subtract from the cursor.
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 %w[AscentOffset FixedHeight].include?(mode)
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
- if mode == "AscentOffset"
648
- ascent_scaled(font) * first_paragraph_leading(state)
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.ascent_scaled(font)
657
- upem = font.units_per_em.zero? ? 1 : font.units_per_em
658
- font.ascent / upem
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 :ascent_scaled
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 space.
10
- # CJK runs get a kinsoku shori post-pass (no line starts or
11
- # ends with a forbidden character).
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
- last_space_idx = -1
48
- width_at_space = 0
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.is_space
55
- last_space_idx = current.length - 1
56
- width_at_space = current_width
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 last_space_idx >= 0
62
- line_glyphs = current[0..last_space_idx]
63
- lines << Line.new(line_glyphs, width_at_space, 0)
64
- current = current[(last_space_idx + 1)..]
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
- last_space_idx = -1
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Idml
4
- VERSION = "0.10.6"
4
+ VERSION = "0.10.7"
5
5
  end
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.6
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-26 00:00:00.000000000 Z
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