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.
@@ -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
- # Resolves IDML gradient references to discrete PDF gradient fills.
6
- # A gradient is rendered as N thin colored rectangles that
7
- # approximate the smooth gradient transition. This is not a true
8
- # PDF shading pattern (which requires Function dictionaries), but
9
- # produces a visually correct gradient without complex PDF structure.
10
- class GradientResolver
11
- SEGMENTS = 32
12
-
13
- def self.build(graphic)
14
- table = {}
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 RectangleRenderer.strokeable?(line)
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 = RectangleRenderer.placement_box(line, context.page_height)
14
+ box = Placement.box(line, context.page_height)
15
15
  return unless box
16
16
 
17
- canvas.save_graphics_state do
18
- canvas.stroke_color(ColorHelper.to_canvas(color))
19
- canvas.line_width = line.stroke_weight
20
- canvas.move_to(box[:x], box[:y] + box[:height])
21
- canvas.line_to(box[:x] + box[:width], box[:y])
22
- canvas.stroke
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 = RectangleRenderer.placement_box(poly, context.page_height)
11
+ box = Placement.box(poly, context.page_height)
12
12
  return unless box
13
13
 
14
- canvas.save_graphics_state do
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 RectangleRenderer.strokeable?(poly)
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
- canvas.stroke_color(ColorHelper.to_canvas(color))
39
- canvas.line_width = poly.stroke_weight
40
- canvas.rectangle(box[:x], box[:y], box[:width], box[:height])
41
- canvas.stroke
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 = placement_box(rect, context.page_height)
12
+ box = Placement.box(rect, context.page_height)
13
13
  return unless box
14
14
 
15
- canvas.save_graphics_state do
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
- stops = gradient_stops_for(rect, context)
48
- return unless stops&.length&.>=(2)
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
- shading = canvas.document.shadings.add_axial(
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 :render_gradient_fill
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.gradient_stops_for(rect, context)
78
+ def self.gradient_for(rect, context)
61
79
  graphic = context.package&.graphic
62
80
  return nil unless graphic
63
81
 
64
- gradient = graphic.gradient.find do |g|
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 :gradient_stops_for
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
- canvas.stroke_color(ColorHelper.to_canvas(color))
91
- canvas.line_width = rect.stroke_weight
92
- canvas.rectangle(box[:x], box[:y], box[:width], box[:height])
93
- canvas.stroke
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 = RectangleRenderer.placement_box(table, context.page_height)
12
+ box = Placement.box(table, context.page_height)
13
13
  return unless box
14
14
 
15
15
  row_count = table.table_row.length