idml 0.8.8 → 0.9.0

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: ad2709aae7d11f8a91843141700ab7882b9b7a196480d7b6077fe4f0b6473d16
4
- data.tar.gz: b6fd8d8df9996dabd3fd4ffbfc9fb788930cb3ca04bb6f014ae8b3c9fce5b6af
3
+ metadata.gz: ff487a296f9cbd5b926b963c7acb7d4864d43060ffed5b1ec7438a75bac1446a
4
+ data.tar.gz: f4926fe3add1833a954628aace986839f101722deafde891783c54afdd509a58
5
5
  SHA512:
6
- metadata.gz: 20154b884fb1688985cdca3c7a3866e7c55be9059768d4a00eae4058bdd237f3606c964318d224bdd198853bc61a0a9167012b5dc6cc558f4a31537146a45a27
7
- data.tar.gz: 7075c7451518b208e6ce17da854fb9954048ed40825287dc87bf4b4543467a0677a277fdf49f124703c353dfc4faf1b770de936d69acf1b0958998887a1cb101
6
+ metadata.gz: 043ed38d8ebdcf1af8506e83d8548b6590d923f0b4b1ef01687dca5c7528bc5d9a16c79c67e06dc626abe70334afd72882aefac2f15f8253c71d56bc40224274
7
+ data.tar.gz: 0fefb80560e396290585e3677ca395abd1096511b78321c7ff05ff3add4b5bf0242a4c21ea3321244b7da5290651e369b376696400dc19429ecc547b1c534842
@@ -1,47 +1,44 @@
1
1
  # TODO PDF 101: Paragraph borders and rules (RuleAbove, RuleBelow, ParagraphBorder)
2
2
 
3
- ## Status: PARTIALRuleAbove and RuleBelow implemented via new
4
- `Render::ParagraphRules` helper; TextFrameRenderer emits them at
5
- paragraph top/bottom. Rule color comes from PSR#stroke_color (IDML's
6
- PSR doesn't have a dedicated RuleAboveColor attribute). RuleAboveWidth
7
- "Text" vs "Column" honored. Tint scaling + line weight + offset honored.
8
-
9
- ParagraphBorder and ParagraphShading remain open — they need the
10
- paragraph's bounding rect, which the renderer doesn't currently
11
- expose to ParagraphRules. Will add when a real document fixture
12
- exercises them.
13
-
14
- ## Problem
15
-
16
- `ParagraphStyleRange` carries:
17
-
18
- - `RuleAbove`, `RuleAboveLineWeight`, `RuleAboveTint`, `RuleAboveOffset`,
19
- `RuleAboveLeftIndent`, `RuleAboveRightIndent`, `RuleAboveWidth`
20
- - `RuleBelow`, `RuleBelowLineWeight`, `RuleBelowTint`, `RuleBelowOffset`,
21
- `RuleBelowLeftIndent`, `RuleBelowRightIndent`, `RuleBelowWidth`
22
- - `ParagraphBorderOn`, `ParagraphBorderTopLineWeight`,
23
- `ParagraphBorderLeftLineWeight`, etc.
24
- - `ParagraphShadingOn`, `ParagraphShadingTint`, `ParagraphShadingColor`
25
-
26
- None of these are rendered today. Real-world documents use RuleAbove
27
- heavily for headings ("section divider line"), and ParagraphShading
28
- for callouts.
29
-
30
- ## What needs to happen
31
-
32
- 1. After laying out a paragraph, emit RuleAbove as a horizontal line
33
- above the first line, with the rule's weight/tint/color/offset/indent.
34
- 2. Emit RuleBelow similarly below the last line.
35
- 3. Optionally emit ParagraphBorder as a rectangle around the paragraph.
36
- 4. Optionally emit ParagraphShading as a fill behind the paragraph.
3
+ ## Status: COMPLETE — implemented 2026-08-17
4
+
5
+ ## What was built
6
+
7
+ - RuleAbove / RuleBelow (earlier milestone): horizontal rules at
8
+ paragraph top/bottom with weight / tint / offset / indent /
9
+ TextWidth-ColumnWidth modes, via `Render::ParagraphRules`.
10
+ - Rule colors now resolve from `Properties > RuleAboveColor` /
11
+ `RuleBelowColor` (typed value elements) with StrokeColor fallback
12
+ modeled via `Elements::TypedValue` + Properties mapping, PSR/CSR
13
+ now carry `properties` collections.
14
+ - ParagraphShading: fill rect behind the paragraph block. Color from
15
+ `Properties > ParagraphShadingColor`, tint scaling, ParagraphShading
16
+ width mode (ColumnWidth default / TextWidth), four outward offsets.
17
+ `TextEngine::Measurement` pre-measures the paragraph's height so
18
+ the rect is known before the lines are laid out (clamped to the
19
+ frame's bottom limit when the paragraph splits).
20
+ - ParagraphBorder: per-side strokes around the same block rect, each
21
+ side with its own line weight and the border offsets/tint/color
22
+ (Properties > ParagraphBorderColor). Shading and border share one
23
+ extent so they always agree.
24
+
25
+ ## Known limitations
26
+
27
+ - Border corner radii and gap colors are parsed on the model but not
28
+ rendered; MergeConsecutiveParaBorders (merged rects across like-
29
+ bordered paragraphs) is not modeled.
30
+ - DisplayIfSplits: when a paragraph splits across frames, both
31
+ shading and border are drawn around the rendered portion only.
37
32
 
38
33
  ## Acceptance criteria
39
34
 
40
- - [ ] PSR with RuleAbove=true and RuleAboveLineWeight=2 draws a 2pt
41
- horizontal line above the paragraph.
42
- - [ ] PSR with RuleAboveOffset=4 leaves 4pt gap between rule and text.
43
- - [ ] PSR with ParagraphShadingOn=true and tint fills the paragraph rect.
35
+ - [x] PSR with RuleAbove=true and RuleAboveLineWeight=2 draws the
36
+ rule above the paragraph.
37
+ - [x] PSR with RuleAboveOffset=4 leaves a 4pt gap.
38
+ - [x] PSR with ParagraphShadingOn=true and tint fills the paragraph
39
+ rect.
40
+ - [x] ParagraphBorderOn draws per-side strokes.
44
41
 
45
42
  ## Dependencies
46
43
 
47
- - TODO 94 (VerticalLayout knows the paragraph's rect).
44
+ - TODO 94 (VerticalLayout knows the paragraph's rect) — satisfied.
@@ -1,12 +1,42 @@
1
1
  # TODO PDF 106: Page-item type coverage — Ellipse, Path, EPS, WMF, PICT, HtmlItem
2
2
 
3
- ## Status: PARTIAL Oval and Path elements modeled and rendered
4
- (registered in RENDERER_MAP, delegate to RectangleRenderer which draws
5
- the bounding box with fill/stroke). Documents containing Oval or Path
6
- page items no longer silently skip them. True elliptical rendering
7
- (bezier anchor/control points) and compound-path drawing are
8
- refinements. EPS / WMF / PICT / HtmlItem remain out of scope (foreign
9
- formats).
3
+ ## Status: COMPLETE (rendering scope) implemented 2026-08-17
4
+
5
+ ## What was built (2026-08-17 refinement: true contours)
6
+
7
+ - `Render::Contour` draws an item's PathGeometry as true Bézier
8
+ contours: PathPointType anchors, LeftDirection / RightDirection
9
+ control handles, ItemTransform applied per point, y flipped into
10
+ PDF space. One subpath per GeometryPathType (compound paths);
11
+ closed unless PathOpen="true"; degenerate handles fall back to the
12
+ anchor. OvalRenderer, PathRenderer, and PolygonRenderer all
13
+ delegate here — ovals render as real ellipses (rotation preserved
14
+ through the transform), polygons as their actual outlines, Paths
15
+ as compound contours. Bounding-box rectangle fallback when an
16
+ item has no PathGeometry but has geometric_bounds.
17
+ - `Render::ShapePaint` — the single owner of the IDML → PDF paint
18
+ mapping (solid fill, gradient shading fill, stroke styling, and
19
+ the PDF paint-op sequencing including fill_stroke on one path).
20
+ RectangleRenderer now delegates to it; the fill/stroke/gradient
21
+ logic it previously duplicated across shape renderers lives in
22
+ one place.
23
+ - Oval/Path modeled + registered in RENDERER_MAP (earlier
24
+ milestone); Polygon renders contours instead of its bounding box.
25
+
26
+ ## Out of scope (unchanged)
27
+
28
+ - EPS / WMF / PICT / HtmlItem / Movie / Sound / FormField — foreign
29
+ or interactive formats, not modeled. Image placement goes through
30
+ ImageCollector (separate, model-driven path).
31
+
32
+ ## Acceptance criteria
33
+
34
+ - [x] SpreadObject has `oval` + `path` collections (Ellipse is the
35
+ Oval element in the schema's naming).
36
+ - [x] OvalRenderer / PathRenderer registered in RENDERER_MAP.
37
+ - [x] True Bézier rendering from PathGeometry anchors + control
38
+ points (TODO 106 refinement complete).
39
+ - [x] Compound Path objects render with multiple sub-paths.
10
40
 
11
41
  ## Problem
12
42
 
@@ -298,6 +298,7 @@ module Idml
298
298
  attribute :content, Idml::Elements::Content, collection: true
299
299
  attribute :hyperlink_text_source, Idml::Elements::HyperlinkTextSource,
300
300
  collection: true
301
+ attribute :properties, Idml::Elements::Properties, collection: true
301
302
  attribute :table, Idml::Elements::Table, collection: true
302
303
  attribute :xml_element, Idml::Elements::XmlElement, collection: true
303
304
  attribute :footnote, Idml::Elements::Footnote, collection: true
@@ -664,6 +665,7 @@ module Idml
664
665
  map_element "CharacterStyleRange", to: :character_style_range
665
666
  map_element "Content", to: :content
666
667
  map_element "HyperlinkTextSource", to: :hyperlink_text_source
668
+ map_element "Properties", to: :properties
667
669
  map_element "Table", to: :table
668
670
  map_element "XMLElement", to: :xml_element
669
671
  map_element "Footnote", to: :footnote
@@ -298,6 +298,7 @@ module Idml
298
298
  collection: true
299
299
  attribute :content, Idml::Elements::Content, collection: true
300
300
  attribute :xml_element, Idml::Elements::XmlElement, collection: true
301
+ attribute :properties, Idml::Elements::Properties, collection: true
301
302
 
302
303
  xml do
303
304
  root "ParagraphStyleRange"
@@ -651,6 +652,7 @@ module Idml
651
652
  map_element "ParagraphStyleRange", to: :paragraph_style_range
652
653
  map_element "Content", to: :content
653
654
  map_element "XMLElement", to: :xml_element
655
+ map_element "Properties", to: :properties
654
656
  end
655
657
 
656
658
  def text_content
@@ -2,18 +2,31 @@
2
2
 
3
3
  module Idml
4
4
  module Elements
5
- # `<Properties>` — a common child element on page items. Carries
6
- # optional PathBoundingBox, PathGeometry, and Label. Only the
7
- # modeled children are populated from XML; others are ignored.
5
+ # `<Properties>` — a common child element on page items and
6
+ # story style ranges. Carries optional PathBoundingBox,
7
+ # PathGeometry, Label, and the paragraph-decoration color value
8
+ # elements (RuleAboveColor, ParagraphShadingColor, …) used on
9
+ # PSR/CSR Properties. Only the modeled children are populated
10
+ # from XML; others are ignored.
8
11
  class Properties < Lutaml::Model::Serializable
9
12
  attribute :path_bounding_box, :string
10
13
  attribute :path_geometry, Idml::Elements::PathGeometry,
11
14
  collection: true
15
+ attribute :rule_above_color, Idml::Elements::TypedValue
16
+ attribute :rule_below_color, Idml::Elements::TypedValue
17
+ attribute :paragraph_shading_color, Idml::Elements::TypedValue
18
+ attribute :paragraph_border_color, Idml::Elements::TypedValue
19
+ attribute :paragraph_border_gap_color, Idml::Elements::TypedValue
12
20
 
13
21
  xml do
14
22
  root "Properties"
15
23
  map_element "PathBoundingBox", to: :path_bounding_box
16
24
  map_element "PathGeometry", to: :path_geometry
25
+ map_element "RuleAboveColor", to: :rule_above_color
26
+ map_element "RuleBelowColor", to: :rule_below_color
27
+ map_element "ParagraphShadingColor", to: :paragraph_shading_color
28
+ map_element "ParagraphBorderColor", to: :paragraph_border_color
29
+ map_element "ParagraphBorderGapColor", to: :paragraph_border_gap_color
17
30
  end
18
31
 
19
32
  def first_geometry
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Elements
5
+ # Typed value child element — IDML's `(object_type, xsd:string) |
6
+ # (string_type, xsd:string)` wrapper used inside Properties for
7
+ # values like RuleAboveColor and ParagraphShadingColor: a `type`
8
+ # attribute plus the value as text content.
9
+ class TypedValue < Lutaml::Model::Serializable
10
+ attribute :type, :string
11
+ attribute :value, :string
12
+
13
+ xml do
14
+ root "TypedValue"
15
+ map_attribute "type", to: :type
16
+ map_content to: :value
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/idml/elements.rb CHANGED
@@ -147,6 +147,7 @@ module Idml
147
147
  autoload :TOCStyle, "idml/elements/toc_style"
148
148
  autoload :TOCStyleEntry, "idml/elements/toc_style_entry"
149
149
  autoload :Table, "idml/elements/table"
150
+ autoload :TypedValue, "idml/elements/typed_value"
150
151
  autoload :TableCell, "idml/elements/table_cell"
151
152
  autoload :TableRow, "idml/elements/table_row"
152
153
  autoload :TableStyle, "idml/elements/table_style"
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Renders a page item's PathGeometry as true Bézier contours on
6
+ # a pdfrb canvas. Shared by OvalRenderer, PathRenderer, and
7
+ # PolygonRenderer: every item whose Properties carry a
8
+ # PathGeometry draws its actual outline — PathPointType anchors
9
+ # with LeftDirection / RightDirection control handles,
10
+ # ItemTransform applied, y flipped into PDF space — instead of
11
+ # its bounding box. Paint op selection (fill / stroke /
12
+ # gradient) lives in ShapePaint. Items without usable geometry
13
+ # fall back to the bounding-box rectangle.
14
+ module Contour
15
+ # Renders the item's contours with fill/stroke from
16
+ # ShapePaint. Returns true when painted.
17
+ def self.render(canvas, context)
18
+ item = context.item
19
+ return false if item.visible == false
20
+
21
+ paths = geometry_paths(item)
22
+ return rectangle_fallback(canvas, item, context) if paths.empty?
23
+
24
+ ShapePaint.paint(canvas, item, context) do |c|
25
+ draw_path(c, paths, item.item_transform, context.page_height)
26
+ end
27
+ end
28
+
29
+ def self.rectangle_fallback(canvas, item, context)
30
+ box = Placement.box(item, context.page_height)
31
+ return false unless box
32
+
33
+ ShapePaint.paint(canvas, item, context) do |c|
34
+ c.rectangle(box[:x], box[:y], box[:width], box[:height])
35
+ end
36
+ end
37
+ private_class_method :rectangle_fallback
38
+
39
+ # All GeometryPathType children across the item's Properties.
40
+ def self.geometry_paths(item)
41
+ item.properties.flat_map do |props|
42
+ props.path_geometry.flat_map(&:geometry_path_type)
43
+ end
44
+ end
45
+
46
+ # Constructs the full path on the canvas: one subpath per
47
+ # GeometryPathType, each starting at its first anchor and
48
+ # curving through every subsequent anchor via the points'
49
+ # control handles. Closed paths (PathOpen false or absent)
50
+ # loop back to the first anchor and close.
51
+ def self.draw_path(canvas, paths, item_transform, page_height)
52
+ transform = Geometry.parse_transform(item_transform)
53
+ paths.each do |path|
54
+ draw_subpath(canvas, path, transform, page_height)
55
+ end
56
+ end
57
+
58
+ def self.draw_subpath(canvas, path, transform, page_height)
59
+ points = path.points
60
+ return if points.length < 2
61
+
62
+ open = path.path_open == true
63
+ anchors = points.map do |point|
64
+ pdf_pair(point.anchor, transform, page_height)
65
+ end
66
+ canvas.move_to(anchors.first[0], anchors.first[1])
67
+ draw_segments(canvas, points, anchors, transform, page_height, open)
68
+ canvas.close_path unless open
69
+ end
70
+ private_class_method :draw_subpath
71
+
72
+ def self.draw_segments(canvas, points, anchors, transform,
73
+ page_height, open)
74
+ segment_count = open ? points.length - 1 : points.length
75
+ segment_count.times do |index|
76
+ from = points[index]
77
+ to = points[(index + 1) % points.length]
78
+ c1 = control_point(from, :right_direction, from.anchor,
79
+ transform, page_height)
80
+ c2 = control_point(to, :left_direction, to.anchor,
81
+ transform, page_height)
82
+ to_anchor = anchors[(index + 1) % points.length]
83
+ canvas.curve_to(c1[0], c1[1], c2[0], c2[1],
84
+ to_anchor[0], to_anchor[1])
85
+ end
86
+ end
87
+ private_class_method :draw_segments
88
+
89
+ # Control point for one end of a segment: the point's
90
+ # direction handle when present, else the anchor itself
91
+ # (degenerate control = straight-ish curve).
92
+ def self.control_point(point, direction_attr, fallback, transform,
93
+ page_height)
94
+ raw = point.public_send(direction_attr)
95
+ raw = nil if raw && raw.strip.empty?
96
+ pdf_pair(raw || fallback, transform, page_height)
97
+ end
98
+ private_class_method :control_point
99
+
100
+ # Parses an IDML "x y" pair, applies the item transform, and
101
+ # flips y into PDF space (origin bottom-left).
102
+ def self.pdf_pair(raw, transform, page_height)
103
+ x, y = raw.to_s.split(/\s+/).map(&:to_f)
104
+ tx, ty = Geometry.apply_transform(transform, x, y)
105
+ [tx, page_height - ty]
106
+ end
107
+ private_class_method :pdf_pair
108
+ end
109
+ end
110
+ end
@@ -82,6 +82,131 @@ module Idml
82
82
  end
83
83
  private_class_method :emit_rule
84
84
 
85
+ # Emits the paragraph's shading: a fill rect behind the
86
+ # paragraph block (top_y..bottom_y within the column). The
87
+ # ParagraphShading* offsets expand the rect outward; tint
88
+ # scales the color. Color comes from Properties >
89
+ # ParagraphShadingColor (black when absent, per InDesign's
90
+ # default shading color).
91
+ def self.emit_shading(canvas, paragraph, context, top_y, bottom_y,
92
+ frame_left, frame_right)
93
+ return unless paragraph.paragraph_shading_on
94
+ return unless top_y > bottom_y
95
+
96
+ tint_value = decoration_tint(paragraph.paragraph_shading_tint)
97
+ return unless tint_value
98
+
99
+ color = resolve_color(paragraph.paragraph_shading_color,
100
+ tint_value, context)
101
+ return unless color
102
+
103
+ left, right, top, bottom = decoration_rect(
104
+ paragraph, top_y, bottom_y, frame_left, frame_right,
105
+ left_offset: paragraph.paragraph_shading_left_offset,
106
+ right_offset: paragraph.paragraph_shading_right_offset,
107
+ top_offset: paragraph.paragraph_shading_top_offset,
108
+ bottom_offset: paragraph.paragraph_shading_bottom_offset
109
+ )
110
+ canvas.fill_color(color)
111
+ canvas.rectangle(left, bottom, right - left, top - bottom)
112
+ canvas.fill
113
+ end
114
+
115
+ # Emits the paragraph's border: per-side strokes around the
116
+ # paragraph block, each side with its own line weight, drawn
117
+ # at the block rect expanded by the border offsets.
118
+ def self.emit_border(canvas, paragraph, context, top_y, bottom_y,
119
+ frame_left, frame_right)
120
+ return unless paragraph.paragraph_border_on
121
+ return unless top_y > bottom_y
122
+
123
+ tint_value = decoration_tint(paragraph.paragraph_border_tint)
124
+ return unless tint_value
125
+
126
+ color = resolve_color(paragraph.paragraph_border_color,
127
+ tint_value, context)
128
+ return unless color
129
+
130
+ left, right, top, bottom = decoration_rect(
131
+ paragraph, top_y, bottom_y, frame_left, frame_right,
132
+ left_offset: paragraph.paragraph_border_left_offset,
133
+ right_offset: paragraph.paragraph_border_right_offset,
134
+ top_offset: paragraph.paragraph_border_top_offset,
135
+ bottom_offset: paragraph.paragraph_border_bottom_offset
136
+ )
137
+ apply_rule_color(canvas, paragraph.paragraph_border_color,
138
+ tint_value, context)
139
+ stroke_border_sides(canvas, paragraph, left, right, top, bottom)
140
+ end
141
+
142
+ # Tint value for a decoration, or nil when zero/absent.
143
+ def self.decoration_tint(tint)
144
+ value = (tint || DEFAULT_TINT).to_f
145
+ value.positive? ? value : nil
146
+ end
147
+ private_class_method :decoration_tint
148
+
149
+ # The decoration rect [left, right, top, bottom]: the column
150
+ # (or text) extents expanded outward by the given offsets.
151
+ def self.decoration_rect(paragraph, top_y, bottom_y, frame_left,
152
+ frame_right, left_offset:, right_offset:,
153
+ top_offset:, bottom_offset:)
154
+ left, right = decoration_extents(paragraph, frame_left, frame_right)
155
+ [left - (left_offset || 0).to_f,
156
+ right + (right_offset || 0).to_f,
157
+ top_y + (top_offset || 0).to_f,
158
+ bottom_y - (bottom_offset || 0).to_f]
159
+ end
160
+ private_class_method :decoration_rect
161
+
162
+ def self.stroke_border_sides(canvas, paragraph, left, right,
163
+ top, bottom)
164
+ stroke_horizontal(canvas, left, right, top,
165
+ paragraph.paragraph_border_top_line_weight)
166
+ stroke_horizontal(canvas, left, right, bottom,
167
+ paragraph.paragraph_border_bottom_line_weight)
168
+ stroke_vertical(canvas, bottom, top, left,
169
+ paragraph.paragraph_border_left_line_weight)
170
+ stroke_vertical(canvas, bottom, top, right,
171
+ paragraph.paragraph_border_right_line_weight)
172
+ end
173
+ private_class_method :stroke_border_sides
174
+
175
+ def self.stroke_horizontal(canvas, x1, x2, y, weight)
176
+ thickness = weight.to_f
177
+ return unless thickness.positive?
178
+
179
+ canvas.line_width = thickness
180
+ canvas.move_to(x1, y)
181
+ canvas.line_to(x2, y)
182
+ canvas.stroke
183
+ end
184
+ private_class_method :stroke_horizontal
185
+
186
+ def self.stroke_vertical(canvas, y1, y2, x, weight)
187
+ thickness = weight.to_f
188
+ return unless thickness.positive?
189
+
190
+ canvas.line_width = thickness
191
+ canvas.move_to(x, y1)
192
+ canvas.line_to(x, y2)
193
+ canvas.stroke
194
+ end
195
+ private_class_method :stroke_vertical
196
+
197
+ # Horizontal extents for shading/border: the column width, or
198
+ # the text width (indented by the paragraph's indents) when
199
+ # the shading width mode is "TextWidth".
200
+ def self.decoration_extents(paragraph, frame_left, frame_right)
201
+ if paragraph.paragraph_shading_width == "TextWidth"
202
+ [frame_left + (paragraph.left_indent || 0).to_f,
203
+ frame_right - (paragraph.right_indent || 0).to_f]
204
+ else
205
+ [frame_left, frame_right]
206
+ end
207
+ end
208
+ private_class_method :decoration_extents
209
+
85
210
  # Returns `[x1, x2]` for the rule. Width mode "Text" indents
86
211
  # the rule by the paragraph's left/right indent (matches the
87
212
  # text bounds); "Column" (default) spans the full column.
@@ -3,14 +3,13 @@
3
3
  module Idml
4
4
  module Render
5
5
  module Renderers
6
- # Renders an IDML Oval on a Pdfrb::Content::Canvas. Delegates to
7
- # RectangleRenderer which draws the bounding box with fill/stroke.
8
- # Approximation: a true ellipse would generate bezier anchor and
9
- # control points from the PathGeometry; the bounding box renders
10
- # the correct extent and colors. See TODO 106 for refinement.
6
+ # Renders an IDML Oval on a Pdfrb::Content::Canvas as a true
7
+ # Bézier ellipse from its PathGeometry (which preserves any
8
+ # rotation in the ItemTransform). Falls back to the
9
+ # bounding-box rectangle when the item carries no geometry.
11
10
  class OvalRenderer
12
11
  def self.render(canvas, context)
13
- RectangleRenderer.render(canvas, context)
12
+ Contour.render(canvas, context)
14
13
  end
15
14
  end
16
15
  end
@@ -4,14 +4,13 @@ module Idml
4
4
  module Render
5
5
  module Renderers
6
6
  # Renders an IDML Path (standalone compound path) on a
7
- # Pdfrb::Content::Canvas. Delegates to RectangleRenderer which
8
- # draws the bounding box with fill/stroke. Approximation: a true
9
- # compound path would draw each sub-path from the EntirePath
10
- # list or PathPoint children; the bounding box renders the
11
- # correct extent and colors. See TODO 106 for refinement.
7
+ # Pdfrb::Content::Canvas as its actual Bézier contours — one
8
+ # subpath per GeometryPathType, closed or open per PathOpen.
9
+ # Falls back to the bounding-box rectangle when the item
10
+ # carries no geometry.
12
11
  class PathRenderer
13
12
  def self.render(canvas, context)
14
- RectangleRenderer.render(canvas, context)
13
+ Contour.render(canvas, context)
15
14
  end
16
15
  end
17
16
  end
@@ -3,46 +3,15 @@
3
3
  module Idml
4
4
  module Render
5
5
  module Renderers
6
+ # Renders an IDML Polygon on a Pdfrb::Content::Canvas as its
7
+ # actual Bézier contours from PathGeometry (control handles
8
+ # default to the anchors, so plain polygons render straight
9
+ # edges). Falls back to the bounding-box rectangle when the
10
+ # item carries no geometry.
6
11
  class PolygonRenderer
7
12
  def self.render(canvas, context)
8
- poly = context.item
9
- return if poly.visible == false
10
-
11
- box = Placement.box(poly, context.page_height)
12
- return unless box
13
-
14
- Blending.wrap(canvas, poly.transparency_setting) do
15
- render_fill(canvas, poly, context, box)
16
- render_stroke(canvas, poly, context, box)
17
- end
18
- end
19
-
20
- def self.render_fill(canvas, poly, context, box)
21
- return unless poly.fill_color && poly.fill_color != "Color/None"
22
-
23
- color = context.color_resolver&.resolve(poly.fill_color)
24
- return unless color
25
-
26
- canvas.fill_color(ColorHelper.to_canvas(color))
27
- canvas.rectangle(box[:x], box[:y], box[:width], box[:height])
28
- canvas.fill
29
- end
30
- private_class_method :render_fill
31
-
32
- def self.render_stroke(canvas, poly, context, box)
33
- return unless StrokeStyle.strokeable?(poly)
34
-
35
- color = context.color_resolver&.resolve(poly.stroke_color)
36
- return unless color
37
-
38
- StrokeStyle.apply(canvas, poly) do
39
- canvas.stroke_color(ColorHelper.to_canvas(color))
40
- canvas.line_width = poly.stroke_weight
41
- canvas.rectangle(box[:x], box[:y], box[:width], box[:height])
42
- canvas.stroke
43
- end
13
+ Contour.render(canvas, context)
44
14
  end
45
- private_class_method :render_stroke
46
15
  end
47
16
  end
48
17
  end
@@ -3,7 +3,9 @@
3
3
  module Idml
4
4
  module Render
5
5
  module Renderers
6
- # Renders an IDML Rectangle on a Pdfrb::Content::Canvas.
6
+ # Renders an IDML Rectangle on a Pdfrb::Content::Canvas: an
7
+ # axis-aligned rectangle from the item's placement box, with
8
+ # fill / stroke / gradient painting delegated to ShapePaint.
7
9
  class RectangleRenderer
8
10
  def self.render(canvas, context)
9
11
  rect = context.item
@@ -12,101 +14,10 @@ module Idml
12
14
  box = Placement.box(rect, context.page_height)
13
15
  return unless box
14
16
 
15
- Blending.wrap(canvas, rect.transparency_setting) do
16
- render_fill(canvas, rect, context, box)
17
- render_stroke(canvas, rect, context, box)
17
+ ShapePaint.paint(canvas, rect, context) do |c|
18
+ c.rectangle(box[:x], box[:y], box[:width], box[:height])
18
19
  end
19
20
  end
20
-
21
- def self.render_fill(canvas, rect, context, box)
22
- return unless rect.fill_color
23
- return if rect.fill_color == "Color/None"
24
-
25
- if GradientResolver.gradient?(rect.fill_color)
26
- render_gradient_fill(canvas, rect, context, box)
27
- return
28
- end
29
-
30
- color = context.color_resolver&.resolve(rect.fill_color)
31
- return unless color
32
-
33
- canvas.fill_color(ColorHelper.to_canvas(color))
34
- canvas.rectangle(box[:x], box[:y], box[:width], box[:height])
35
- canvas.fill
36
- end
37
- private_class_method :render_fill
38
-
39
- def self.render_gradient_fill(canvas, rect, context, box)
40
- gradient = gradient_for(rect, context)
41
- return unless gradient
42
- return unless gradient.gradient_stop.length >= 2
43
-
44
- stops = gradient_stops(gradient, context)
45
- return unless stops.length >= 2
46
-
47
- shading = if gradient.type == "Radial"
48
- radial_shading(canvas, box, stops)
49
- else
50
- axial_shading(canvas, box, stops)
51
- end
52
- canvas.rectangle(box[:x], box[:y], box[:width], box[:height])
53
- canvas.fill_shading(shading)
54
- end
55
- private_class_method :render_gradient_fill
56
-
57
- def self.axial_shading(canvas, box, stops)
58
- canvas.document.shadings.add_axial(
59
- from: [box[:x], box[:y] + box[:height]],
60
- to: [box[:x] + box[:width], box[:y]],
61
- stops: stops,
62
- )
63
- end
64
- private_class_method :axial_shading
65
-
66
- def self.radial_shading(canvas, box, stops)
67
- cx = box[:x] + (box[:width] / 2.0)
68
- cy = box[:y] + (box[:height] / 2.0)
69
- radius = [box[:width], box[:height]].max / 2.0
70
- canvas.document.shadings.add_radial(
71
- from: [cx, cy, 0.0],
72
- to: [cx, cy, radius],
73
- stops: stops,
74
- )
75
- end
76
- private_class_method :radial_shading
77
-
78
- def self.gradient_for(rect, context)
79
- graphic = context.package&.graphic
80
- return nil unless graphic
81
-
82
- graphic.gradient.find { |g| g.self_attr == rect.fill_color }
83
- end
84
- private_class_method :gradient_for
85
-
86
- def self.gradient_stops(gradient, context)
87
- gradient.gradient_stop.filter_map do |stop|
88
- color = context.color_resolver&.resolve(stop.stop_color)
89
- next unless color
90
-
91
- [stop.location || 0.0, ColorHelper.to_canvas(color)]
92
- end
93
- end
94
- private_class_method :gradient_stops
95
-
96
- def self.render_stroke(canvas, rect, context, box)
97
- return unless StrokeStyle.strokeable?(rect)
98
-
99
- color = context.color_resolver&.resolve(rect.stroke_color)
100
- return unless color
101
-
102
- StrokeStyle.apply(canvas, rect) do
103
- canvas.stroke_color(ColorHelper.to_canvas(color))
104
- canvas.line_width = rect.stroke_weight
105
- canvas.rectangle(box[:x], box[:y], box[:width], box[:height])
106
- canvas.stroke
107
- end
108
- end
109
- private_class_method :render_stroke
110
21
  end
111
22
  end
112
23
  end
@@ -445,6 +445,12 @@ module Idml
445
445
 
446
446
  emit_paragraph_top_rule(canvas, paragraph, context, cursor_y,
447
447
  frame_left, frame_right, first_paragraph)
448
+ block_top, block_bottom = paragraph_block_extent(
449
+ paragraph, font, wrap_width, cursor_y, bottom_limit
450
+ )
451
+ ParagraphRules.emit_shading(canvas, paragraph, context,
452
+ block_top, block_bottom,
453
+ frame_left, frame_right)
448
454
  drop_cap = emit_drop_cap(canvas, paragraph, context, frame_left,
449
455
  cursor_y, first_paragraph)
450
456
 
@@ -463,10 +469,28 @@ module Idml
463
469
  cursor_y, frame_left,
464
470
  frame_right, para_attrs,
465
471
  remaining_runs)
472
+ ParagraphRules.emit_border(canvas, paragraph, context,
473
+ block_top, block_bottom,
474
+ frame_left, frame_right)
466
475
  [remaining_runs, cursor_y, char_cursor]
467
476
  end
468
477
  private_class_method :render_runs_for_paragraph
469
478
 
479
+ # The paragraph's block extent: top at the current cursor,
480
+ # bottom at the pre-measured height clamped to the frame's
481
+ # bottom limit. Shading and border both use this one rect so
482
+ # they agree even when the paragraph splits across frames
483
+ # (the split case shades only the rendered part).
484
+ def self.paragraph_block_extent(paragraph, font, wrap_width,
485
+ cursor_y, bottom_limit)
486
+ height = TextEngine::Measurement.paragraph_height(
487
+ paragraph, font, wrap_width
488
+ )
489
+ block_bottom = [cursor_y - height, bottom_limit].max
490
+ [cursor_y, block_bottom]
491
+ end
492
+ private_class_method :paragraph_block_extent
493
+
470
494
  def self.iterate_paragraph_runs(canvas, paragraph, runs, context,
471
495
  layout_frame, font, wrap_width,
472
496
  cursor_y, bottom_limit, char_cursor,
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Paints a page item's fill and stroke on a pdfrb canvas,
6
+ # delegating path construction to the caller's block. The
7
+ # single owner of the IDML → PDF paint mapping — solid fill,
8
+ # gradient shading fill, stroke styling — shared by every
9
+ # shape renderer. Callers choose the geometry (axis-aligned
10
+ # rectangle, Bézier contour); this module chooses the paint
11
+ # and the PDF paint op sequencing (fill, stroke, or
12
+ # fill-and-stroke on one path).
13
+ module ShapePaint
14
+ # Paints the item. The block receives the canvas and must
15
+ # construct the current path (move_to/line_to/curve_to/...).
16
+ # The block may be called more than once (a path is consumed
17
+ # by each paint op, so fill-then-stroke rebuilds it).
18
+ # Returns nil when there is nothing to paint; truthy
19
+ # otherwise.
20
+ def self.paint(canvas, item, context, &build_path)
21
+ fill = fill_style(item, context)
22
+ stroke = stroke_style(item, context)
23
+ return unless fill || stroke
24
+
25
+ Blending.wrap(canvas, item.transparency_setting) do
26
+ paint_styles(canvas, item, context, fill, stroke, &build_path)
27
+ end
28
+ end
29
+
30
+ # Returns the fill plan: { kind: :solid, color: } for a
31
+ # resolved color, { kind: :gradient, gradient:, stops: } for
32
+ # a fill referencing a Gradient, nil when nothing fillable.
33
+ def self.fill_style(item, context)
34
+ return nil unless item.fill_color
35
+ return nil if item.fill_color == "Color/None"
36
+
37
+ if GradientResolver.gradient?(item.fill_color)
38
+ gradient_fill_style(item, context)
39
+ else
40
+ solid_fill_style(item, context)
41
+ end
42
+ end
43
+ private_class_method :fill_style
44
+
45
+ def self.gradient_fill_style(item, context)
46
+ gradient = gradient_for(item, context)
47
+ return nil unless gradient
48
+
49
+ stops = gradient_stops(gradient, context)
50
+ return nil unless stops.length >= 2
51
+
52
+ { kind: :gradient, gradient: gradient, stops: stops }
53
+ end
54
+ private_class_method :gradient_fill_style
55
+
56
+ def self.solid_fill_style(item, context)
57
+ color = context.color_resolver&.resolve(item.fill_color)
58
+ return nil unless color
59
+
60
+ { kind: :solid, color: ColorHelper.to_canvas(color) }
61
+ end
62
+ private_class_method :solid_fill_style
63
+
64
+ def self.stroke_style(item, context)
65
+ return nil unless StrokeStyle.strokeable?(item)
66
+
67
+ color = context.color_resolver&.resolve(item.stroke_color)
68
+ return nil unless color
69
+
70
+ { color: ColorHelper.to_canvas(color) }
71
+ end
72
+ private_class_method :stroke_style
73
+
74
+ def self.paint_styles(canvas, item, context, fill, stroke, &)
75
+ if fill && stroke
76
+ paint_fill_and_stroke(canvas, item, context, fill, stroke,
77
+ &)
78
+ elsif fill
79
+ paint_fill(canvas, item, context, fill, &)
80
+ else
81
+ paint_stroke(canvas, item, stroke, &)
82
+ end
83
+ end
84
+ private_class_method :paint_styles
85
+
86
+ def self.paint_fill(canvas, item, context, fill, &)
87
+ yield(canvas)
88
+ if fill[:kind] == :gradient
89
+ canvas.fill_shading(shading_for(canvas, item, context, fill))
90
+ else
91
+ canvas.fill_color(fill[:color])
92
+ canvas.fill
93
+ end
94
+ end
95
+ private_class_method :paint_fill
96
+
97
+ def self.paint_stroke(canvas, item, stroke, &)
98
+ StrokeStyle.apply(canvas, item) do
99
+ canvas.stroke_color(stroke[:color])
100
+ canvas.line_width = item.stroke_weight
101
+ yield(canvas)
102
+ canvas.stroke
103
+ end
104
+ end
105
+ private_class_method :paint_stroke
106
+
107
+ # Fill and stroke on one path where possible (fill_stroke);
108
+ # a gradient fill consumes the path, so the stroke rebuilds
109
+ # it afterward.
110
+ def self.paint_fill_and_stroke(canvas, item, context, fill, stroke,
111
+ &)
112
+ if fill[:kind] == :gradient
113
+ paint_fill(canvas, item, context, fill, &)
114
+ paint_stroke(canvas, item, stroke, &)
115
+ else
116
+ yield(canvas)
117
+ canvas.fill_color(fill[:color])
118
+ StrokeStyle.apply(canvas, item) do
119
+ canvas.stroke_color(stroke[:color])
120
+ canvas.line_width = item.stroke_weight
121
+ canvas.fill_stroke
122
+ end
123
+ end
124
+ end
125
+ private_class_method :paint_fill_and_stroke
126
+
127
+ def self.shading_for(canvas, item, context, fill)
128
+ box = Placement.box(item, context.page_height)
129
+ stops = fill[:stops]
130
+ if fill[:gradient].type == "Radial"
131
+ canvas.document.shadings.add_radial(
132
+ from: radial_center(box, 0.0),
133
+ to: radial_center(box, [box[:width], box[:height]].max / 2.0),
134
+ stops: stops,
135
+ )
136
+ else
137
+ canvas.document.shadings.add_axial(
138
+ from: [box[:x], box[:y] + box[:height]],
139
+ to: [box[:x] + box[:width], box[:y]],
140
+ stops: stops,
141
+ )
142
+ end
143
+ end
144
+ private_class_method :shading_for
145
+
146
+ def self.radial_center(box, radius)
147
+ cx = box[:x] + (box[:width] / 2.0)
148
+ cy = box[:y] + (box[:height] / 2.0)
149
+ [cx, cy, radius]
150
+ end
151
+ private_class_method :radial_center
152
+
153
+ def self.gradient_for(item, context)
154
+ graphic = context.package&.graphic
155
+ return nil unless graphic
156
+
157
+ graphic.gradient.find { |g| g.self_attr == item.fill_color }
158
+ end
159
+ private_class_method :gradient_for
160
+
161
+ def self.gradient_stops(gradient, context)
162
+ gradient.gradient_stop.filter_map do |stop|
163
+ color = context.color_resolver&.resolve(stop.stop_color)
164
+ next unless color
165
+
166
+ [stop.location || 0.0, ColorHelper.to_canvas(color)]
167
+ end
168
+ end
169
+ private_class_method :gradient_stops
170
+ end
171
+ end
172
+ end
@@ -82,6 +82,28 @@ module Idml
82
82
  :rule_below_left_indent,
83
83
  :rule_below_right_indent,
84
84
  :rule_below_width,
85
+ # ParagraphShading — fill rect behind the paragraph block.
86
+ # Color comes from Properties > ParagraphShadingColor.
87
+ :paragraph_shading_on,
88
+ :paragraph_shading_color,
89
+ :paragraph_shading_tint,
90
+ :paragraph_shading_width,
91
+ :paragraph_shading_left_offset,
92
+ :paragraph_shading_right_offset,
93
+ :paragraph_shading_top_offset,
94
+ :paragraph_shading_bottom_offset,
95
+ # ParagraphBorder — per-side strokes around the paragraph.
96
+ :paragraph_border_on,
97
+ :paragraph_border_color,
98
+ :paragraph_border_tint,
99
+ :paragraph_border_top_line_weight,
100
+ :paragraph_border_left_line_weight,
101
+ :paragraph_border_right_line_weight,
102
+ :paragraph_border_bottom_line_weight,
103
+ :paragraph_border_top_offset,
104
+ :paragraph_border_left_offset,
105
+ :paragraph_border_right_offset,
106
+ :paragraph_border_bottom_offset,
85
107
  keyword_init: true,
86
108
  )
87
109
 
@@ -171,7 +193,8 @@ module Idml
171
193
  rule_above: resolve_attr(style_lookup, psr, :rule_above),
172
194
  rule_above_line_weight: resolve_attr(style_lookup, psr,
173
195
  :rule_above_line_weight),
174
- rule_above_color: resolve_attr(style_lookup, psr, :stroke_color),
196
+ rule_above_color: properties_color(psr, :rule_above_color) ||
197
+ resolve_attr(style_lookup, psr, :stroke_color),
175
198
  rule_above_tint: resolve_attr(style_lookup, psr,
176
199
  :rule_above_tint),
177
200
  rule_above_offset: resolve_attr(style_lookup, psr,
@@ -185,7 +208,8 @@ module Idml
185
208
  rule_below: resolve_attr(style_lookup, psr, :rule_below),
186
209
  rule_below_line_weight: resolve_attr(style_lookup, psr,
187
210
  :rule_below_line_weight),
188
- rule_below_color: resolve_attr(style_lookup, psr, :stroke_color),
211
+ rule_below_color: properties_color(psr, :rule_below_color) ||
212
+ resolve_attr(style_lookup, psr, :stroke_color),
189
213
  rule_below_tint: resolve_attr(style_lookup, psr,
190
214
  :rule_below_tint),
191
215
  rule_below_offset: resolve_attr(style_lookup, psr,
@@ -196,12 +220,70 @@ module Idml
196
220
  :rule_below_right_indent),
197
221
  rule_below_width: resolve_attr(style_lookup, psr,
198
222
  :rule_below_width),
223
+ paragraph_shading_on: resolve_attr(style_lookup, psr,
224
+ :paragraph_shading_on),
225
+ paragraph_shading_color: properties_color(psr,
226
+ :paragraph_shading_color),
227
+ paragraph_shading_tint: resolve_attr(style_lookup, psr,
228
+ :paragraph_shading_tint),
229
+ paragraph_shading_width: resolve_attr(style_lookup, psr,
230
+ :paragraph_shading_width),
231
+ paragraph_shading_left_offset: resolve_attr(
232
+ style_lookup, psr, :paragraph_shading_left_offset
233
+ ),
234
+ paragraph_shading_right_offset: resolve_attr(
235
+ style_lookup, psr, :paragraph_shading_right_offset
236
+ ),
237
+ paragraph_shading_top_offset: resolve_attr(
238
+ style_lookup, psr, :paragraph_shading_top_offset
239
+ ),
240
+ paragraph_shading_bottom_offset: resolve_attr(
241
+ style_lookup, psr, :paragraph_shading_bottom_offset
242
+ ),
243
+ paragraph_border_on: resolve_attr(style_lookup, psr,
244
+ :paragraph_border_on),
245
+ paragraph_border_color: properties_color(psr,
246
+ :paragraph_border_color),
247
+ paragraph_border_tint: resolve_attr(style_lookup, psr,
248
+ :paragraph_border_tint),
249
+ paragraph_border_top_line_weight: resolve_attr(
250
+ style_lookup, psr, :paragraph_border_top_line_weight
251
+ ),
252
+ paragraph_border_left_line_weight: resolve_attr(
253
+ style_lookup, psr, :paragraph_border_left_line_weight
254
+ ),
255
+ paragraph_border_right_line_weight: resolve_attr(
256
+ style_lookup, psr, :paragraph_border_right_line_weight
257
+ ),
258
+ paragraph_border_bottom_line_weight: resolve_attr(
259
+ style_lookup, psr, :paragraph_border_bottom_line_weight
260
+ ),
261
+ paragraph_border_top_offset: resolve_attr(
262
+ style_lookup, psr, :paragraph_border_top_offset
263
+ ),
264
+ paragraph_border_left_offset: resolve_attr(
265
+ style_lookup, psr, :paragraph_border_left_offset
266
+ ),
267
+ paragraph_border_right_offset: resolve_attr(
268
+ style_lookup, psr, :paragraph_border_right_offset
269
+ ),
270
+ paragraph_border_bottom_offset: resolve_attr(
271
+ style_lookup, psr, :paragraph_border_bottom_offset
272
+ ),
199
273
  )
200
274
  prepend_list_marker(paragraph)
201
275
  paragraph
202
276
  end
203
277
  end
204
278
 
279
+ # Reads a color value element from the PSR's Properties (e.g.
280
+ # Properties > ParagraphShadingColor). Returns the color name
281
+ # string, or nil when the element is absent.
282
+ def self.properties_color(psr, element_attr)
283
+ psr.properties.first&.public_send(element_attr)&.value
284
+ end
285
+ private_class_method :properties_color
286
+
205
287
  def self.csr_runs(psr, condition_filter: nil, style_lookup: nil,
206
288
  footnote_counter: nil, footnote_option: nil)
207
289
  counter = footnote_counter || Footnote.counter_for(footnote_option)
data/lib/idml/render.rb CHANGED
@@ -23,6 +23,8 @@ module Idml
23
23
  autoload :ConditionFilter, "#{__dir__}/render/condition_filter"
24
24
  autoload :TextWrapResolver, "#{__dir__}/render/text_wrap_resolver"
25
25
  autoload :Footnote, "#{__dir__}/render/footnote"
26
+ autoload :ShapePaint, "#{__dir__}/render/shape_paint"
27
+ autoload :Contour, "#{__dir__}/render/contour"
26
28
  autoload :Placement, "#{__dir__}/render/placement"
27
29
  autoload :ImageCollector, "#{__dir__}/render/image_collector"
28
30
  autoload :StructureTracker, "#{__dir__}/render/structure_tracker"
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module TextEngine
5
+ # Height measurement for paragraphs — shape + wrap each run and
6
+ # sum the line count × leading. Used where a paragraph's full
7
+ # extent must be known before its lines are laid out (paragraph
8
+ # shading/border rects).
9
+ module Measurement
10
+ DEFAULT_POINT_SIZE = 12.0
11
+
12
+ # Returns the paragraph's rendered height at `wrap_width`:
13
+ # every run's wrapped line count × leading, plus paragraph
14
+ # SpaceBefore / SpaceAfter.
15
+ def self.paragraph_height(paragraph, font, wrap_width)
16
+ lines_height = paragraph.runs.sum do |run|
17
+ size = run.point_size || DEFAULT_POINT_SIZE
18
+ lines = wrapped_lines(run, font, wrap_width, size)
19
+ lines.length *
20
+ TextEngine::VerticalLayout.leading_for(paragraph.auto_leading,
21
+ size)
22
+ end
23
+ lines_height + space_before_after(paragraph)
24
+ end
25
+
26
+ def self.wrapped_lines(run, font, wrap_width, size)
27
+ glyphs = TextEngine::Shaper.shape(text: run.text, font: font,
28
+ size: size)
29
+ TextEngine::LineBreaker.break(glyphs: glyphs,
30
+ frame_width: wrap_width)
31
+ end
32
+ private_class_method :wrapped_lines
33
+
34
+ def self.space_before_after(paragraph)
35
+ (paragraph.space_before || 0) + (paragraph.space_after || 0)
36
+ end
37
+ private_class_method :space_before_after
38
+ end
39
+ end
40
+ end
@@ -6,7 +6,8 @@ module Idml
6
6
  # .ttf/.otf files. IDML-agnostic — works on any styled text.
7
7
  module TextEngine
8
8
  autoload :PdfrbFontMetrics, "#{__dir__}/text_engine/pdfrb_font_metrics"
9
- autoload :Shaper, "#{__dir__}/text_engine/shaper"
9
+ autoload :Shaper, "#{__dir__}/text_engine/shaper"
10
+ autoload :Measurement, "#{__dir__}/text_engine/measurement"
10
11
  autoload :ShapedGlyph, "#{__dir__}/text_engine/shaper"
11
12
  autoload :LineBreaker, "#{__dir__}/text_engine/line_breaker"
12
13
  autoload :Line, "#{__dir__}/text_engine/line_breaker"
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.8.8"
4
+ VERSION = "0.9.0"
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.8.8
4
+ version: 0.9.0
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-16 00:00:00.000000000 Z
11
+ date: 2026-08-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -407,6 +407,7 @@ files:
407
407
  - lib/idml/elements/transform_attribute_option.rb
408
408
  - lib/idml/elements/transparency_setting.rb
409
409
  - lib/idml/elements/trap_preset.rb
410
+ - lib/idml/elements/typed_value.rb
410
411
  - lib/idml/elements/xml_element.rb
411
412
  - lib/idml/elements/xml_export_map.rb
412
413
  - lib/idml/elements/xml_import_map.rb
@@ -439,6 +440,7 @@ files:
439
440
  - lib/idml/render/color_helper.rb
440
441
  - lib/idml/render/color_resolver.rb
441
442
  - lib/idml/render/condition_filter.rb
443
+ - lib/idml/render/contour.rb
442
444
  - lib/idml/render/drop_cap.rb
443
445
  - lib/idml/render/font_reference_resolver.rb
444
446
  - lib/idml/render/font_setup.rb
@@ -469,6 +471,7 @@ files:
469
471
  - lib/idml/render/renderers/rectangle_renderer.rb
470
472
  - lib/idml/render/renderers/table_renderer.rb
471
473
  - lib/idml/render/renderers/text_frame_renderer.rb
474
+ - lib/idml/render/shape_paint.rb
472
475
  - lib/idml/render/spread_renderer.rb
473
476
  - lib/idml/render/story_chain_controller.rb
474
477
  - lib/idml/render/story_threader.rb
@@ -482,6 +485,7 @@ files:
482
485
  - lib/idml/text_engine/cjk_layout.rb
483
486
  - lib/idml/text_engine/justifier.rb
484
487
  - lib/idml/text_engine/line_breaker.rb
488
+ - lib/idml/text_engine/measurement.rb
485
489
  - lib/idml/text_engine/pdfrb_font_metrics.rb
486
490
  - lib/idml/text_engine/shaper.rb
487
491
  - lib/idml/text_engine/vertical_layout.rb