idml 0.10.0 → 0.10.2

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: 13770ed8937eeed41a40bd223bb8caadc833da870858d691caa0e1806058c286
4
- data.tar.gz: dec0fe8f24c1aaa8258ecf18506560b1e0793bd7d4ebe56bf361dc1a69ff736a
3
+ metadata.gz: 40262a683342f0a9308e33ab2cce4a8bcf6b803288d6a0bc3ece3d2b376c6761
4
+ data.tar.gz: 78cb5495d1d9b72d5e850c6d25c8cd594622d6484476ea61a1ff239df41e9e99
5
5
  SHA512:
6
- metadata.gz: '0394efac9722c854cd4e8c2771d0ec494aaa8d3b50ca6ea471bbe8f5f44daf62a1f1bd4d12031ee9d59b0d7b6c5f65f5378db661d5a110d056734958369a870e'
7
- data.tar.gz: 07d3a9492ea5a41819f4fe0353176671307ad7263feedeb909bd08f8284eb79b84df0fef0057db1d089a3a02ff7d255459ea2e232e5c929d3d1ffccc70685e81
6
+ metadata.gz: b10228f8870d90e1a66f8fb9b621a26951db4a71e155a3f28165c22a3e9c44b53e7dbc17916ec4b5db0f49987403e4e645e5f0872a79cb0fe591881ff6f9caca
7
+ data.tar.gz: d4904d3ef2a6d0b1c4dfcd497597042fcedee1b5c3c1643ff892ac867e7b4c3f0881419a906527e5f1705a64292c8c5c5e06ae8af774b8ce725f81c8bcf9ff92
data/README.adoc CHANGED
@@ -1,4 +1,64 @@
1
- = Ruby parser for Adobe IDML
1
+ = idml
2
2
 
3
- IDML is the InDesign Markup Language from Adobe used by InDesign.
3
+ A pure-Ruby toolkit for Adobe InDesign IDML files: parse, validate,
4
+ round-trip, compose, and render to PDF.
4
5
 
6
+ == What it does
7
+
8
+ * **Parse & round-trip** — every IDML part is a typed
9
+ `Lutaml::Model` class generated from Adobe's RelaxNG schemas
10
+ (`reference-docs/schemas/`): byte-faithful re-serialization with
11
+ `ZIP_STORED` mimetype ordering preserved.
12
+ * **Validate** — RelaxNG validation against the committed InDesign
13
+ 2026 (DOMVersion 21.5) schemas.
14
+ * **Compose** — `insert_idml`, `add_page_from_idml`, `prefix`,
15
+ `import_xml`/`export_xml` (ported from SimpleIDML).
16
+ * **Render to PDF** — a full pure-Ruby layout + paint pipeline
17
+ (pdfrb): multi-frame threaded stories, justification with
18
+ word/letter/glyph limits, footnotes (bottom-of-frame with
19
+ separator rules), anchored objects, tables (row/column banding,
20
+ per-side cell strokes, merged cells), Bézier shape contours,
21
+ gradients, text wrap (bounding-box, contour, and inverse modes),
22
+ paragraph shading/borders/rules, drop caps, bullets & numbering,
23
+ CJK layout (kinsoku shori, ruby, vertical writing with
24
+ tate-chu-yoko and rotated Latin, script spacing, line-end
25
+ punctuation compression), tagged PDF, hyperlinks, bookmarks,
26
+ PDF/A, and font subsetting.
27
+
28
+ == Install
29
+
30
+ ....
31
+ gem install idml
32
+ ....
33
+
34
+ `.indd` files are a proprietary binary format — export them to
35
+ `.idml` from InDesign first (File → Export), or use InDesign Server
36
+ for headless automation.
37
+
38
+ == Quick start
39
+
40
+ ....
41
+ require "idml"
42
+
43
+ # Round-trip a package
44
+ pkg = Idml::Package.new("doc.idml")
45
+ spec = pkg.spreads.first
46
+
47
+ # Render to PDF
48
+ Idml.render(package: pkg, to: "doc.pdf")
49
+ ....
50
+
51
+ == Requirements
52
+
53
+ * Ruby >= 3.3 (tested through 4.0 on macOS/Linux/Windows)
54
+
55
+ == Reference material
56
+
57
+ The `reference-docs/` tree carries Adobe's IDML specification, the
58
+ Plug-in SDK's idmltools (behavioral ground truth), and the
59
+ generated RelaxNG schemas — the attribute authority for every
60
+ model class.
61
+
62
+ == License
63
+
64
+ BSD-2-Clause.
@@ -1,6 +1,8 @@
1
1
  # TODO PDF 114: Text wrap around shapes (TextWrapPreference)
2
2
 
3
- ## Status: DONE for BoundingBox mode `Render::TextWrapResolver` computes
3
+ ## Status: COMPLETE for BoundingBoxTextWrap + Contour (Contour and
4
+ the schema-spelling enum fix landed 2026-08-23 as TODO 130; Inverse
5
+ mode remains open) — `Render::TextWrapResolver` computes
4
6
  wrap contours from page items declaring TextWrapPreference with
5
7
  TextWrapMode != "None". SpreadRenderer builds the resolver and passes
6
8
  it via RenderContext. TextFrameRenderer's `text_wrap_overlap` queries
@@ -24,7 +24,8 @@ kinsoku shori, ruby annotations).
24
24
  and vertical ruby (TODO 122), CJK/Latin automatic script spacing
25
25
  (TODO 125, a mojikumi subset wired into Shaper). The remaining
26
26
  stretch goal is full class-based mojikumi (per-pair punctuation
27
- compression tables). See `lib/idml/text_engine/cjk_layout.rb` and
27
+ compression tables); line-end punctuation compression landed
28
+ 2026-08-23 as TODO 132. See `lib/idml/text_engine/cjk_layout.rb` and
28
29
  `lib/idml/text_engine/vertical_text_layout.rb`.
29
30
 
30
31
  ## Progress 2026-08-17
@@ -0,0 +1,38 @@
1
+ # TODO PDF 130: Shape-mode (Contour) text wrap
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-23
4
+
5
+ ## Problem
6
+
7
+ Two gaps in the text wrap (TODO 114):
8
+
9
+ 1. The TextWrapMode dispatch compared against "BoundingBox" — the
10
+ schema enum (TextWrapModes_EnumValue in datatype.rnc) spells it
11
+ "BoundingBoxTextWrap", so REAL documents never matched any mode
12
+ and text never wrapped.
13
+ 2. Contour mode (text following the item's actual shape) was not
14
+ implemented at all.
15
+
16
+ ## What was done
17
+
18
+ - Enum fix: BoundingBoxTextWrap accepted (legacy "BoundingBox"
19
+ spelling kept for the synthetic fixtures); JumpObjectTextWrap /
20
+ NextColumnTextWrap approximate as the bounding box (the per-run
21
+ width reduction already pushes text past the object);
22
+ Contour gets real contour following.
23
+ - `Render::WrapContour`: flattens the item's PathGeometry (Bézier
24
+ segments sampled at 8 steps, ItemTransform applied, y flipped)
25
+ into PDF-space polygons — one per subpath, so compound paths
26
+ with holes work via even-odd ray casting at the text band's
27
+ midline. The overlap intervals expand by the TextWrapOffset
28
+ bounds (`Properties > TextWrapOffset`, now modeled on
29
+ TextWrapPreference via TypedValue) and clip to the frame.
30
+ - `TextWrapResolver` dispatches per mode; overlap_width handles
31
+ both the box rect and polygon shapes.
32
+
33
+ ## Known limitations
34
+
35
+ - Band coverage samples the midline (sub-half-line-height notches
36
+ are missed); offsets expand intervals uniformly rather than
37
+ offsetting the polygon geometrically; Inverse mode is not
38
+ implemented (falls back to no wrap).
@@ -0,0 +1,19 @@
1
+ # TODO PDF 131: Inverse text wrap
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-23
4
+
5
+ ## What was done
6
+
7
+ `TextWrapPreference Inverse="true"` flips the wrap region: text
8
+ may only flow INSIDE the wrap shape (the avoid-region becomes
9
+ everything outside it within the frame). Works for both the
10
+ bounding-box contour and the Contour-mode polygon shape (TODO
11
+ 130); the inversion lives in one place — TextWrapResolver's
12
+ overlap dispatch — and reduces by (frame width − inside width),
13
+ so bands outside the shape block text entirely.
14
+
15
+ ## Known limitations
16
+
17
+ - Side-aware narrowing is not implemented: the reduction shrinks
18
+ the wrap width from the right regardless of which side the
19
+ shape occupies.
@@ -0,0 +1,22 @@
1
+ # TODO PDF 132: Line-end punctuation compression (mojikumi subset 2)
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-23
4
+
5
+ ## What was done
6
+
7
+ `CjkLayout.apply_line_end_compression`: a CJK line whose final
8
+ glyph is full-width closing punctuation (。、」』)]}ー etc.) renders
9
+ that glyph at half advance (行末約物半角詰め), tightening the line
10
+ as InDesign's default mojikumi does. Applied in LineBreaker's CJK
11
+ post-pass after kinsoku shori, so horizontal and vertical layout
12
+ both benefit.
13
+
14
+ Also fixed a latent bug the specs caught: kinsoku's return value
15
+ (new dup'd line array) was discarded when composing the post-pass
16
+ chain — kinsoku adjustments silently vanished once a second
17
+ post-pass followed it.
18
+
19
+ ## Remaining from TODO 13
20
+
21
+ Per-pair punctuation compression tables (full class-based
22
+ mojikumi).
@@ -22,12 +22,14 @@ this list reflects the current state.
22
22
  (TODO 128); CapHeight / XHeight approximate as leading.
23
23
  - **StartParagraph** breaks act at frame/column granularity — no
24
24
  odd/even page parity.
25
- - **Text wrap** (TODO 114): BoundingBox mode only; Shape contour
26
- following and Inverse mode are not implemented.
25
+ - **Text wrap**: BoundingBoxTextWrap, Contour, and Inverse modes
26
+ render (TODOs 130-131); JumpObject/NextColumn approximate as
27
+ the box; side-aware narrowing is not implemented.
27
28
 
28
29
  ### CJK
29
- - **Mojikumi**: CJK/Latin auto script spacing is applied (TODO
30
- 125); full class-based punctuation compression tables are not.
30
+ - **Mojikumi**: CJK/Latin auto script spacing (TODO 125) and
31
+ line-end punctuation compression (TODO 132) are applied; full
32
+ class-based compression tables are not.
31
33
  - **Vertical mode**: Latin rotation is per glyph (not run-grouped);
32
34
  ruby placement is beside-column; keep options do not apply.
33
35
 
@@ -16,6 +16,7 @@ module Idml
16
16
  attribute :rule_below_color, Idml::Elements::TypedValue
17
17
  attribute :paragraph_shading_color, Idml::Elements::TypedValue
18
18
  attribute :paragraph_border_color, Idml::Elements::TypedValue
19
+ attribute :text_wrap_offset, Idml::Elements::TypedValue
19
20
  attribute :paragraph_border_gap_color, Idml::Elements::TypedValue
20
21
 
21
22
  xml do
@@ -26,6 +27,7 @@ module Idml
26
27
  map_element "RuleBelowColor", to: :rule_below_color
27
28
  map_element "ParagraphShadingColor", to: :paragraph_shading_color
28
29
  map_element "ParagraphBorderColor", to: :paragraph_border_color
30
+ map_element "TextWrapOffset", to: :text_wrap_offset
29
31
  map_element "ParagraphBorderGapColor", to: :paragraph_border_gap_color
30
32
  end
31
33
 
@@ -11,6 +11,7 @@ module Idml
11
11
  attribute :apply_to_master_page_only, :boolean
12
12
  attribute :text_wrap_side, :string
13
13
  attribute :text_wrap_mode, :string
14
+ attribute :properties, Idml::Elements::Properties, collection: true
14
15
 
15
16
  xml do
16
17
  root "TextWrapPreference"
@@ -18,6 +19,7 @@ module Idml
18
19
  map_attribute "ApplyToMasterPageOnly", to: :apply_to_master_page_only
19
20
  map_attribute "TextWrapSide", to: :text_wrap_side
20
21
  map_attribute "TextWrapMode", to: :text_wrap_mode
22
+ map_element "Properties", to: :properties
21
23
  end
22
24
  end
23
25
  end
@@ -14,7 +14,7 @@ module Idml
14
14
  class TextWrapResolver
15
15
  # A wrap contour: a PDF-coordinate rectangle the text avoids.
16
16
  Contour = Struct.new(
17
- :x, :y, :width, :height,
17
+ :x, :y, :width, :height, :inverse,
18
18
  keyword_init: true
19
19
  )
20
20
 
@@ -40,20 +40,45 @@ module Idml
40
40
  # Returns the total horizontal overlap of all contours with a
41
41
  # text line at the given y range within the given x range.
42
42
  # Sum, not max — multiple overlapping contours compound.
43
- # Returns 0.0 when there's no overlap.
43
+ # Returns 0.0 when there's no overlap. Inverse contours
44
+ # reduce by the frame width minus their own width (text may
45
+ # only flow inside).
44
46
  def overlap_width(line_y, line_height, frame_x, frame_right)
45
47
  @contours.sum do |contour|
46
- overlap = line_overlap(contour, line_y, line_height,
47
- frame_x, frame_right)
48
- overlap[:width]
48
+ width = if contour.is_a?(WrapContour::Shape)
49
+ WrapContour.overlap_width(contour, line_y,
50
+ line_height, frame_x,
51
+ frame_right)
52
+ else
53
+ line_overlap(contour, line_y, line_height,
54
+ frame_x, frame_right)[:width]
55
+ end
56
+ next width unless contour.inverse
57
+
58
+ (frame_right - frame_x) - width
49
59
  end
50
60
  end
51
61
 
62
+ # The wrap shape for an item: a bounding-box rectangle
63
+ # (BoundingBoxTextWrap — the legacy "BoundingBox" spelling is
64
+ # also accepted from early synthetic fixtures) or a flattened
65
+ # PathGeometry polygon (Contour mode). JumpObject /
66
+ # NextColumn modes approximate as the bounding box (the
67
+ # per-run width reduction already pushes text past the
68
+ # object).
52
69
  def self.contour_for(item, page_height)
53
70
  pref = wrap_preference(item)
54
71
  return nil unless pref
55
- return nil unless bounding_box_mode?(pref)
56
72
 
73
+ if contour_mode?(pref)
74
+ WrapContour.shape(item, pref, page_height)
75
+ else
76
+ box_contour(item, page_height)
77
+ end
78
+ end
79
+ private_class_method :contour_for
80
+
81
+ def self.box_contour(item, page_height)
57
82
  bounds = item.geometric_bounds
58
83
  return nil unless bounds
59
84
 
@@ -64,7 +89,7 @@ module Idml
64
89
  width: rect[:width], height: rect[:height]
65
90
  )
66
91
  end
67
- private_class_method :contour_for
92
+ private_class_method :box_contour
68
93
 
69
94
  # Page item types that can declare TextWrapPreference per the
70
95
  # Spread schema. Used to guard the attribute read — querying
@@ -95,10 +120,10 @@ module Idml
95
120
  end
96
121
  private_class_method :wrappable?
97
122
 
98
- def self.bounding_box_mode?(pref)
99
- pref.text_wrap_mode == "BoundingBox"
123
+ def self.contour_mode?(pref)
124
+ pref.text_wrap_mode == "Contour"
100
125
  end
101
- private_class_method :bounding_box_mode?
126
+ private_class_method :contour_mode?
102
127
 
103
128
  def line_overlap(contour, line_y, line_height, frame_x, frame_right)
104
129
  return { width: 0.0 } unless y_overlap?(contour, line_y, line_height)
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Shape-mode text wrap: builds a PDF-space polygon from a page
6
+ # item's PathGeometry (Bézier segments flattened at a fixed
7
+ # resolution) and measures the horizontal overlap of a text
8
+ # line's band with the polygon, expanded by the wrap offsets.
9
+ # Band coverage uses even-odd ray casting at the band's
10
+ # midline, so compound paths with holes wrap correctly.
11
+ module WrapContour
12
+ FLATTEN_STEPS = 8
13
+
14
+ # A wrap shape: flattened polygons (one per subpath) plus the
15
+ # TextWrapOffset bounds. `inverse` flips the wrap region:
16
+ # text may only flow INSIDE the shape.
17
+ Shape = Struct.new(:polygons, :offset, :inverse,
18
+ keyword_init: true)
19
+
20
+ # Builds the wrap shape for an item's PathGeometry and wrap
21
+ # preference. Returns nil when the item has no geometry.
22
+ def self.shape(item, pref, page_height)
23
+ polygons = polygons_for(item, page_height)
24
+ return nil if polygons.empty?
25
+
26
+ Shape.new(polygons: polygons, offset: wrap_offset(pref),
27
+ inverse: pref.inverse)
28
+ end
29
+
30
+ def self.polygons_for(item, page_height)
31
+ transform = Geometry.parse_transform(item.item_transform)
32
+ paths = item.properties.flat_map do |props|
33
+ props.path_geometry.flat_map(&:geometry_path_type)
34
+ end
35
+ paths.filter_map do |path|
36
+ points = flatten_path(path, transform, page_height)
37
+ points.length >= 3 ? points : nil
38
+ end
39
+ end
40
+ private_class_method :polygons_for
41
+
42
+ # Flattens one subpath's Bézier segments into line segments
43
+ # in PDF space (ItemTransform applied, y flipped).
44
+ def self.flatten_path(path, transform, page_height)
45
+ anchors = path.points
46
+ return [] if anchors.length < 2
47
+
48
+ open = path.path_open == true
49
+ segments = open ? anchors.length - 1 : anchors.length
50
+ points = [pdf_point(anchors.first, transform, page_height)]
51
+ segments.times do |index|
52
+ from = anchors[index]
53
+ to = anchors[(index + 1) % anchors.length]
54
+ append_segment(points, from, to, transform, page_height)
55
+ end
56
+ points
57
+ end
58
+ private_class_method :flatten_path
59
+
60
+ def self.append_segment(points, from, to, transform, page_height)
61
+ c1 = pair_or_nil(from, :right_direction, transform, page_height)
62
+ c2 = pair_or_nil(to, :left_direction, transform, page_height)
63
+ start = points.last
64
+ if c1.nil? && c2.nil?
65
+ points << pdf_point(to, transform, page_height)
66
+ return
67
+ end
68
+
69
+ c1 ||= start
70
+ c2 ||= pdf_point(to, transform, page_height)
71
+ (1..FLATTEN_STEPS).each do |step|
72
+ t = step.to_f / FLATTEN_STEPS
73
+ points << bezier_point(start, c1, c2,
74
+ pdf_point(to, transform, page_height), t)
75
+ end
76
+ end
77
+ private_class_method :append_segment
78
+
79
+ def self.bezier_point(p0, p1, p2, p3, t)
80
+ u = 1 - t
81
+ w0 = u * u * u
82
+ w1 = 3 * u * u * t
83
+ w2 = 3 * u * t * t
84
+ w3 = t * t * t
85
+ [
86
+ (w0 * p0[0]) + (w1 * p1[0]) + (w2 * p2[0]) + (w3 * p3[0]),
87
+ (w0 * p0[1]) + (w1 * p1[1]) + (w2 * p2[1]) + (w3 * p3[1]),
88
+ ]
89
+ end
90
+ private_class_method :bezier_point
91
+
92
+ def self.pair_or_nil(point, direction_attr, transform, page_height)
93
+ raw = point.public_send(direction_attr)
94
+ raw = nil if raw && raw.strip.empty?
95
+ raw && pdf_pair(raw, transform, page_height)
96
+ end
97
+ private_class_method :pair_or_nil
98
+
99
+ def self.pdf_point(point, transform, page_height)
100
+ pdf_pair(point.anchor, transform, page_height)
101
+ end
102
+ private_class_method :pdf_point
103
+
104
+ def self.pdf_pair(raw, transform, page_height)
105
+ x, y = raw.to_s.split(/\s+/).map(&:to_f)
106
+ tx, ty = Geometry.apply_transform(transform, x, y)
107
+ [tx, page_height - ty]
108
+ end
109
+ private_class_method :pdf_pair
110
+
111
+ # The wrap offsets from Properties > TextWrapOffset
112
+ # ("top left bottom right"); zero when absent.
113
+ def self.wrap_offset(pref)
114
+ raw = pref.properties.first&.text_wrap_offset&.value
115
+ parts = raw.to_s.split(/\s+/).map(&:to_f)
116
+ return [0.0, 0.0] if parts.length < 4
117
+
118
+ [parts[1], parts[3]] # left, right
119
+ end
120
+ private_class_method :wrap_offset
121
+
122
+ # Total horizontal overlap of the text line's band with the
123
+ # shape, expanded by the offsets and clipped to the frame.
124
+ # The caller (TextWrapResolver) owns inverse flipping.
125
+ def self.overlap_width(shape, line_y, line_height, frame_x,
126
+ frame_right)
127
+ mid_y = line_y + (line_height / 2.0)
128
+ width = shape.polygons.sum do |polygon|
129
+ polygon_overlap(polygon, mid_y, shape.offset)
130
+ end
131
+ clip_width(width, frame_x, frame_right)
132
+ end
133
+
134
+ # Even-odd intervals at mid_y expanded by [left, right]
135
+ # offsets, summed (separate subpaths sum; the common case is
136
+ # one polygon).
137
+ def self.polygon_overlap(polygon, mid_y, offset)
138
+ crossings = edge_crossings(ring(polygon), mid_y).sort
139
+ return 0.0 if crossings.length < 2
140
+
141
+ left, right = offset
142
+ crossings.each_slice(2).sum do |entry_x, exit_x|
143
+ (exit_x + right) - (entry_x - left)
144
+ end
145
+ end
146
+ private_class_method :polygon_overlap
147
+
148
+ # Closes the polygon ring (each_cons alone misses the
149
+ # last→first edge).
150
+ def self.ring(polygon)
151
+ polygon + [polygon.first]
152
+ end
153
+ private_class_method :ring
154
+
155
+ def self.edge_crossings(points, mid_y)
156
+ points.each_cons(2).filter_map do |(x1, y1), (x2, y2)|
157
+ next unless crosses_midline?(y1, y2, mid_y)
158
+
159
+ t = (mid_y - y1) / (y2 - y1)
160
+ x1 + (t * (x2 - x1))
161
+ end
162
+ end
163
+ private_class_method :edge_crossings
164
+
165
+ def self.crosses_midline?(y1, y2, mid_y)
166
+ (y1 <= mid_y && y2 > mid_y) || (y2 <= mid_y && y1 > mid_y)
167
+ end
168
+ private_class_method :crosses_midline?
169
+
170
+ def self.clip_width(width, frame_x, frame_right)
171
+ [width, frame_right - frame_x].min
172
+ end
173
+ private_class_method :clip_width
174
+ end
175
+ end
176
+ end
data/lib/idml/render.rb CHANGED
@@ -25,6 +25,7 @@ module Idml
25
25
  autoload :Footnote, "#{__dir__}/render/footnote"
26
26
  autoload :ShapePaint, "#{__dir__}/render/shape_paint"
27
27
  autoload :Contour, "#{__dir__}/render/contour"
28
+ autoload :WrapContour, "#{__dir__}/render/wrap_contour"
28
29
  autoload :Placement, "#{__dir__}/render/placement"
29
30
  autoload :ImageCollector, "#{__dir__}/render/image_collector"
30
31
  autoload :StructureTracker, "#{__dir__}/render/structure_tracker"
@@ -39,6 +39,41 @@ module Idml
39
39
  (0x61..0x7A).cover?(codepoint)
40
40
  end
41
41
 
42
+ # Mojikumi subset 2: line-end punctuation compression —
43
+ # full-width closing punctuation occupying a line's final
44
+ # position renders at half advance (行末約物半角詰め),
45
+ # tightening the line as InDesign's default mojikumi does.
46
+ TRAILING_COMPRESSION_CODEPOINTS = [
47
+ 0x3001, # 、 ideographic comma
48
+ 0x3002, # 。 ideographic full stop
49
+ 0x3009, # 〉
50
+ 0x300B, # 》
51
+ 0x300D, # 」
52
+ 0x300F, # 』
53
+ 0x3011, # 】
54
+ 0x3015, # 〕
55
+ 0xFF09, # )fullwidth right paren
56
+ 0xFF3D, # ]
57
+ 0xFF5D, # }
58
+ 0x30FC, # ー prolonged sound mark
59
+ ].freeze
60
+
61
+ def apply_line_end_compression(lines)
62
+ lines.each do |line|
63
+ last = line.glyphs.last
64
+ next unless last
65
+ next unless compressible?(last.codepoint)
66
+
67
+ last.width /= 2.0
68
+ line.width = line.glyphs.sum(&:width)
69
+ end
70
+ lines
71
+ end
72
+
73
+ def compressible?(codepoint)
74
+ TRAILING_COMPRESSION_CODEPOINTS.include?(codepoint)
75
+ end
76
+
42
77
  CJK_RANGES = [
43
78
  0x3000..0x303F, # CJK Symbols and Punctuation
44
79
  0x3040..0x309F, # Hiragana
@@ -14,7 +14,8 @@ module Idml
14
14
  lines = new(frame_width).break(glyphs)
15
15
  return lines unless cjk_run?(glyphs)
16
16
 
17
- CjkLayout.apply_kinsoku(lines)
17
+ lines = CjkLayout.apply_kinsoku(lines)
18
+ CjkLayout.apply_line_end_compression(lines)
18
19
  end
19
20
 
20
21
  def self.cjk_run?(glyphs)
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.0"
4
+ VERSION = "0.10.2"
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.10.0
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -189,6 +189,9 @@ files:
189
189
  - TODO.pdf/128-first-baseline-offset.md
190
190
  - TODO.pdf/129-justification-glyph-scaling.md
191
191
  - TODO.pdf/13-cjk-text-layout.md
192
+ - TODO.pdf/130-contour-text-wrap.md
193
+ - TODO.pdf/131-inverse-wrap.md
194
+ - TODO.pdf/132-line-end-compression.md
192
195
  - TODO.pdf/14-page-item-element-models.md
193
196
  - TODO.pdf/15-wire-spread-children.md
194
197
  - TODO.pdf/16-typed-model-pipeline.md
@@ -497,6 +500,7 @@ files:
497
500
  - lib/idml/render/style_lookup.rb
498
501
  - lib/idml/render/style_resolver.rb
499
502
  - lib/idml/render/text_wrap_resolver.rb
503
+ - lib/idml/render/wrap_contour.rb
500
504
  - lib/idml/text_engine.rb
501
505
  - lib/idml/text_engine/cjk_layout.rb
502
506
  - lib/idml/text_engine/justifier.rb