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 +4 -4
- data/TODO.pdf/125-script-spacing.md +22 -0
- data/TODO.pdf/126-part-cache.md +22 -0
- data/TODO.pdf/127-keep-with-next.md +20 -0
- data/TODO.pdf/13-cjk-text-layout.md +5 -3
- data/TODO.pdf/61-known-limitations.md +42 -26
- data/lib/idml/package.rb +10 -0
- data/lib/idml/render/renderers/text_frame_renderer.rb +57 -8
- data/lib/idml/render/style_resolver.rb +6 -1
- data/lib/idml/text_engine/cjk_layout.rb +32 -0
- data/lib/idml/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd4031c47fca99598c64ed757ac446d3e1f2d2870603eb31af338132dae01288
|
|
4
|
+
data.tar.gz: 520a5ded7c74cfc50d0f893921486930c295b113f7e0d3c7af4dfb576186498c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
24
|
-
vertical
|
|
25
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
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.
|
|
632
|
+
state.paragraphs.each_with_index do |paragraph, index|
|
|
633
633
|
break if cursor_y < bottom_limit
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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,
|
|
973
|
-
|
|
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 :
|
|
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
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.
|
|
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
|