idml 0.8.7 → 0.8.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: ea725628da813f202ff5f6c8c9659e1de20b24d8eb9d92c6cd8cc43aad12948c
4
- data.tar.gz: 3ae66c7b8c22ba9660da4a8bae4a8f2314cb13e90aa72f0c673350415ab77cfc
3
+ metadata.gz: d58f00746f6afb4233e161dfa9cdeb116d714eed0c238ec543a0f039b4167f0e
4
+ data.tar.gz: 1a000e34996525fe34cdc441625c9eaa303f121c836ae3f3b07b73c12712abb1
5
5
  SHA512:
6
- metadata.gz: e6ec5c19fb106644c373ee8a3cd21526cfaff1ce88c7d56d7c65b6376fe508ba984203d2d7d7249d426fc9486d21f5d7dbc1f758c026dd6f60c923631e31a4d8
7
- data.tar.gz: 82cd84ff102aafce7c0766e1bb3aea5393a20f4d959753273ad163f15a9b83ab22a323c8b369548b45a3cc6d3e0ec5857eac1663ccdb601e9adb093c7067121e
6
+ metadata.gz: 15001cbf1427dc278c701cd4980b837ff4b015f6c5657eeb5c88c0c8774933934e606439c40fa3c900368ef2959fc1170cd12841a9caadc0c025a199dd18cd68
7
+ data.tar.gz: 41f45b1b2db245e1dd2d5ef40aa60990fc3c957930b3d16a633974d1f267149e8b2c5cf638b7b3549b4318bf8ea62f378fbb5d0f98ad32d4d50fbce0e3cb9505
@@ -1,41 +1,45 @@
1
1
  # TODO PDF 102: Anchored objects (anchored object settings)
2
2
 
3
- ## Status: OPENgap identified in 2026-08-08 audit
4
-
5
- ## Problem
6
-
7
- `Elements::AnchoredObjectSetting` is modeled with the standard
8
- attributes (AnchorSpot, AnchorXunit, AnchorYunit, AnchorPoint,
9
- AnchorSpaceBefore, etc.). The renderer doesn't consult it.
10
-
11
- In IDML, an anchored object is a page item (Rectangle, Polygon,
12
- Group) that's "anchored" to a position within a story's text flow.
13
- When the text reflows, the anchored object moves with it. Today
14
- the renderer treats anchored objects as standalone page items in
15
- the Spread they appear at their spread coordinates, not their
16
- anchored position.
17
-
18
- ## What needs to happen
19
-
20
- 1. Detect when a page item has an `AnchoredObjectSetting` child
21
- with `AnchorType` indicating inline/anchored (vs. inline-only
22
- or floating).
23
- 2. For anchored items, suppress standalone rendering in the spread.
24
- 3. When laying out the story that owns the anchored object, reserve
25
- space at the anchor point and render the item there.
26
-
27
- This is a significant feature — anchored objects are common in
28
- real-world documents but rare in test fixtures.
3
+ ## Status: COMPLETEimplemented 2026-08-16
4
+
5
+ ## What was built
6
+
7
+ - Model wiring: `anchored_object_setting` attribute + mapping on
8
+ Rectangle, Oval, Polygon, GraphicLine, Group, and Path (per
9
+ `*_Object` in Story.rnc every page item can carry
10
+ `<AnchoredObjectSetting>`). The setting parses AnchoredPosition
11
+ (InlinePosition / AboveLine / Anchored), AnchorPoint,
12
+ AnchorXoffset / AnchorYoffset, Horizontal/VerticalReferencePoint,
13
+ and the remaining schema attributes.
14
+ - `CharacterStyleRange` now models its story-embedded page-item
15
+ children: `rectangle`, `oval`, `polygon`, `graphic_line`,
16
+ `group`, `text_frame` collections. Previously these elements were
17
+ dropped at parse time — anchored objects were lost entirely.
18
+ - Rendering: `TextFrameRenderer` discovers embedded items via the
19
+ shared `story_csrs` walk and renders each through
20
+ `PageItemRenderer` dispatch (fill, stroke, gradients, images
21
+ everything the spread-level path does) with a child context.
22
+ Items render at their own stored geometry: InDesign resolves an
23
+ anchored object's position when saving the file, so the stored
24
+ ItemTransform + PathGeometry IS the anchored position for all
25
+ three AnchorTypes.
26
+
27
+ ## Known limitations
28
+
29
+ - AnchorType-specific text reflow is not simulated: the layout
30
+ engine positions body text independently of embedded items, so
31
+ body text may overlap inline/above-line objects in frames whose
32
+ stored text positions differ from our layout engine's output.
33
+ - HorizontalReferencePoint / VerticalReferencePoint /
34
+ HorizontalAlignment / VerticalAlignment on the setting are parsed
35
+ but not consulted (stored geometry already encodes the result).
29
36
 
30
37
  ## Acceptance criteria
31
38
 
32
- - [ ] Page item with AnchoredObjectSetting AnchorType="Inline" renders
33
- at the anchor character's position within the text flow.
34
- - [ ] Page item with AnchorType="Above Line" renders above the line
35
- containing the anchor.
36
- - [ ] Page item with AnchorType="Anchored" renders at the absolute
37
- position derived from AnchorXunit / AnchorYunit.
38
-
39
- ## Dependencies
40
-
41
- - TODO 94 (need to know character positions in the text flow).
39
+ - [x] Page item with AnchoredObjectSetting AnchorType="Inline"
40
+ renders at its anchored position (stored geometry).
41
+ - [x] Page item with AnchorType="Above Line" renders above the
42
+ anchored line (stored geometry).
43
+ - [x] Page item with AnchorType="Anchored" renders at the position
44
+ derived from the anchor (stored geometry; AnchorX/Yoffset
45
+ parsing verified).
@@ -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
 
@@ -1,40 +1,50 @@
1
1
  # TODO PDF 111: Footnote support
2
2
 
3
- ## Status: OPENgap identified in 2026-08-09 audit
4
-
5
- ## Problem
6
-
7
- IDML has full footnote support:
8
- - `<Footnote>` elements in Stories (inline footnote markers + text)
9
- - `<FootnoteOption>` on PSR (numbering rules, layout)
10
- - `TextFramePreference.FootnotesEnableOverrides`,
11
- `FootnotesSpanAcrossColumns`, `FootnotesMinimumSpacing`,
12
- `FootnotesSpaceBetween` — frame-level footnote layout
13
- - `<TextFrameFootnoteOptionsObject>` — per-frame footnote config
14
-
15
- The renderer doesn't model footnote content or lay it out.
16
- Documents with footnotes (academic, legal, technical) currently
17
- lose footnote text in the PDF.
18
-
19
- ## What needs to happen
20
-
21
- 1. Model `<Footnote>` element (story-embedded footnote text +
22
- reference marker).
23
- 2. Story parsing extracts footnotes alongside main text.
24
- 3. Footnote text renders at the bottom of the page that contains
25
- the reference marker.
26
- 4. Footnote numbering rules from FootnoteOption honored.
27
- 5. Continuous footnotes flow across pages.
28
-
29
- This is a large feature — defer until needed by a real fixture.
3
+ ## Status: COMPLETEimplemented 2026-08-16
4
+
5
+ ## What was built
6
+
7
+ - `Elements::Footnote` schema-faithful model of `Footnote_Object`
8
+ (Story.rnc): no attributes, PSR/CSR children carrying the footnote
9
+ text. Wired into `CharacterStyleRange` and `StoryInner` (`footnote`
10
+ collection). Footnote text is excluded from `text_content` so it
11
+ never leaks into body text.
12
+ - `Render::Footnote` — footnote semantics unit (MECE):
13
+ - `Counter`: story-scoped sequential numbering, honoring
14
+ FootnoteOption `StartAt`.
15
+ - `marker_run`: superscript marker emitted into the body text at the
16
+ anchoring CSR's position; inherits the CSR's character styling.
17
+ - `extract`: footnote paragraphs via StyleResolver with the marker
18
+ text (`Prefix + number + Suffix`) prefixed to the first run.
19
+ - `layout_entries` / `reserved_height`: one shared Shaper →
20
+ LineBreaker → VerticalLayout walk drives both height measurement
21
+ and rendering, so the reserved area always matches what's drawn.
22
+ - `emit_separator`: separator rule honoring FootnoteOption RuleOn /
23
+ RuleLineWeight / RuleWidth / RuleLeftIndent.
24
+ - `TextFrameRenderer`: marker runs are collected during body layout
25
+ (`register_footnote`), each raising the effective bottom limit so
26
+ body text avoids the footnote area (incremental reservation,
27
+ InDesign-style); after body layout, `render_footnote_entries` draws
28
+ the separator and the footnote paragraphs above the frame's content
29
+ bottom. Per-column in multi-column frames.
30
+ - `FootnoteOption` (Preferences.xml) honored: StartAt, Prefix, Suffix,
31
+ RuleOn, RuleLineWeight, RuleWidth, RuleLeftIndent, Spacer
32
+ (separator gap), SpaceBetween (inter-entry gap).
33
+
34
+ ## Known limitations
35
+
36
+ - Numbering restarts per story; document-wide continuous numbering
37
+ and per-section restart are not modeled.
38
+ - Footnote overflow balancing (a footnote too tall for its frame
39
+ moving wholly to the next frame, NoSplitting) is not modeled.
40
+ - The simple-render fallback (no font metrics) shows markers inline
41
+ but does not render footnote text at the frame bottom.
42
+ - Footnotes nested in CSRs inside CSRs (CSR > CSR > Footnote) are
43
+ not extracted; only direct CSR children emit markers.
30
44
 
31
45
  ## Acceptance criteria
32
46
 
33
- - [ ] Footnote text rendered at bottom of containing page.
34
- - [ ] Footnote marker in body text links to footnote text.
35
- - [ ] Numbering restarts per document section per FootnoteOption.
36
-
37
- ## Dependencies
38
-
39
- - TODO 108 (multi-frame story flow) — footnotes need to know which
40
- page a reference appears on.
47
+ - [x] Footnote text rendered at bottom of containing frame.
48
+ - [x] Footnote marker in body text (superscript, numbered).
49
+ - [x] Numbering honors FootnoteOption StartAt / Prefix / Suffix
50
+ (per-story scope).
@@ -300,6 +300,17 @@ module Idml
300
300
  collection: true
301
301
  attribute :table, Idml::Elements::Table, collection: true
302
302
  attribute :xml_element, Idml::Elements::XmlElement, collection: true
303
+ attribute :footnote, Idml::Elements::Footnote, collection: true
304
+ # Anchored page items embedded in the story flow. Their
305
+ # AnchoredObjectSetting (child of each item) declares how the
306
+ # item anchors to the surrounding text.
307
+ attribute :rectangle, Idml::Elements::Rectangle, collection: true
308
+ attribute :oval, Idml::Elements::Oval, collection: true
309
+ attribute :polygon, Idml::Elements::Polygon, collection: true
310
+ attribute :graphic_line, Idml::Elements::GraphicLine,
311
+ collection: true
312
+ attribute :group, Idml::Elements::Group, collection: true
313
+ attribute :text_frame, Idml::Elements::TextFrame, collection: true
303
314
 
304
315
  xml do
305
316
  root "CharacterStyleRange"
@@ -655,6 +666,13 @@ module Idml
655
666
  map_element "HyperlinkTextSource", to: :hyperlink_text_source
656
667
  map_element "Table", to: :table
657
668
  map_element "XMLElement", to: :xml_element
669
+ map_element "Footnote", to: :footnote
670
+ map_element "Rectangle", to: :rectangle
671
+ map_element "Oval", to: :oval
672
+ map_element "Polygon", to: :polygon
673
+ map_element "GraphicLine", to: :graphic_line
674
+ map_element "Group", to: :group
675
+ map_element "TextFrame", to: :text_frame
658
676
  end
659
677
 
660
678
  def text_content
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Elements
5
+ # `<Footnote>` — story-embedded footnote content. Schema-faithful
6
+ # model of `Footnote_Object` in
7
+ # `reference-docs/schemas/package/Stories/Story.rnc`: the element
8
+ # declares no attributes; its children are HiddenText /
9
+ # GaijiOwnedItemObject / Table / TextVariableInstance /
10
+ # ParagraphStyleRange / CharacterStyleRange. Of these, the
11
+ # ParagraphStyleRange (with its CSR children) and
12
+ # CharacterStyleRange children carry the footnote text; the rest
13
+ # are parsed by their own models where relevant.
14
+ #
15
+ # A Footnote sits inside the CharacterStyleRange that owns the
16
+ # anchor position: the body-text marker renders where the CSR's
17
+ # text ends, and the Footnote's paragraphs render at the bottom
18
+ # of the frame holding that marker (see Render::Footnote).
19
+ class Footnote < Lutaml::Model::Serializable
20
+ attribute :paragraph_style_range, Idml::Elements::ParagraphStyleRange,
21
+ collection: true
22
+ attribute :character_style_range, Idml::Elements::CharacterStyleRange,
23
+ collection: true
24
+
25
+ xml do
26
+ root "Footnote"
27
+ map_element "ParagraphStyleRange", to: :paragraph_style_range
28
+ map_element "CharacterStyleRange", to: :character_style_range
29
+ end
30
+ end
31
+ end
32
+ end
@@ -19,6 +19,8 @@ module Idml
19
19
  attribute :properties, Idml::Elements::Properties, collection: true
20
20
  attribute :transparency_setting, Idml::Elements::TransparencySetting
21
21
  attribute :text_wrap_preference, Idml::Elements::TextWrapPreference
22
+ attribute :anchored_object_setting,
23
+ Idml::Elements::AnchoredObjectSetting
22
24
 
23
25
  xml do
24
26
  root "GraphicLine"
@@ -36,6 +38,7 @@ module Idml
36
38
  map_element "Properties", to: :properties
37
39
  map_element "TransparencySetting", to: :transparency_setting
38
40
  map_element "TextWrapPreference", to: :text_wrap_preference
41
+ map_element "AnchoredObjectSetting", to: :anchored_object_setting
39
42
  end
40
43
 
41
44
  def geometric_bounds
@@ -10,6 +10,8 @@ module Idml
10
10
  attribute :item_transform, :string
11
11
  attribute :visible, :boolean
12
12
  attribute :item_layer, :string
13
+ attribute :anchored_object_setting,
14
+ Idml::Elements::AnchoredObjectSetting
13
15
  attribute :rectangle, Idml::Elements::Rectangle, collection: true
14
16
  attribute :text_frame, Idml::Elements::TextFrame, collection: true
15
17
  attribute :polygon, Idml::Elements::Polygon, collection: true
@@ -22,6 +24,7 @@ module Idml
22
24
  map_attribute "ItemTransform", to: :item_transform
23
25
  map_attribute "Visible", to: :visible
24
26
  map_attribute "ItemLayer", to: :item_layer
27
+ map_element "AnchoredObjectSetting", to: :anchored_object_setting
25
28
  map_element "Rectangle", to: :rectangle
26
29
  map_element "TextFrame", to: :text_frame
27
30
  map_element "Polygon", to: :polygon
@@ -30,6 +30,8 @@ module Idml
30
30
  attribute :properties, Idml::Elements::Properties, collection: true
31
31
  attribute :transparency_setting, Idml::Elements::TransparencySetting
32
32
  attribute :text_wrap_preference, Idml::Elements::TextWrapPreference
33
+ attribute :anchored_object_setting,
34
+ Idml::Elements::AnchoredObjectSetting
33
35
  attribute :image, Idml::Elements::Image, collection: true
34
36
 
35
37
  xml do
@@ -52,6 +54,7 @@ module Idml
52
54
  map_element "Properties", to: :properties
53
55
  map_element "TransparencySetting", to: :transparency_setting
54
56
  map_element "TextWrapPreference", to: :text_wrap_preference
57
+ map_element "AnchoredObjectSetting", to: :anchored_object_setting
55
58
  map_element "Image", to: :image
56
59
  end
57
60
 
@@ -26,6 +26,8 @@ module Idml
26
26
  attribute :properties, Idml::Elements::Properties, collection: true
27
27
  attribute :transparency_setting, Idml::Elements::TransparencySetting
28
28
  attribute :text_wrap_preference, Idml::Elements::TextWrapPreference
29
+ attribute :anchored_object_setting,
30
+ Idml::Elements::AnchoredObjectSetting
29
31
 
30
32
  xml do
31
33
  root "Path"
@@ -43,6 +45,7 @@ module Idml
43
45
  map_element "Properties", to: :properties
44
46
  map_element "TransparencySetting", to: :transparency_setting
45
47
  map_element "TextWrapPreference", to: :text_wrap_preference
48
+ map_element "AnchoredObjectSetting", to: :anchored_object_setting
46
49
  end
47
50
 
48
51
  def geometric_bounds
@@ -21,6 +21,8 @@ module Idml
21
21
  attribute :properties, Idml::Elements::Properties, collection: true
22
22
  attribute :transparency_setting, Idml::Elements::TransparencySetting
23
23
  attribute :text_wrap_preference, Idml::Elements::TextWrapPreference
24
+ attribute :anchored_object_setting,
25
+ Idml::Elements::AnchoredObjectSetting
24
26
  attribute :image, Idml::Elements::Image, collection: true
25
27
 
26
28
  xml do
@@ -41,6 +43,7 @@ module Idml
41
43
  map_element "Properties", to: :properties
42
44
  map_element "TransparencySetting", to: :transparency_setting
43
45
  map_element "TextWrapPreference", to: :text_wrap_preference
46
+ map_element "AnchoredObjectSetting", to: :anchored_object_setting
44
47
  map_element "Image", to: :image
45
48
  end
46
49
 
@@ -25,6 +25,8 @@ module Idml
25
25
  attribute :properties, Idml::Elements::Properties, collection: true
26
26
  attribute :transparency_setting, Idml::Elements::TransparencySetting
27
27
  attribute :text_wrap_preference, Idml::Elements::TextWrapPreference
28
+ attribute :anchored_object_setting,
29
+ Idml::Elements::AnchoredObjectSetting
28
30
  attribute :image, Idml::Elements::Image, collection: true
29
31
 
30
32
  xml do
@@ -48,6 +50,7 @@ module Idml
48
50
  map_element "Properties", to: :properties
49
51
  map_element "TransparencySetting", to: :transparency_setting
50
52
  map_element "TextWrapPreference", to: :text_wrap_preference
53
+ map_element "AnchoredObjectSetting", to: :anchored_object_setting
51
54
  map_element "Image", to: :image
52
55
  end
53
56
 
@@ -312,6 +312,7 @@ module Idml
312
312
  collection: true
313
313
  attribute :content, Idml::Elements::Content, collection: true
314
314
  attribute :xml_element, Idml::Elements::XmlElement, collection: true
315
+ attribute :footnote, Idml::Elements::Footnote, collection: true
315
316
 
316
317
  xml do
317
318
  root "Story"
@@ -676,6 +677,7 @@ module Idml
676
677
  map_element "CharacterStyleRange", to: :character_style_range
677
678
  map_element "Content", to: :content
678
679
  map_element "XMLElement", to: :xml_element
680
+ map_element "Footnote", to: :footnote
679
681
  end
680
682
 
681
683
  def text_content
data/lib/idml/elements.rb CHANGED
@@ -54,6 +54,7 @@ module Idml
54
54
  "idml/elements/flex_layout_attribute_option"
55
55
  autoload :Font, "idml/elements/font"
56
56
  autoload :FontFamily, "idml/elements/font_family"
57
+ autoload :Footnote, "idml/elements/footnote"
57
58
  autoload :FootnoteOption, "idml/elements/pref_footnote_option"
58
59
  autoload :FrameFittingOption, "idml/elements/frame_fitting_option"
59
60
  autoload :GraphicLine, "idml/elements/graphic_line"
@@ -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