idml 0.2.5 → 0.2.7
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/Gemfile.lock +2 -2
- data/TODO.pdf/40-font-ps-name-resolution.md +40 -28
- data/TODO.pdf/52-font-subsetting.md +34 -4
- data/TODO.pdf/55-dead-code-deprecation.md +15 -21
- data/TODO.pdf/63-replace-fontmetrics-with-pdfrb.md +60 -18
- data/TODO.pdf/65-pdfrb-019-integration.md +16 -16
- data/TODO.pdf/67-text-rich-multi-run.md +51 -0
- data/TODO.pdf/68-radial-gradient-shadings.md +36 -0
- data/TODO.pdf/69-transparency-blend-modes.md +84 -0
- data/TODO.pdf/70-stroke-styling.md +32 -0
- data/TODO.pdf/71-shared-placement-module.md +41 -0
- data/TODO.pdf/72-stroke-styling.md +55 -0
- data/TODO.pdf/73-gradient-resolver-cleanup.md +48 -0
- data/TODO.pdf/74-renderer-spec-coverage.md +53 -0
- data/lib/idml/elements/graphic_line.rb +10 -0
- data/lib/idml/elements/polygon.rb +10 -0
- data/lib/idml/elements/rectangle.rb +10 -0
- data/lib/idml/elements/text_frame.rb +2 -0
- data/lib/idml/elements/transparency_setting.rb +25 -0
- data/lib/idml/render/blending.rb +60 -0
- data/lib/idml/render/gradient_resolver.rb +10 -102
- data/lib/idml/render/placement.rb +31 -0
- data/lib/idml/render/renderers/graphic_line_renderer.rb +10 -8
- data/lib/idml/render/renderers/polygon_renderer.rb +9 -7
- data/lib/idml/render/renderers/rectangle_renderer.rb +43 -35
- data/lib/idml/render/renderers/table_renderer.rb +1 -1
- data/lib/idml/render/renderers/text_frame_renderer.rb +1 -9
- data/lib/idml/render/stroke_style.rb +97 -0
- data/lib/idml/render.rb +3 -0
- data/lib/idml/version.rb +1 -1
- metadata +12 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# TODO PDF 73: Drop dead GradientResolver approximation code
|
|
2
|
+
|
|
3
|
+
## Status: DONE
|
|
4
|
+
|
|
5
|
+
## What was removed
|
|
6
|
+
|
|
7
|
+
`Idml::Render::GradientResolver` previously carried a 32-rectangle
|
|
8
|
+
discrete-approximation renderer (`render_gradient`, `interpolate`,
|
|
9
|
+
`blend_between`, `blend`, and the `SEGMENTS = 32` constant). The
|
|
10
|
+
discrete approximation was replaced by real PDF shadings in TODO 66
|
|
11
|
+
(pdfrb 0.4.0 `Shadings#add_axial`) and TODO 68 (`add_radial`); the
|
|
12
|
+
old code became dead.
|
|
13
|
+
|
|
14
|
+
The class also had a `build(graphic)` factory that built a lookup
|
|
15
|
+
table no consumer reads.
|
|
16
|
+
|
|
17
|
+
## What was kept
|
|
18
|
+
|
|
19
|
+
- `GradientResolver.gradient?(name)` — pure predicate (`name` is a
|
|
20
|
+
`Gradient/*` reference). Still used by `RectangleRenderer#render_fill`
|
|
21
|
+
to choose between gradient and solid color paths.
|
|
22
|
+
|
|
23
|
+
The module is now a 4-line pure predicate. Every other responsibility
|
|
24
|
+
moved to `RectangleRenderer`'s direct `Shadings#add_axial` /
|
|
25
|
+
`add_radial` calls.
|
|
26
|
+
|
|
27
|
+
## Why
|
|
28
|
+
|
|
29
|
+
Dead code accumulates. Three reasons to drop it:
|
|
30
|
+
|
|
31
|
+
1. **Confusion** — readers see `SEGMENTS = 32` and assume the discrete
|
|
32
|
+
approximation is still in use somewhere.
|
|
33
|
+
2. **Maintenance** — every time `ColorResolver` or the gradient model
|
|
34
|
+
changed, the dead code needed reading to confirm it didn't need
|
|
35
|
+
updating.
|
|
36
|
+
3. **Test surface** — the dead `render_gradient` had its own spec
|
|
37
|
+
using `instance_double(Idml::Parts::Graphic)`, violating the
|
|
38
|
+
no-doubles rule. Removing the method let us delete the offending
|
|
39
|
+
spec.
|
|
40
|
+
|
|
41
|
+
## Acceptance criteria
|
|
42
|
+
|
|
43
|
+
- [x] `GradientResolver` exposes only `.gradient?`.
|
|
44
|
+
- [x] No `render_gradient`, `interpolate`, `blend_between`, `blend`,
|
|
45
|
+
`SEGMENTS`, or `build` in the module.
|
|
46
|
+
- [x] Renderer code unchanged in behavior (still routes gradient fills
|
|
47
|
+
to pdfrb Shadings).
|
|
48
|
+
- [x] `bundle exec rake` green.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# TODO PDF 74: Renderer spec coverage
|
|
2
|
+
|
|
3
|
+
## Status: DONE
|
|
4
|
+
|
|
5
|
+
## What was implemented
|
|
6
|
+
|
|
7
|
+
Dedicated spec files for every renderer that previously had none.
|
|
8
|
+
Each spec uses real `Idml::Elements::*` instances (parsed from XML
|
|
9
|
+
fragments) or lightweight `Struct` doubles that mirror the renderer's
|
|
10
|
+
interface — no RSpec `double()` or `instance_double` anywhere.
|
|
11
|
+
|
|
12
|
+
New spec files:
|
|
13
|
+
|
|
14
|
+
- `spec/idml/render/polygon_renderer_spec.rb` — 4 examples covering
|
|
15
|
+
visibility, missing bounds, fill rendering, and stroke + cap.
|
|
16
|
+
- `spec/idml/render/graphic_line_renderer_spec.rb` — 4 examples
|
|
17
|
+
covering unstrokable skip, color-miss skip, basic line drawing,
|
|
18
|
+
and StrokeStyle application.
|
|
19
|
+
- `spec/idml/render/group_renderer_spec.rb` — 4 examples covering
|
|
20
|
+
empty group, child dispatch, item_transform concat, and
|
|
21
|
+
layer_filter visibility.
|
|
22
|
+
- `spec/idml/render/table_renderer_spec.rb` — 4 examples covering
|
|
23
|
+
visibility skip, empty rows skip, per-cell rectangle count, and
|
|
24
|
+
stroke emission.
|
|
25
|
+
- `spec/idml/render/text_frame_renderer_spec.rb` — 3 examples
|
|
26
|
+
covering no-story skip, non-chain-head skip, and end-to-end BT/ET
|
|
27
|
+
emission against the sample-with-image fixture.
|
|
28
|
+
- `spec/idml/render/blending_spec.rb` — 11 examples (added earlier
|
|
29
|
+
in TODO 69).
|
|
30
|
+
- `spec/idml/render/stroke_style_spec.rb` — 13 examples (added
|
|
31
|
+
earlier in TODO 72).
|
|
32
|
+
- `spec/idml/render/rectangle_renderer_spec.rb` — 6 examples (added
|
|
33
|
+
earlier in TODO 68).
|
|
34
|
+
|
|
35
|
+
## Cleanups
|
|
36
|
+
|
|
37
|
+
- `render_helpers_spec.rb` — removed the `#render_gradient` test that
|
|
38
|
+
used `instance_double(Idml::Parts::Graphic)` (TODO 73).
|
|
39
|
+
- `rectangle_renderer_spec.rb` — replaced
|
|
40
|
+
`canvas.document.shadings.instance_variable_get(:@registry)` with
|
|
41
|
+
the public `shadings.registry` reader.
|
|
42
|
+
- `designmap_spec.rb` — replaced `parsed.send(actual_attr(attr))`
|
|
43
|
+
with an explicit case/when reader.
|
|
44
|
+
|
|
45
|
+
## Acceptance criteria
|
|
46
|
+
|
|
47
|
+
- [x] Every renderer in `lib/idml/render/renderers/` has a dedicated
|
|
48
|
+
spec file with at least 3 examples.
|
|
49
|
+
- [x] No `instance_double` or `double()` in render specs.
|
|
50
|
+
- [x] No `instance_variable_get` / `instance_variable_set` in render
|
|
51
|
+
specs.
|
|
52
|
+
- [x] No `.send(...)` in any spec.
|
|
53
|
+
- [x] `bundle exec rake` green (2350+ examples, 0 failures).
|
|
@@ -10,9 +10,14 @@ module Idml
|
|
|
10
10
|
attribute :stroke_color, :string
|
|
11
11
|
attribute :stroke_weight, :float
|
|
12
12
|
attribute :stroke_tint, :float
|
|
13
|
+
attribute :end_cap, :string
|
|
14
|
+
attribute :end_join, :string
|
|
15
|
+
attribute :miter_limit, :float
|
|
16
|
+
attribute :stroke_dash_and_gap, :string
|
|
13
17
|
attribute :visible, :boolean
|
|
14
18
|
attribute :item_layer, :string
|
|
15
19
|
attribute :properties, Idml::Elements::Properties, collection: true
|
|
20
|
+
attribute :transparency_setting, Idml::Elements::TransparencySetting
|
|
16
21
|
|
|
17
22
|
xml do
|
|
18
23
|
root "GraphicLine"
|
|
@@ -21,9 +26,14 @@ module Idml
|
|
|
21
26
|
map_attribute "StrokeColor", to: :stroke_color
|
|
22
27
|
map_attribute "StrokeWeight", to: :stroke_weight
|
|
23
28
|
map_attribute "StrokeTint", to: :stroke_tint
|
|
29
|
+
map_attribute "EndCap", to: :end_cap
|
|
30
|
+
map_attribute "EndJoin", to: :end_join
|
|
31
|
+
map_attribute "MiterLimit", to: :miter_limit
|
|
32
|
+
map_attribute "StrokeDashAndGap", to: :stroke_dash_and_gap
|
|
24
33
|
map_attribute "Visible", to: :visible
|
|
25
34
|
map_attribute "ItemLayer", to: :item_layer
|
|
26
35
|
map_element "Properties", to: :properties
|
|
36
|
+
map_element "TransparencySetting", to: :transparency_setting
|
|
27
37
|
end
|
|
28
38
|
|
|
29
39
|
def geometric_bounds
|
|
@@ -12,9 +12,14 @@ module Idml
|
|
|
12
12
|
attribute :fill_tint, :float
|
|
13
13
|
attribute :stroke_color, :string
|
|
14
14
|
attribute :stroke_weight, :float
|
|
15
|
+
attribute :end_cap, :string
|
|
16
|
+
attribute :end_join, :string
|
|
17
|
+
attribute :miter_limit, :float
|
|
18
|
+
attribute :stroke_dash_and_gap, :string
|
|
15
19
|
attribute :visible, :boolean
|
|
16
20
|
attribute :item_layer, :string
|
|
17
21
|
attribute :properties, Idml::Elements::Properties, collection: true
|
|
22
|
+
attribute :transparency_setting, Idml::Elements::TransparencySetting
|
|
18
23
|
attribute :image, Idml::Elements::Image, collection: true
|
|
19
24
|
|
|
20
25
|
xml do
|
|
@@ -26,9 +31,14 @@ module Idml
|
|
|
26
31
|
map_attribute "FillTint", to: :fill_tint
|
|
27
32
|
map_attribute "StrokeColor", to: :stroke_color
|
|
28
33
|
map_attribute "StrokeWeight", to: :stroke_weight
|
|
34
|
+
map_attribute "EndCap", to: :end_cap
|
|
35
|
+
map_attribute "EndJoin", to: :end_join
|
|
36
|
+
map_attribute "MiterLimit", to: :miter_limit
|
|
37
|
+
map_attribute "StrokeDashAndGap", to: :stroke_dash_and_gap
|
|
29
38
|
map_attribute "Visible", to: :visible
|
|
30
39
|
map_attribute "ItemLayer", to: :item_layer
|
|
31
40
|
map_element "Properties", to: :properties
|
|
41
|
+
map_element "TransparencySetting", to: :transparency_setting
|
|
32
42
|
map_element "Image", to: :image
|
|
33
43
|
end
|
|
34
44
|
|
|
@@ -15,10 +15,15 @@ module Idml
|
|
|
15
15
|
attribute :stroke_color, :string
|
|
16
16
|
attribute :stroke_weight, :float
|
|
17
17
|
attribute :stroke_tint, :float
|
|
18
|
+
attribute :end_cap, :string
|
|
19
|
+
attribute :end_join, :string
|
|
20
|
+
attribute :miter_limit, :float
|
|
21
|
+
attribute :stroke_dash_and_gap, :string
|
|
18
22
|
attribute :visible, :boolean
|
|
19
23
|
attribute :name, :string
|
|
20
24
|
attribute :item_layer, :string
|
|
21
25
|
attribute :properties, Idml::Elements::Properties, collection: true
|
|
26
|
+
attribute :transparency_setting, Idml::Elements::TransparencySetting
|
|
22
27
|
attribute :image, Idml::Elements::Image, collection: true
|
|
23
28
|
|
|
24
29
|
xml do
|
|
@@ -32,10 +37,15 @@ module Idml
|
|
|
32
37
|
map_attribute "StrokeColor", to: :stroke_color
|
|
33
38
|
map_attribute "StrokeWeight", to: :stroke_weight
|
|
34
39
|
map_attribute "StrokeTint", to: :stroke_tint
|
|
40
|
+
map_attribute "EndCap", to: :end_cap
|
|
41
|
+
map_attribute "EndJoin", to: :end_join
|
|
42
|
+
map_attribute "MiterLimit", to: :miter_limit
|
|
43
|
+
map_attribute "StrokeDashAndGap", to: :stroke_dash_and_gap
|
|
35
44
|
map_attribute "Visible", to: :visible
|
|
36
45
|
map_attribute "Name", to: :name
|
|
37
46
|
map_attribute "ItemLayer", to: :item_layer
|
|
38
47
|
map_element "Properties", to: :properties
|
|
48
|
+
map_element "TransparencySetting", to: :transparency_setting
|
|
39
49
|
map_element "Image", to: :image
|
|
40
50
|
end
|
|
41
51
|
|
|
@@ -20,6 +20,7 @@ module Idml
|
|
|
20
20
|
attribute :name, :string
|
|
21
21
|
attribute :item_layer, :string
|
|
22
22
|
attribute :properties, Idml::Elements::Properties, collection: true
|
|
23
|
+
attribute :transparency_setting, Idml::Elements::TransparencySetting
|
|
23
24
|
attribute :text_frame_preference, Idml::Elements::TextFramePreference,
|
|
24
25
|
collection: true
|
|
25
26
|
|
|
@@ -39,6 +40,7 @@ module Idml
|
|
|
39
40
|
map_attribute "Name", to: :name
|
|
40
41
|
map_attribute "ItemLayer", to: :item_layer
|
|
41
42
|
map_element "Properties", to: :properties
|
|
43
|
+
map_element "TransparencySetting", to: :transparency_setting
|
|
42
44
|
map_element "TextFramePreference", to: :text_frame_preference
|
|
43
45
|
end
|
|
44
46
|
|
|
@@ -7,8 +7,33 @@ module Idml
|
|
|
7
7
|
# `reference-docs/schemas/package/Resources/Styles.rnc` is declared (0 attributes),
|
|
8
8
|
# generated by `scripts/rnc_to_lutaml.rb`.
|
|
9
9
|
class TransparencySetting < Lutaml::Model::Serializable
|
|
10
|
+
attribute :blending_setting, Idml::Elements::BlendingSetting
|
|
11
|
+
attribute :drop_shadow_setting, Idml::Elements::DropShadowSetting
|
|
12
|
+
attribute :feather_setting, Idml::Elements::FeatherSetting
|
|
13
|
+
attribute :inner_shadow_setting, Idml::Elements::InnerShadowSetting
|
|
14
|
+
attribute :outer_glow_setting, Idml::Elements::OuterGlowSetting
|
|
15
|
+
attribute :inner_glow_setting, Idml::Elements::InnerGlowSetting
|
|
16
|
+
attribute :bevel_and_emboss_setting, Idml::Elements::BevelAndEmbossSetting
|
|
17
|
+
attribute :satin_setting, Idml::Elements::SatinSetting
|
|
18
|
+
attribute :directional_feather_setting,
|
|
19
|
+
Idml::Elements::DirectionalFeatherSetting
|
|
20
|
+
attribute :gradient_feather_setting,
|
|
21
|
+
Idml::Elements::GradientFeatherSetting
|
|
22
|
+
|
|
10
23
|
xml do
|
|
11
24
|
root "TransparencySetting"
|
|
25
|
+
map_element "BlendingSetting", to: :blending_setting
|
|
26
|
+
map_element "DropShadowSetting", to: :drop_shadow_setting
|
|
27
|
+
map_element "FeatherSetting", to: :feather_setting
|
|
28
|
+
map_element "InnerShadowSetting", to: :inner_shadow_setting
|
|
29
|
+
map_element "OuterGlowSetting", to: :outer_glow_setting
|
|
30
|
+
map_element "InnerGlowSetting", to: :inner_glow_setting
|
|
31
|
+
map_element "BevelAndEmbossSetting", to: :bevel_and_emboss_setting
|
|
32
|
+
map_element "SatinSetting", to: :satin_setting
|
|
33
|
+
map_element "DirectionalFeatherSetting",
|
|
34
|
+
to: :directional_feather_setting
|
|
35
|
+
map_element "GradientFeatherSetting",
|
|
36
|
+
to: :gradient_feather_setting
|
|
12
37
|
end
|
|
13
38
|
end
|
|
14
39
|
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Render
|
|
5
|
+
# Maps an `Elements::BlendingSetting` to the keyword arguments that
|
|
6
|
+
# `Pdfrb::Content::Canvas#with_transparency` expects. IDML stores
|
|
7
|
+
# opacity as 0–100 (percentage); pdfrb wants 0.0–1.0 alpha. Blend
|
|
8
|
+
# mode names line up directly with PDF `BM` names; unknown modes
|
|
9
|
+
# fall back to `Normal`.
|
|
10
|
+
module Blending
|
|
11
|
+
PDF_BLEND_MODES = %w[
|
|
12
|
+
Normal Multiply Screen Overlay SoftLight HardLight Darken Lighten
|
|
13
|
+
ColorDodge ColorBurn Difference Exclusion Hue Saturation Color
|
|
14
|
+
Luminosity
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
def self.args_for(setting)
|
|
18
|
+
return nil unless setting&.blending_setting
|
|
19
|
+
|
|
20
|
+
build_args(setting.blending_setting)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.build_args(blending)
|
|
24
|
+
opacity = normalize_opacity(blending.opacity)
|
|
25
|
+
mode = normalize_mode(blending.blend_mode)
|
|
26
|
+
return nil unless opacity || mode
|
|
27
|
+
|
|
28
|
+
{ opacity: opacity || 1.0, blend_mode: mode || "Normal" }
|
|
29
|
+
end
|
|
30
|
+
private_class_method :build_args
|
|
31
|
+
|
|
32
|
+
# Wraps the given block in `Canvas#with_transparency` when the
|
|
33
|
+
# page item has a BlendingSetting, otherwise yields directly.
|
|
34
|
+
def self.wrap(canvas, setting, &)
|
|
35
|
+
args = args_for(setting)
|
|
36
|
+
if args
|
|
37
|
+
canvas.with_transparency(**args, &)
|
|
38
|
+
else
|
|
39
|
+
yield
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.normalize_opacity(raw)
|
|
44
|
+
return nil if raw.nil?
|
|
45
|
+
|
|
46
|
+
(raw.to_f / 100.0).clamp(0.0, 1.0)
|
|
47
|
+
end
|
|
48
|
+
private_class_method :normalize_opacity
|
|
49
|
+
|
|
50
|
+
def self.normalize_mode(raw)
|
|
51
|
+
return nil if raw.nil? || raw == "Normal"
|
|
52
|
+
|
|
53
|
+
camel = raw.to_s.dup
|
|
54
|
+
camel[0] = camel[0].upcase
|
|
55
|
+
PDF_BLEND_MODES.include?(camel) ? camel : nil
|
|
56
|
+
end
|
|
57
|
+
private_class_method :normalize_mode
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -2,111 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module Idml
|
|
4
4
|
module Render
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return new(table) unless graphic&.gradient
|
|
16
|
-
|
|
17
|
-
graphic.gradient.each do |gradient|
|
|
18
|
-
table[gradient.self_attr] = gradient
|
|
19
|
-
end
|
|
20
|
-
new(table)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def initialize(table)
|
|
24
|
-
@table = table
|
|
25
|
-
@color_resolver = nil
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Detect if a color reference is a gradient.
|
|
5
|
+
# Identifies IDML gradient color references and exposes the
|
|
6
|
+
# gradient catalogue from `Resources/Graphic.xml`.
|
|
7
|
+
#
|
|
8
|
+
# The discrete-rectangle approximation that this module used to
|
|
9
|
+
# carry was removed once pdfrb 0.4.0 introduced real PDF shadings
|
|
10
|
+
# (TODOs 49, 66, 68). Today the renderers go through pdfrb's
|
|
11
|
+
# `Shadings#add_axial` / `add_radial` directly; this module's
|
|
12
|
+
# only remaining responsibility is the predicate that distinguishes
|
|
13
|
+
# gradient colour references from solid ones.
|
|
14
|
+
module GradientResolver
|
|
29
15
|
def self.gradient?(name)
|
|
30
16
|
name.is_a?(String) && name.start_with?("Gradient/")
|
|
31
17
|
end
|
|
32
|
-
|
|
33
|
-
# Render a gradient fill as PDF operators. Returns nil if the
|
|
34
|
-
# color is not a gradient. The returned operators emit colored
|
|
35
|
-
# rectangles from y2 down to y1 (matching PDF Y-up coords).
|
|
36
|
-
def render_gradient(name, x:, y:, width:, height:, color_resolver:)
|
|
37
|
-
@color_resolver = color_resolver
|
|
38
|
-
gradient = @table[name]
|
|
39
|
-
return nil unless gradient
|
|
40
|
-
|
|
41
|
-
stops = gradient.gradient_stop
|
|
42
|
-
return nil if stops.length < 2
|
|
43
|
-
|
|
44
|
-
segment_height = height.to_f / SEGMENTS
|
|
45
|
-
ops = []
|
|
46
|
-
SEGMENTS.downto(1) do |i|
|
|
47
|
-
offset = (i - 1) / SEGMENTS.to_f
|
|
48
|
-
color = interpolate(stops, offset)
|
|
49
|
-
next unless color
|
|
50
|
-
|
|
51
|
-
ops << "q"
|
|
52
|
-
ops << Idml::Render::ColorHelper.to_canvas(color)
|
|
53
|
-
ops << format("%<x>.2f %<y>.2f %<w>.2f %<h>.2f re",
|
|
54
|
-
x: x,
|
|
55
|
-
y: y + ((SEGMENTS - i) * segment_height),
|
|
56
|
-
w: width,
|
|
57
|
-
h: segment_height + 0.1)
|
|
58
|
-
ops << "f"
|
|
59
|
-
ops << "Q"
|
|
60
|
-
end
|
|
61
|
-
ops.join("\n")
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def interpolate(stops, offset)
|
|
65
|
-
next_stop = stops.find { |s| s.location >= offset } || stops.last
|
|
66
|
-
prev_stop = stops.reverse.find { |s| s.location <= offset } || stops.first
|
|
67
|
-
return resolve_color(next_stop.stop_color) if next_stop == prev_stop
|
|
68
|
-
|
|
69
|
-
blend_between(prev_stop, next_stop, offset)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def blend_between(prev_stop, next_stop, offset)
|
|
73
|
-
range = (next_stop.location - prev_stop.location)
|
|
74
|
-
range = 1.0 if range.zero?
|
|
75
|
-
local = (offset - prev_stop.location) / range
|
|
76
|
-
|
|
77
|
-
prev_color = resolve_color(prev_stop.stop_color)
|
|
78
|
-
next_color = resolve_color(next_stop.stop_color)
|
|
79
|
-
return prev_color unless prev_color && next_color
|
|
80
|
-
return next_color if local >= 1.0
|
|
81
|
-
|
|
82
|
-
blend(prev_color, next_color, local)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def resolve_color(name)
|
|
86
|
-
return nil unless @color_resolver && name
|
|
87
|
-
|
|
88
|
-
@color_resolver.resolve(name)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def blend(color_a, color_b, t)
|
|
92
|
-
case color_a[:model]
|
|
93
|
-
when :rgb
|
|
94
|
-
{
|
|
95
|
-
model: :rgb,
|
|
96
|
-
r: color_a[:r] + ((color_b[:r] - color_a[:r]) * t),
|
|
97
|
-
g: color_a[:g] + ((color_b[:g] - color_a[:g]) * t),
|
|
98
|
-
b: color_a[:b] + ((color_b[:b] - color_a[:b]) * t),
|
|
99
|
-
}
|
|
100
|
-
when :cmyk
|
|
101
|
-
{
|
|
102
|
-
model: :cmyk,
|
|
103
|
-
c: color_a[:c] + ((color_b[:c] - color_a[:c]) * t),
|
|
104
|
-
m: color_a[:m] + ((color_b[:m] - color_a[:m]) * t),
|
|
105
|
-
y: color_a[:y] + ((color_b[:y] - color_a[:y]) * t),
|
|
106
|
-
k: color_a[:k] + ((color_b[:k] - color_a[:k]) * t),
|
|
107
|
-
}
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
18
|
end
|
|
111
19
|
end
|
|
112
20
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Render
|
|
5
|
+
# Resolves a page item's geometric placement on the PDF page.
|
|
6
|
+
# Encapsulates the read of `geometric_bounds` and `item_transform`
|
|
7
|
+
# from any page-item model and the subsequent transform into a
|
|
8
|
+
# PDF-coordinate `{ x:, y:, width:, height: }` rect via `Geometry`.
|
|
9
|
+
#
|
|
10
|
+
# This is the single source of truth for "where does this item
|
|
11
|
+
# draw" — every shape renderer goes through here instead of each
|
|
12
|
+
# one re-implementing the bounds/transform dance.
|
|
13
|
+
module Placement
|
|
14
|
+
FALLBACK = { x: 72.0, y: 72.0, width: 400.0, height: 600.0 }.freeze
|
|
15
|
+
|
|
16
|
+
# Returns the PDF-placement rect for the given page item, or
|
|
17
|
+
# `nil` when the item has no geometric bounds. Pass
|
|
18
|
+
# `fallback: true` to receive a default rect instead of nil
|
|
19
|
+
# (used by text frames that need *somewhere* to render even
|
|
20
|
+
# when geometry is absent).
|
|
21
|
+
def self.box(item, page_height, fallback: false)
|
|
22
|
+
bounds = item.geometric_bounds
|
|
23
|
+
if bounds
|
|
24
|
+
Geometry.placement_rect(bounds, item.item_transform, page_height)
|
|
25
|
+
elsif fallback
|
|
26
|
+
FALLBACK.dup.merge(y: page_height - FALLBACK[:height] - 72.0)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -6,20 +6,22 @@ module Idml
|
|
|
6
6
|
class GraphicLineRenderer
|
|
7
7
|
def self.render(canvas, context)
|
|
8
8
|
line = context.item
|
|
9
|
-
return unless
|
|
9
|
+
return unless StrokeStyle.strokeable?(line)
|
|
10
10
|
|
|
11
11
|
color = context.color_resolver&.resolve(line.stroke_color)
|
|
12
12
|
return unless color
|
|
13
13
|
|
|
14
|
-
box =
|
|
14
|
+
box = Placement.box(line, context.page_height)
|
|
15
15
|
return unless box
|
|
16
16
|
|
|
17
|
-
canvas.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
Blending.wrap(canvas, line.transparency_setting) do
|
|
18
|
+
StrokeStyle.apply(canvas, line) do
|
|
19
|
+
canvas.stroke_color(ColorHelper.to_canvas(color))
|
|
20
|
+
canvas.line_width = line.stroke_weight
|
|
21
|
+
canvas.move_to(box[:x], box[:y] + box[:height])
|
|
22
|
+
canvas.line_to(box[:x] + box[:width], box[:y])
|
|
23
|
+
canvas.stroke
|
|
24
|
+
end
|
|
23
25
|
end
|
|
24
26
|
end
|
|
25
27
|
end
|
|
@@ -8,10 +8,10 @@ module Idml
|
|
|
8
8
|
poly = context.item
|
|
9
9
|
return if poly.visible == false
|
|
10
10
|
|
|
11
|
-
box =
|
|
11
|
+
box = Placement.box(poly, context.page_height)
|
|
12
12
|
return unless box
|
|
13
13
|
|
|
14
|
-
canvas.
|
|
14
|
+
Blending.wrap(canvas, poly.transparency_setting) do
|
|
15
15
|
render_fill(canvas, poly, context, box)
|
|
16
16
|
render_stroke(canvas, poly, context, box)
|
|
17
17
|
end
|
|
@@ -30,15 +30,17 @@ module Idml
|
|
|
30
30
|
private_class_method :render_fill
|
|
31
31
|
|
|
32
32
|
def self.render_stroke(canvas, poly, context, box)
|
|
33
|
-
return unless
|
|
33
|
+
return unless StrokeStyle.strokeable?(poly)
|
|
34
34
|
|
|
35
35
|
color = context.color_resolver&.resolve(poly.stroke_color)
|
|
36
36
|
return unless color
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
42
44
|
end
|
|
43
45
|
private_class_method :render_stroke
|
|
44
46
|
end
|
|
@@ -9,22 +9,15 @@ module Idml
|
|
|
9
9
|
rect = context.item
|
|
10
10
|
return if rect.visible == false
|
|
11
11
|
|
|
12
|
-
box =
|
|
12
|
+
box = Placement.box(rect, context.page_height)
|
|
13
13
|
return unless box
|
|
14
14
|
|
|
15
|
-
canvas.
|
|
15
|
+
Blending.wrap(canvas, rect.transparency_setting) do
|
|
16
16
|
render_fill(canvas, rect, context, box)
|
|
17
17
|
render_stroke(canvas, rect, context, box)
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def self.placement_box(rect, page_height)
|
|
22
|
-
bounds = rect.geometric_bounds
|
|
23
|
-
return nil unless bounds
|
|
24
|
-
|
|
25
|
-
Geometry.placement_rect(bounds, rect.item_transform, page_height)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
21
|
def self.render_fill(canvas, rect, context, box)
|
|
29
22
|
return unless rect.fill_color
|
|
30
23
|
return if rect.fill_color == "Color/None"
|
|
@@ -44,32 +37,51 @@ module Idml
|
|
|
44
37
|
private_class_method :render_fill
|
|
45
38
|
|
|
46
39
|
def self.render_gradient_fill(canvas, rect, context, box)
|
|
47
|
-
|
|
48
|
-
return unless
|
|
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
|
|
49
56
|
|
|
50
|
-
|
|
57
|
+
def self.axial_shading(canvas, box, stops)
|
|
58
|
+
canvas.document.shadings.add_axial(
|
|
51
59
|
from: [box[:x], box[:y] + box[:height]],
|
|
52
60
|
to: [box[:x] + box[:width], box[:y]],
|
|
53
61
|
stops: stops,
|
|
54
62
|
)
|
|
55
|
-
canvas.rectangle(box[:x], box[:y], box[:width], box[:height])
|
|
56
|
-
canvas.fill_shading(shading)
|
|
57
63
|
end
|
|
58
|
-
private_class_method :
|
|
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
|
|
59
77
|
|
|
60
|
-
def self.
|
|
78
|
+
def self.gradient_for(rect, context)
|
|
61
79
|
graphic = context.package&.graphic
|
|
62
80
|
return nil unless graphic
|
|
63
81
|
|
|
64
|
-
|
|
65
|
-
g.self_attr == rect.fill_color
|
|
66
|
-
end
|
|
67
|
-
return nil unless gradient
|
|
68
|
-
return nil unless gradient.gradient_stop.length >= 2
|
|
69
|
-
|
|
70
|
-
gradient_stops(gradient, context)
|
|
82
|
+
graphic.gradient.find { |g| g.self_attr == rect.fill_color }
|
|
71
83
|
end
|
|
72
|
-
private_class_method :
|
|
84
|
+
private_class_method :gradient_for
|
|
73
85
|
|
|
74
86
|
def self.gradient_stops(gradient, context)
|
|
75
87
|
gradient.gradient_stop.filter_map do |stop|
|
|
@@ -82,23 +94,19 @@ module Idml
|
|
|
82
94
|
private_class_method :gradient_stops
|
|
83
95
|
|
|
84
96
|
def self.render_stroke(canvas, rect, context, box)
|
|
85
|
-
return unless strokeable?(rect)
|
|
97
|
+
return unless StrokeStyle.strokeable?(rect)
|
|
86
98
|
|
|
87
99
|
color = context.color_resolver&.resolve(rect.stroke_color)
|
|
88
100
|
return unless color
|
|
89
101
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
94
108
|
end
|
|
95
109
|
private_class_method :render_stroke
|
|
96
|
-
|
|
97
|
-
def self.strokeable?(rect)
|
|
98
|
-
rect.stroke_color &&
|
|
99
|
-
rect.stroke_color != "Color/None" &&
|
|
100
|
-
rect.stroke_weight&.positive?
|
|
101
|
-
end
|
|
102
110
|
end
|
|
103
111
|
end
|
|
104
112
|
end
|
|
@@ -9,7 +9,7 @@ module Idml
|
|
|
9
9
|
return if table.visible == false
|
|
10
10
|
return if table.table_row.empty?
|
|
11
11
|
|
|
12
|
-
box =
|
|
12
|
+
box = Placement.box(table, context.page_height)
|
|
13
13
|
return unless box
|
|
14
14
|
|
|
15
15
|
row_count = table.table_row.length
|