idml 0.8.8 → 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 +4 -4
- data/TODO.pdf/106-page-item-type-coverage.md +37 -7
- data/lib/idml/render/contour.rb +110 -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/shape_paint.rb +172 -0
- data/lib/idml/render.rb +2 -0
- data/lib/idml/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d58f00746f6afb4233e161dfa9cdeb116d714eed0c238ec543a0f039b4167f0e
|
|
4
|
+
data.tar.gz: 1a000e34996525fe34cdc441625c9eaa303f121c836ae3f3b07b73c12712abb1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15001cbf1427dc278c701cd4980b837ff4b015f6c5657eeb5c88c0c8774933934e606439c40fa3c900368ef2959fc1170cd12841a9caadc0c025a199dd18cd68
|
|
7
|
+
data.tar.gz: 41f45b1b2db245e1dd2d5ef40aa60990fc3c957930b3d16a633974d1f267149e8b2c5cf638b7b3549b4318bf8ea62f378fbb5d0f98ad32d4d50fbce0e3cb9505
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
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"
|
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.8.
|
|
4
|
+
version: 0.8.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|
|
@@ -439,6 +439,7 @@ files:
|
|
|
439
439
|
- lib/idml/render/color_helper.rb
|
|
440
440
|
- lib/idml/render/color_resolver.rb
|
|
441
441
|
- lib/idml/render/condition_filter.rb
|
|
442
|
+
- lib/idml/render/contour.rb
|
|
442
443
|
- lib/idml/render/drop_cap.rb
|
|
443
444
|
- lib/idml/render/font_reference_resolver.rb
|
|
444
445
|
- lib/idml/render/font_setup.rb
|
|
@@ -469,6 +470,7 @@ files:
|
|
|
469
470
|
- lib/idml/render/renderers/rectangle_renderer.rb
|
|
470
471
|
- lib/idml/render/renderers/table_renderer.rb
|
|
471
472
|
- lib/idml/render/renderers/text_frame_renderer.rb
|
|
473
|
+
- lib/idml/render/shape_paint.rb
|
|
472
474
|
- lib/idml/render/spread_renderer.rb
|
|
473
475
|
- lib/idml/render/story_chain_controller.rb
|
|
474
476
|
- lib/idml/render/story_threader.rb
|