idml 0.10.8 → 0.10.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: e05a75aaa533dcd8f478d6b4ab635d15cf6c64ac85bebbd42fe3b136ed0c6783
4
- data.tar.gz: 3a478cdb4b23ac26ceb9914d5e3740286ef6fda60c80605a01dc6f2a38086b7a
3
+ metadata.gz: c3ebdf83496770ecda7b37cc6f6cdc2388874785ccad23ac57aa396f76aef03b
4
+ data.tar.gz: d7304a209e09d4de3ee6d9f7556d18053f838ee3fef9a4f787f06693aa42ee32
5
5
  SHA512:
6
- metadata.gz: effb89c7a3869aa7c268027e4653ecc2072fc41402d53ff2ad8ca0405862b83076bd82637cc9f64a369cd3ceb2ca23b2e6e9ca7ba300de2fd9f073801c65c199
7
- data.tar.gz: 371206ab57061b8fa94733db6e489565eeb0cd588ffb6b0908a9a0c6e84fcec2d64745501dd8ec7b8147cb3044119c47d08f479e50cec26e939a35f5127ecf9e
6
+ metadata.gz: bd4dc75a85966131506edf7f3e01df91127fbf26525bb02edcf783f0c07580380be0d8bd38562c11a477602731e2efff8de78061580b471f1389f3bb87a07119
7
+ data.tar.gz: 74286965e709455a356789cc6f7e4c93f75d414a1132301ee8e347a85d114065bd1988921c180a522c646824d79f2049bdfacf7db609e9981b270d3a88b71fbb
@@ -0,0 +1,37 @@
1
+ # TODO PDF 145: Apply mojikumi aki to layout
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-28
4
+
5
+ ## Problem
6
+
7
+ Two gaps: (a) `apply_script_spacing` (TODO 125) was implemented
8
+ but never wired into the render pipeline — CJK/Latin auto spacing
9
+ never actually applied; (b) the named mojikumi sets modeled in
10
+ TODO 144 were parsed but unused.
11
+
12
+ ## Solution
13
+
14
+ - `CjkLayout.apply_script_spacing` now takes the document's aki
15
+ overrides: when the designmap declares a `MojikumiTable`, each
16
+ adjacent glyph pair whose classes match an
17
+ `OverrideMojikumiAki` entry (Target/Side class ×
18
+ SideIsAfterTarget) gets that entry's Desired spacing (em ×
19
+ point size). Unlisted pairs keep the default eighth-em at
20
+ CJK/Latin script boundaries.
21
+ - New `CjkLayout.mojikumi_class` classifies the seven InDesign
22
+ mojikumi classes (ideograph, opening bracket, closing bracket,
23
+ comma/period, middle dot, digit — incl. fullwidth, Latin —
24
+ incl. fullwidth).
25
+ - `TextFrameRenderer#layout_run` applies the spacing between
26
+ shaping and line breaking, using
27
+ `mojikumi_aki_overrides(context)` (first named set from the
28
+ designmap; empty when absent).
29
+
30
+ The result: documents declaring custom mojikumi now render their
31
+ declared inter-class spacing.
32
+
33
+ ## Files
34
+
35
+ - `lib/idml/text_engine/cjk_layout.rb`
36
+ - `lib/idml/render/renderers/text_frame_renderer.rb`
37
+ - `spec/idml/text_engine/cjk_layout_spec.rb`
@@ -0,0 +1,25 @@
1
+ # TODO PDF 146: NextColumn wrap behavior
2
+
3
+ ## Status: COMPLETE (approximation) — implemented 2026-08-28
4
+
5
+ ## Problem
6
+
7
+ NextColumnTextWrap approximated as the bounding box (text narrowed
8
+ beside the object). InDesign moves the text to the next COLUMN.
9
+
10
+ ## Solution
11
+
12
+ NextColumnTextWrap now joins JumpObjectTextWrap in the jump path:
13
+ the object blocks the full frame width and the run skips below
14
+ the object's bottom edge. In a single-column frame this matches
15
+ InDesign's visible outcome (the "next column" is below).
16
+
17
+ Remaining approximation (documented in TODO.pdf/61): in a
18
+ MULTI-column frame, true NextColumn jumps to the next column in
19
+ the same frame — that needs column-jump chain integration, which
20
+ stays a future enhancement.
21
+
22
+ ## Files
23
+
24
+ - `lib/idml/render/text_wrap_resolver.rb`
25
+ - `spec/idml/render/text_wrap_resolver_spec.rb`
@@ -0,0 +1,27 @@
1
+ # TODO PDF 147: Side-aware Contour shapes
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-28
4
+
5
+ ## Problem
6
+
7
+ TextWrapSide applied only to BoundingBoxTextWrap contours;
8
+ Contour-mode shapes narrowed by polygon overlap regardless of
9
+ side — text smeared over shapes at frame edges instead of
10
+ flowing around the chosen side.
11
+
12
+ ## Solution
13
+
14
+ `WrapContour::Shape` carries `side`; `wrap_adjustment`'s shape
15
+ branch dispatches through the shared interval-based side logic
16
+ using the polygon's bounding box: LeftSide keeps text left of the
17
+ shape, RightSide shifts lines past its right edge, LargestArea
18
+ picks the roomier side; BothSides (and spine variants) keep the
19
+ exact polygon-overlap narrowing. Inverse shapes keep their
20
+ flip-to-inside semantics. The side dispatch is now one shared
21
+ `interval_side_adjustment` for boxes and shapes alike.
22
+
23
+ ## Files
24
+
25
+ - `lib/idml/render/wrap_contour.rb`
26
+ - `lib/idml/render/text_wrap_resolver.rb`
27
+ - `spec/idml/render/text_wrap_resolver_spec.rb`
@@ -1,6 +1,6 @@
1
1
  # TODO PDF 61: Known limitations and future work
2
2
 
3
- ## Status: DOCUMENTED — refreshed 2026-08-27 (second refresh)
3
+ ## Status: DOCUMENTED — refreshed 2026-08-28
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;
@@ -25,18 +25,19 @@ this list reflects the current state.
25
25
  - **StartParagraph** breaks act at frame/column granularity — no
26
26
  odd/even page parity.
27
27
  - **Text wrap**: BoundingBoxTextWrap, Contour, and Inverse modes
28
- render (TODOs 130-131) with TextWrapSide side-awareness (TODO 138:
29
- LeftSide / RightSide / LargestArea pick the flow side; spine
30
- variants approximate as BothSides); JumpObject moves text below
31
- the object (TODO 143); NextColumn approximates as the box;
32
- Contour-mode shapes are not side-aware.
28
+ render (TODOs 130-131) with TextWrapSide side-awareness for both
29
+ box contours and Contour shapes (TODOs 138/147); JumpObject and
30
+ NextColumn move text below the object (TODOs 143/146) — NextColumn
31
+ in a MULTI-column frame should jump to the next column instead
32
+ (chain integration, future); spine variants approximate as
33
+ BothSides.
33
34
 
34
35
  ### CJK
35
- - **Mojikumi**: script spacing (TODO 125), line-end compression
36
- (TODO 132), and class-based pair compression are applied; named
37
- mojikumi sets are modeled (TODO 144: MojikumiTable +
38
- OverrideMojikumiAki parse and round-trip) but not yet APPLIED to
39
- layout the built-in class-based aki rules stand in.
36
+ - **Mojikumi**: script spacing (TODO 125, now actually wired),
37
+ line-end compression (TODO 132), class-based pair compression,
38
+ and named-set aki overrides (TODO 145: OverrideMojikumiAki
39
+ Desired applies per class pair) are applied; Minimum/Maximum
40
+ aki bounds and CompressionPriority are parsed but unused.
40
41
  - **Vertical mode**: Latin runs rotate as one group per segment
41
42
  (TODO 142); ruby placement is beside-column; keep options do
42
43
  not apply.
@@ -59,8 +60,7 @@ this list reflects the current state.
59
60
  ## Future enhancements
60
61
 
61
62
  - Hyphenation dictionary integration
62
- - Shape-mode text wrap contours (side-aware)
63
- - Apply named mojikumi sets to CJK layout (consume TODO 144's model)
64
- - NextColumn wrap via column-jump chaining
63
+ - NextColumn wrap via column-jump chaining (multi-column frames)
64
+ - Minimum/Maximum mojikumi aki bounds + CompressionPriority
65
65
  - ToBinding / RTL binding alignment
66
66
  - Vertical-mode keep options
@@ -1145,6 +1145,9 @@ module Idml
1145
1145
  glyphs = TextEngine::Shaper.shape(
1146
1146
  text: run.text, font: font, size: size,
1147
1147
  )
1148
+ glyphs = TextEngine::CjkLayout.apply_script_spacing(
1149
+ glyphs, size, mojikumi_aki_overrides(context)
1150
+ )
1148
1151
  lines = TextEngine::LineBreaker.break(
1149
1152
  glyphs: glyphs, frame_width: wrap_width,
1150
1153
  )
@@ -1169,6 +1172,19 @@ module Idml
1169
1172
  end
1170
1173
  private_class_method :layout_run
1171
1174
 
1175
+ # Aki overrides from the document's first named mojikumi
1176
+ # set, when the designmap declares one (TODO 145). Empty
1177
+ # otherwise — apply_script_spacing then uses the default
1178
+ # CJK/Latin script spacing.
1179
+ def self.mojikumi_aki_overrides(context)
1180
+ package = context.package
1181
+ return [] unless package&.has_part?("designmap.xml")
1182
+
1183
+ table = package.designmap.mojikumi_table.first
1184
+ table&.aki_overrides || []
1185
+ end
1186
+ private_class_method :mojikumi_aki_overrides
1187
+
1172
1188
  # True when the paragraph should move wholly to the next
1173
1189
  # frame: a StartParagraph forced break, or KeepAllLines-
1174
1190
  # Together with insufficient remaining space (never for the
@@ -63,10 +63,23 @@ module Idml
63
63
  width = WrapContour.overlap_width(contour, line_y,
64
64
  line_height, frame_x,
65
65
  frame_right)
66
- next unless contour.inverse
66
+ if contour.inverse
67
+ reduction = [reduction,
68
+ (frame_right - frame_x) - width].max
69
+ next
70
+ end
67
71
 
68
- reduction = [reduction,
69
- (frame_right - frame_x) - width].max
72
+ # Side-aware shapes (TODO 147): the side dispatch uses
73
+ # the polygon's bounding box; narrowing itself keeps the
74
+ # exact polygon overlap for BothSides.
75
+ bbox_left, _bottom, bbox_right, = WrapContour.bounding_box(
76
+ contour,
77
+ )
78
+ r, s = interval_side_adjustment(contour.side, bbox_left,
79
+ bbox_right, width,
80
+ frame_x, frame_right)
81
+ reduction = [reduction, r].max
82
+ shift = [shift, s].max
70
83
  end
71
84
  [reduction, shift]
72
85
  end
@@ -86,12 +99,14 @@ module Idml
86
99
 
87
100
  return [0.0, 0.0] if overlap.zero?
88
101
 
89
- # JumpObject: text never flows beside the object — the full
90
- # frame width is blocked and the renderer skips the run
91
- # below the object's bottom edge.
102
+ # JumpObject / NextColumn: text never flows beside the
103
+ # object — the full frame width is blocked and the renderer
104
+ # skips the run below the object's bottom edge.
92
105
  return [frame_right - frame_x, 0.0] if contour.jump
93
106
 
94
- side_adjustment(contour, overlap, frame_x, frame_right)
107
+ interval_side_adjustment(contour.side, contour.x,
108
+ contour.x + contour.width, overlap,
109
+ frame_x, frame_right)
95
110
  end
96
111
  private :box_adjustment
97
112
 
@@ -114,30 +129,35 @@ module Idml
114
129
  end
115
130
  private :box_contour?
116
131
 
117
- def side_adjustment(contour, overlap, frame_x, frame_right)
118
- case contour.side
132
+ # Side dispatch over an object's horizontal extent
133
+ # [obj_left, obj_right] (TODO 138/147 — shared by box
134
+ # contours and Contour shapes' bounding boxes): LeftSide
135
+ # keeps text left of the object (reduce to its left edge, no
136
+ # shift); RightSide moves text past its right edge (reduce +
137
+ # shift); LargestArea picks the roomier side; BothSides and
138
+ # the spine variants reduce by `overlap` only.
139
+ def interval_side_adjustment(side, obj_left, obj_right, overlap,
140
+ frame_x, frame_right)
141
+ case side
119
142
  when "LeftSide"
120
- [frame_right - contour.x, 0.0]
143
+ [frame_right - obj_left, 0.0]
121
144
  when "RightSide"
122
- shift = contour.x + contour.width - frame_x
145
+ shift = obj_right - frame_x
123
146
  [shift, shift]
124
147
  when "LargestArea"
125
- largest_side_adjustment(contour, frame_x, frame_right)
148
+ left_free = obj_left - frame_x
149
+ right_free = frame_right - obj_right
150
+ if left_free >= right_free
151
+ [frame_right - obj_left, 0.0]
152
+ else
153
+ shift = obj_right - frame_x
154
+ [shift, shift]
155
+ end
126
156
  else
127
157
  [overlap, 0.0]
128
158
  end
129
159
  end
130
- private :side_adjustment
131
-
132
- def largest_side_adjustment(contour, frame_x, frame_right)
133
- left_free = contour.x - frame_x
134
- right_free = frame_right - (contour.x + contour.width)
135
- return [frame_right - contour.x, 0.0] if left_free >= right_free
136
-
137
- shift = contour.x + contour.width - frame_x
138
- [shift, shift]
139
- end
140
- private :largest_side_adjustment
160
+ private :interval_side_adjustment
141
161
 
142
162
  # Returns the total horizontal overlap of all contours with a
143
163
  # text line at the given y range within the given x range.
@@ -164,10 +184,11 @@ module Idml
164
184
  # The wrap shape for an item: a bounding-box rectangle
165
185
  # (BoundingBoxTextWrap — the legacy "BoundingBox" spelling is
166
186
  # also accepted from early synthetic fixtures) or a flattened
167
- # PathGeometry polygon (Contour mode). JumpObject /
168
- # NextColumn modes approximate as the bounding box (the
169
- # per-run width reduction already pushes text past the
170
- # object).
187
+ # PathGeometry polygon (Contour mode). JumpObject moves text
188
+ # below the object; NextColumn approximates as the same
189
+ # jump-below in a single-column frame (true column jumping
190
+ # needs chain integration, documented in TODO.pdf/61).
191
+ JUMP_MODES = %w[JumpObjectTextWrap NextColumnTextWrap].freeze
171
192
  def self.contour_for(item, page_height)
172
193
  pref = wrap_preference(item)
173
194
  return nil unless pref
@@ -190,7 +211,7 @@ module Idml
190
211
  x: rect[:x], y: rect[:y],
191
212
  width: rect[:width], height: rect[:height],
192
213
  side: pref&.text_wrap_side,
193
- jump: pref&.text_wrap_mode == "JumpObjectTextWrap"
214
+ jump: JUMP_MODES.include?(pref&.text_wrap_mode)
194
215
  )
195
216
  end
196
217
  private_class_method :box_contour
@@ -13,8 +13,9 @@ module Idml
13
13
 
14
14
  # A wrap shape: flattened polygons (one per subpath) plus the
15
15
  # TextWrapOffset bounds. `inverse` flips the wrap region:
16
- # text may only flow INSIDE the shape.
17
- Shape = Struct.new(:polygons, :offset, :inverse,
16
+ # text may only flow INSIDE the shape. `side` carries
17
+ # TextWrapSide for side-aware narrowing (TODO 147).
18
+ Shape = Struct.new(:polygons, :offset, :inverse, :side,
18
19
  keyword_init: true)
19
20
 
20
21
  # Builds the wrap shape for an item's PathGeometry and wrap
@@ -24,7 +25,16 @@ module Idml
24
25
  return nil if polygons.empty?
25
26
 
26
27
  Shape.new(polygons: polygons, offset: wrap_offset(pref),
27
- inverse: pref.inverse)
28
+ inverse: pref.inverse, side: pref.text_wrap_side)
29
+ end
30
+
31
+ # The shape's bounding box: [left, bottom, right, top] in
32
+ # PDF coordinates, flattened across subpaths.
33
+ def self.bounding_box(shape)
34
+ points = shape.polygons.flatten(1)
35
+ xs = points.map(&:first)
36
+ ys = points.map(&:last)
37
+ [xs.min, ys.min, xs.max, ys.max]
28
38
  end
29
39
 
30
40
  def self.polygons_for(item, page_height)
@@ -10,22 +10,95 @@ module Idml
10
10
  # Mojikumi subset: automatic inter-script spacing — an eighth
11
11
  # em is inserted between adjacent CJK and ASCII alphanumeric
12
12
  # glyphs (widenning the leading glyph of each pair), matching
13
- # InDesign's default CJK/Latin auto spacing.
13
+ # InDesign's default CJK/Latin auto spacing. A named
14
+ # mojikumi set's OverrideMojikumiAki entries take precedence
15
+ # when given: the Desired value (em units) applies to each
16
+ # matching class pair (TODO 145).
14
17
  SCRIPT_SPACING_EM = 0.125
15
18
 
16
- def apply_script_spacing(glyphs, size)
17
- spacing = size * SCRIPT_SPACING_EM
19
+ # Mojikumi character classes (InDesign mojikumi table rows).
20
+ CLASS_IDEOGRAPH = 1
21
+ CLASS_OPENING = 2
22
+ CLASS_CLOSING = 3
23
+ CLASS_COMMA_PERIOD = 4
24
+ CLASS_MIDDLE_DOT = 5
25
+ CLASS_DIGIT = 6
26
+ CLASS_LATIN = 7
27
+
28
+ COMMA_PERIOD_CODEPOINTS = [
29
+ 0x3001, # 、 ideographic comma
30
+ 0x3002, # 。 ideographic full stop
31
+ ].freeze
32
+
33
+ CLOSING_CODEPOINTS = [
34
+ 0x3009, # 〉
35
+ 0x300B, # 》
36
+ 0x300D, # 」
37
+ 0x300F, # 』
38
+ 0x3011, # 】
39
+ 0x3015, # 〕
40
+ 0xFF09, # )fullwidth right paren
41
+ 0xFF3D, # ]
42
+ 0xFF5D, # }
43
+ ].freeze
44
+
45
+ def apply_script_spacing(glyphs, size, aki_overrides = [])
18
46
  (0...(glyphs.length - 1)).each do |index|
19
47
  leading = glyphs[index]
20
48
  following = glyphs[index + 1]
21
- next unless script_boundary?(leading.codepoint,
22
- following.codepoint)
49
+ em = aki_em(leading.codepoint, following.codepoint,
50
+ aki_overrides)
51
+ next unless em&.positive?
23
52
 
24
- leading.width += spacing
53
+ leading.width += em * size
25
54
  end
26
55
  glyphs
27
56
  end
28
57
 
58
+ # Spacing (em units) between an adjacent pair: the named
59
+ # set's Desired aki for a matching class-pair override, else
60
+ # the default eighth-em at CJK/Latin script boundaries, else
61
+ # none.
62
+ def aki_em(left, right, aki_overrides)
63
+ override = aki_overrides.find do |entry|
64
+ entry.side_is_after_target &&
65
+ entry.target_mojikumi_class == mojikumi_class(left) &&
66
+ entry.side_mojikumi_class == mojikumi_class(right)
67
+ end
68
+ return override.desired if override&.desired
69
+
70
+ return SCRIPT_SPACING_EM if script_boundary?(left, right)
71
+
72
+ nil
73
+ end
74
+
75
+ # The mojikumi class of a codepoint, or nil outside the
76
+ # CJK punctuation / script classes. Ideographs (the default
77
+ # CJK class) win only when no special class matches.
78
+ def mojikumi_class(codepoint)
79
+ return CLASS_OPENING if OPENING_CODEPOINTS.include?(codepoint)
80
+ return CLASS_COMMA_PERIOD if COMMA_PERIOD_CODEPOINTS.include?(codepoint)
81
+ return CLASS_CLOSING if CLOSING_CODEPOINTS.include?(codepoint)
82
+ return CLASS_MIDDLE_DOT if MIDDLE_CODEPOINTS.include?(codepoint)
83
+ return CLASS_DIGIT if digit_class?(codepoint)
84
+ return CLASS_LATIN if latin_class?(codepoint)
85
+ return CLASS_IDEOGRAPH if cjk?(codepoint)
86
+
87
+ nil
88
+ end
89
+
90
+ def digit_class?(codepoint)
91
+ (0x30..0x39).cover?(codepoint) ||
92
+ (0xFF10..0xFF19).cover?(codepoint)
93
+ end
94
+
95
+ def latin_class?(codepoint)
96
+ (0x41..0x5A).cover?(codepoint) ||
97
+ (0x61..0x7A).cover?(codepoint) ||
98
+ (0xFF21..0xFF3A).cover?(codepoint) ||
99
+ (0xFF41..0xFF5A).cover?(codepoint)
100
+ end
101
+
29
102
  # True when exactly one of the pair is CJK and the other is
30
103
  # an ASCII letter or digit.
31
104
  def script_boundary?(left, right)
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.8"
4
+ VERSION = "0.10.9"
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.8
4
+ version: 0.10.9
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-27 00:00:00.000000000 Z
11
+ date: 2026-08-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -208,6 +208,9 @@ files:
208
208
  - TODO.pdf/142-vertical-latin-run-groups.md
209
209
  - TODO.pdf/143-jump-object-below.md
210
210
  - TODO.pdf/144-mojikumi-table-model.md
211
+ - TODO.pdf/145-apply-mojikumi-aki.md
212
+ - TODO.pdf/146-next-column-wrap.md
213
+ - TODO.pdf/147-side-aware-shapes.md
211
214
  - TODO.pdf/15-wire-spread-children.md
212
215
  - TODO.pdf/16-typed-model-pipeline.md
213
216
  - TODO.pdf/17-shape-rendering.md