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 +4 -4
- data/TODO.pdf/101-paragraph-rules-borders.md +36 -39
- data/TODO.pdf/106-page-item-type-coverage.md +37 -7
- data/lib/idml/elements/character_style_range.rb +2 -0
- data/lib/idml/elements/paragraph_style_range.rb +2 -0
- data/lib/idml/elements/properties.rb +16 -3
- data/lib/idml/elements/typed_value.rb +20 -0
- data/lib/idml/elements.rb +1 -0
- data/lib/idml/render/contour.rb +110 -0
- data/lib/idml/render/paragraph_rules.rb +125 -0
- data/lib/idml/render/renderers/oval_renderer.rb +5 -6
- data/lib/idml/render/renderers/path_renderer.rb +5 -6
- data/lib/idml/render/renderers/polygon_renderer.rb +6 -37
- data/lib/idml/render/renderers/rectangle_renderer.rb +5 -94
- data/lib/idml/render/renderers/text_frame_renderer.rb +24 -0
- data/lib/idml/render/shape_paint.rb +172 -0
- data/lib/idml/render/style_resolver.rb +84 -2
- data/lib/idml/render.rb +2 -0
- data/lib/idml/text_engine/measurement.rb +40 -0
- data/lib/idml/text_engine.rb +2 -1
- data/lib/idml/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff487a296f9cbd5b926b963c7acb7d4864d43060ffed5b1ec7438a75bac1446a
|
|
4
|
+
data.tar.gz: f4926fe3add1833a954628aace986839f101722deafde891783c54afdd509a58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
- [
|
|
41
|
-
|
|
42
|
-
- [
|
|
43
|
-
- [
|
|
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:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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
|
-
|
|
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
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
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:
|
|
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:
|
|
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
|
data/lib/idml/text_engine.rb
CHANGED
|
@@ -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,
|
|
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
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.
|
|
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-
|
|
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
|