idml 0.9.7 → 0.9.9

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: 0d49d9954838f3326eab4e65e2915eab9cd18a526ce7c4dd58669e6dacb8910e
4
- data.tar.gz: 56713c3d26980c24c6baf8d080543cd2471aef5dea4ca83c33be82f73fe2e83e
3
+ metadata.gz: fd4031c47fca99598c64ed757ac446d3e1f2d2870603eb31af338132dae01288
4
+ data.tar.gz: 520a5ded7c74cfc50d0f893921486930c295b113f7e0d3c7af4dfb576186498c
5
5
  SHA512:
6
- metadata.gz: d52298b420e04ae5c4b8292e01d951eb23d03d4e01357f463111477e842aea9dd89f6e162a88ad05dd50fa8b721faf6522f891eb66e3405dd6207769f7a9bfe8
7
- data.tar.gz: 1d7f89c274a8aac75868064826567901fbc0503ead631953fc7b24eaa282e0f1d8870d2e33f1dc6e81c834b4e3c00fc4b2b465b1a12cc349484b0e653628e63f
6
+ metadata.gz: bad5a41e54b9e173241e08ba9e7006e6f621230ff782e89e4821d6b26749ee11b30fe8a38dae5185ee1cb41e4497ec20f32267584852cac43c9a37e41f488990
7
+ data.tar.gz: a62b908ae372636a3dec1722265ee44b24ab847510b348dd0029a3d04b388e41c6ad708f53c11ba21d7077817cfa831d700626ee73cc0380e46bfcb02e7ea58d
@@ -0,0 +1,22 @@
1
+ # TODO PDF 125: CJK/Latin automatic script spacing (mojikumi subset)
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-20
4
+
5
+ ## What was done
6
+
7
+ - `TextEngine::CjkLayout.apply_script_spacing` widens the leading
8
+ glyph by an eighth em at every CJK ↔ ASCII alphanumeric boundary
9
+ — InDesign's default inter-script auto spacing (a mojikumi
10
+ behavior).
11
+ - Wired inside `TextEngine::Shaper#shape`, so every shaping path
12
+ (horizontal frame text, footnotes, ruby, vertical columns)
13
+ applies it uniformly — one integration point, no per-caller
14
+ changes.
15
+ - Punctuation neighbors are ignored (only letters/digits form
16
+ boundaries).
17
+
18
+ ## Remaining from TODO 13
19
+
20
+ Class-based mojikumi (per-pair punctuation compression tables,
21
+ line-end punctuation hanging beyond half-em) remains a documented
22
+ stretch goal.
@@ -0,0 +1,22 @@
1
+ # TODO PDF 126: Package#part memoization
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-20
4
+
5
+ ## Problem
6
+
7
+ `Package#part(name)` re-read and re-parsed the zip entry on every
8
+ call. Rendering resolves `story_by_id` per frame plus preferences
9
+ (Footnote.option), styles, and graphics repeatedly — each lookup
10
+ decompressed and re-parsed the same XML.
11
+
12
+ ## What was done
13
+
14
+ Parsed parts are memoized in a per-`Package` hash
15
+ (`parse_part` remains the single read+parse path; `each_part` /
16
+ `read_part` stay uncached raw reads for round-trip byte fidelity).
17
+
18
+ ## Acceptance criteria
19
+
20
+ - [x] Repeated `part(name)` calls return the same object.
21
+ - [x] Different parts do not share objects.
22
+ - [x] Whole-package round-trip specs unchanged (raw reads).
@@ -0,0 +1,20 @@
1
+ # TODO PDF 127: KeepWithNext
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-20
4
+
5
+ ## What was done
6
+
7
+ - `StyleResolver::Paragraph` carries `keep_with_next` from the PSR.
8
+ - A paragraph with KeepWithNext defers to the next frame when the
9
+ FOLLOWING paragraph is forced there (StartParagraph break) or
10
+ when the follower's first line would not fit after this
11
+ paragraph (pre-measured via TextEngine::Measurement plus the
12
+ follower's leading). Combined with TODO 123's KeepAllLinesTogether,
13
+ both keep flavors share one deferral predicate in
14
+ `consume_paragraphs`.
15
+
16
+ ## Known limitations
17
+
18
+ - The numeric strength of KeepWithNext (1–5, lines to keep) is
19
+ treated as boolean.
20
+ - Not applied in vertical writing mode.
@@ -20,9 +20,11 @@ kinsoku shori, ruby annotations).
20
20
  frame width (previously they overflowed as one long line) — this
21
21
  also fixes horizontal CJK wrapping.
22
22
 
23
- Remaining stretch goals: tate-chu-yoko, mojikumi, Latin rotation in
24
- vertical mode, vertical ruby placement. See
25
- `lib/idml/text_engine/cjk_layout.rb` and
23
+ 2026-08-20 completions: tate-chu-yoko (TODO 124), Latin rotation
24
+ and vertical ruby (TODO 122), CJK/Latin automatic script spacing
25
+ (TODO 125, a mojikumi subset wired into Shaper). The remaining
26
+ stretch goal is full class-based mojikumi (per-pair punctuation
27
+ compression tables). See `lib/idml/text_engine/cjk_layout.rb` and
26
28
  `lib/idml/text_engine/vertical_text_layout.rb`.
27
29
 
28
30
  ## Progress 2026-08-17
@@ -1,35 +1,51 @@
1
1
  # TODO PDF 61: Known limitations and future work
2
2
 
3
- ## Status: DOCUMENTED
3
+ ## Status: DOCUMENTED — refreshed 2026-08-20
4
4
 
5
- ## Known limitations
6
-
7
- 1. **Font embedding**: pdfrb 0.3.0 registers fonts by name but does
8
- not embed FontFile2 binary data in the PDF. Text renders correctly
9
- on systems with the font installed but may not render on other
10
- systems. Requires pdfrb font subsetting support (TODO 52).
5
+ Previously listed limitations (font embedding, tagged PDF, per-run
6
+ styling, dead code) were resolved by TODOs 52/53/55/67/76 long ago;
7
+ this list reflects the current state.
11
8
 
12
- 2. **Image fixture tests**: The fixture IDML references an external
13
- JPEG at a macOS-specific path. Tests that check image embedding
14
- are skipped on CI. A self-contained fixture with an embedded image
15
- would fix this.
16
-
17
- 3. **Text engine integration**: TextFrameRenderer uses the text engine
18
- for line breaking when FontMetrics is available (Helvetica as .ttf).
19
- On macOS, Helvetica is a .ttc collection, so the fallback simple
20
- renderer is used (no line breaking). Per-run styling (different
21
- fonts/sizes in one frame) is simplified to use the first run's style.
9
+ ## Known limitations
22
10
 
23
- 4. **Dead code**: Old modules (PdfWriter, FontEmbedder, Color, Path,
24
- Text) are marked DEPRECATED but cannot be deleted per the project's
25
- "never delete source files" policy. They are not used by any active
26
- code path.
11
+ ### Text layout
12
+ - **Hyphenation** is not implemented (Hyphenation* attributes are
13
+ parsed but unused); a hyphenation dictionary would be required.
14
+ - **Justification**: word/letter spacing caps apply (TODO 119);
15
+ glyph scaling (Min/Max/DesiredGlyphScaling) is not applied;
16
+ ToBinding / RTL binding alignment maps to left.
17
+ - **Keep options**: whole-paragraph KeepAllLinesTogether and
18
+ KeepWithNext(first-line) are honored; partial keep windows
19
+ (KeepFirstLines / KeepLastLines) are not.
20
+ - **StartParagraph** breaks act at frame/column granularity — no
21
+ odd/even page parity.
22
+ - **Text wrap** (TODO 114): BoundingBox mode only; Shape contour
23
+ following and Inverse mode are not implemented.
24
+
25
+ ### CJK
26
+ - **Mojikumi**: CJK/Latin auto script spacing is applied (TODO
27
+ 125); full class-based punctuation compression tables are not.
28
+ - **Vertical mode**: Latin rotation is per glyph (not run-grouped);
29
+ ruby placement is beside-column; keep options do not apply.
30
+
31
+ ### Structural features
32
+ - **Endnotes** (TODO 117): reference markers render; endnote TEXT
33
+ rendering is gated on a real fixture (the EndnoteTextRange
34
+ reference chain is opaque without one).
35
+ - **Footnotes** number per story; overflow balancing (NoSplitting)
36
+ is not modeled; the simple-render fallback shows markers without
37
+ footnote text.
38
+ - **Anchored objects** render at stored geometry; AnchorType
39
+ text-reflow is not simulated.
40
+ - **Tables** do not span frames (no repeated header rows); diagonal
41
+ cell strokes are modeled but not drawn.
42
+ - **Inline anchored objects / footnotes inside table cells** are
43
+ not handled.
27
44
 
28
45
  ## Future enhancements
29
46
 
30
- - Font subsetting (TODO 52) — requires pdfrb enhancement
31
- - Tagged PDF / PDF/UA (TODO 53) — requires pdfrb StructTreeRoot
32
- - Dead code removal (TODO 51) — when policy allows
33
- - Self-contained test fixture with embedded image
34
- - Per-run text styling with cursor tracking
47
+ - Hyphenation dictionary integration
48
+ - Shape-mode text wrap contours
49
+ - Frame-spanning tables with header repeats
50
+ - Class-based mojikumi tables
35
51
  - Performance benchmark for 100+ page documents
data/lib/idml/package.rb CHANGED
@@ -134,13 +134,23 @@ module Idml
134
134
  # Returns a typed part instance for `name` when a class is registered
135
135
  # for that file pattern (e.g. Designmap for designmap.xml). Falls
136
136
  # back to Parts::Raw — a lossless XML wrapper — for unmodeled parts.
137
+ # Parsed parts are memoized: rendering resolves stories,
138
+ # preferences, and styles repeatedly (per frame), and each
139
+ # un-memoized read would re-decompress and re-parse the zip
140
+ # entry.
137
141
  def part(name)
142
+ @part_cache ||= {}
143
+ @part_cache[name] ||= parse_part(name)
144
+ end
145
+
146
+ def parse_part(name)
138
147
  xml = read_part(name)
139
148
  klass = Idml::Parts.class_for(name)
140
149
  return Idml::Parts::Raw.from_xml(xml) unless klass
141
150
 
142
151
  klass.from_xml(xml)
143
152
  end
153
+ private :parse_part
144
154
 
145
155
  def each_part
146
156
  return enum_for(:each_part) unless block_given?
@@ -629,11 +629,13 @@ module Idml
629
629
  cursor_y, bottom_limit, char_cursor,
630
630
  footnotes)
631
631
  pending = 0
632
- state.paragraphs.each do |paragraph|
632
+ state.paragraphs.each_with_index do |paragraph, index|
633
633
  break if cursor_y < bottom_limit
634
- break if paragraph_deferred?(paragraph, layout_frame, font,
635
- cursor_y, bottom_limit,
636
- pending.positive?)
634
+
635
+ next_paragraph = state.paragraphs[index + 1]
636
+ break if paragraph_deferred?(paragraph, next_paragraph,
637
+ layout_frame, font, cursor_y,
638
+ bottom_limit, pending.positive?)
637
639
 
638
640
  remaining_runs, cursor_y, char_cursor = render_runs_for_paragraph(
639
641
  canvas, paragraph, paragraph.runs, context,
@@ -969,11 +971,25 @@ module Idml
969
971
  # frame: a StartParagraph forced break, or KeepAllLines-
970
972
  # Together with insufficient remaining space (never for the
971
973
  # frame's first paragraph, so progress is always made).
972
- def self.paragraph_deferred?(paragraph, layout_frame, font,
973
- cursor_y, bottom_limit, placed_any)
974
+ def self.paragraph_deferred?(paragraph, next_paragraph,
975
+ layout_frame, font, cursor_y,
976
+ bottom_limit, placed_any)
974
977
  return true if paragraph_break?(paragraph) && placed_any
975
- return false unless paragraph.keep_all_lines_together
976
978
  return false unless placed_any
979
+ return true if keep_with_next_break?(paragraph, next_paragraph,
980
+ layout_frame, font,
981
+ cursor_y, bottom_limit)
982
+
983
+ keep_all_lines_break?(paragraph, layout_frame, font,
984
+ cursor_y, bottom_limit)
985
+ end
986
+ private_class_method :paragraph_deferred?
987
+
988
+ # KeepAllLinesTogether: defer when the paragraph cannot
989
+ # fully fit the remaining space.
990
+ def self.keep_all_lines_break?(paragraph, layout_frame, font,
991
+ cursor_y, bottom_limit)
992
+ return false unless paragraph.keep_all_lines_together
977
993
 
978
994
  wrap_width = TextEngine::VerticalLayout.wrap_width(
979
995
  layout_frame, paragraph.right_indent || 0
@@ -983,7 +999,40 @@ module Idml
983
999
  )
984
1000
  (cursor_y - height) < bottom_limit
985
1001
  end
986
- private_class_method :paragraph_deferred?
1002
+ private_class_method :keep_all_lines_break?
1003
+
1004
+ # KeepWithNext: this paragraph defers when the next
1005
+ # paragraph is forced to the next frame, or when the next
1006
+ # paragraph's first line would not fit after this one.
1007
+ def self.keep_with_next_break?(paragraph, next_paragraph,
1008
+ layout_frame, font, cursor_y,
1009
+ bottom_limit)
1010
+ return false unless keepable_pair?(paragraph, next_paragraph)
1011
+ return true if paragraph_break?(next_paragraph)
1012
+
1013
+ wrap_width = TextEngine::VerticalLayout.wrap_width(
1014
+ layout_frame, paragraph.right_indent || 0
1015
+ )
1016
+ height = TextEngine::Measurement.paragraph_height(
1017
+ paragraph, font, wrap_width
1018
+ )
1019
+ (cursor_y - height - follower_leading(next_paragraph)) <
1020
+ bottom_limit
1021
+ end
1022
+ private_class_method :keep_with_next_break?
1023
+
1024
+ def self.keepable_pair?(paragraph, next_paragraph)
1025
+ !paragraph.keep_with_next.nil? && !next_paragraph.nil?
1026
+ end
1027
+ private_class_method :keepable_pair?
1028
+
1029
+ def self.follower_leading(next_paragraph)
1030
+ size = next_paragraph.runs.first&.point_size || DEFAULT_SIZE
1031
+ TextEngine::VerticalLayout.leading_for(
1032
+ next_paragraph.auto_leading, size
1033
+ )
1034
+ end
1035
+ private_class_method :follower_leading
987
1036
 
988
1037
  # True when the paragraph requests a forced break to the
989
1038
  # next frame/column (StartParagraph). All break flavors act
@@ -72,8 +72,11 @@ module Idml
72
72
  # NextColumn / NextFrame / NextOddPage / NextEvenPage).
73
73
  :start_paragraph,
74
74
  # Widow/orphan control: KeepAllLinesTogether pushes the whole
75
- # paragraph to the next frame when it cannot fully fit.
75
+ # paragraph to the next frame when it cannot fully fit;
76
+ # KeepWithNext binds this paragraph to the next paragraph's
77
+ # first line.
76
78
  :keep_all_lines_together,
79
+ :keep_with_next,
77
80
  :bullets_and_numbering_list_type,
78
81
  :bullet_character_value,
79
82
  :bullets_text_after,
@@ -207,6 +210,8 @@ module Idml
207
210
  keep_all_lines_together: resolve_attr(
208
211
  style_lookup, psr, :keep_all_lines_together
209
212
  ),
213
+ keep_with_next: resolve_attr(style_lookup, psr,
214
+ :keep_with_next),
210
215
  drop_cap_lines: resolve_attr(style_lookup, psr, :drop_cap_lines),
211
216
  drop_cap_characters: resolve_attr(style_lookup, psr,
212
217
  :drop_cap_characters),
@@ -7,6 +7,38 @@ module Idml
7
7
  module CjkLayout
8
8
  module_function
9
9
 
10
+ # Mojikumi subset: automatic inter-script spacing — an eighth
11
+ # em is inserted between adjacent CJK and ASCII alphanumeric
12
+ # glyphs (widenning the leading glyph of each pair), matching
13
+ # InDesign's default CJK/Latin auto spacing.
14
+ SCRIPT_SPACING_EM = 0.125
15
+
16
+ def apply_script_spacing(glyphs, size)
17
+ spacing = size * SCRIPT_SPACING_EM
18
+ (0...(glyphs.length - 1)).each do |index|
19
+ leading = glyphs[index]
20
+ following = glyphs[index + 1]
21
+ next unless script_boundary?(leading.codepoint,
22
+ following.codepoint)
23
+
24
+ leading.width += spacing
25
+ end
26
+ glyphs
27
+ end
28
+
29
+ # True when exactly one of the pair is CJK and the other is
30
+ # an ASCII letter or digit.
31
+ def script_boundary?(left, right)
32
+ (cjk?(left) && ascii_alnum?(right)) ||
33
+ (ascii_alnum?(left) && cjk?(right))
34
+ end
35
+
36
+ def ascii_alnum?(codepoint)
37
+ (0x30..0x39).cover?(codepoint) ||
38
+ (0x41..0x5A).cover?(codepoint) ||
39
+ (0x61..0x7A).cover?(codepoint)
40
+ end
41
+
10
42
  CJK_RANGES = [
11
43
  0x3000..0x303F, # CJK Symbols and Punctuation
12
44
  0x3040..0x309F, # Hiragana
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.9.7"
4
+ VERSION = "0.9.9"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.7
4
+ version: 0.9.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -183,6 +183,9 @@ files:
183
183
  - TODO.pdf/122-vertical-latin-ruby.md
184
184
  - TODO.pdf/123-keep-options.md
185
185
  - TODO.pdf/124-tatechuyoko-vertical.md
186
+ - TODO.pdf/125-script-spacing.md
187
+ - TODO.pdf/126-part-cache.md
188
+ - TODO.pdf/127-keep-with-next.md
186
189
  - TODO.pdf/13-cjk-text-layout.md
187
190
  - TODO.pdf/14-page-item-element-models.md
188
191
  - TODO.pdf/15-wire-spread-children.md