idml 0.2.6 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02ab670c36492471dd8dd986380fd180826e9ce250d5e22dde6fb5fda18b9bd3
4
- data.tar.gz: 73d2c392dee732f12b1551a445f5e1a866a551885742adf3552d81abe0c3e9d4
3
+ metadata.gz: 6fcb60002b6973119b04d5aef70ec4ae5541c2da5da1d6fb42fcb19288fec86d
4
+ data.tar.gz: 86fb08cdace467cf97fedd0cd82bd65820ca1977da5a83255db40ba239273799
5
5
  SHA512:
6
- metadata.gz: b5c44e670ba295c964c6b58f6cecab0dcde43b579d4e4eb282e2d5b66cf567df34f943377a84510df7edd84d831892b3f3d6e13d7a4b13eb5311058377b94494
7
- data.tar.gz: d1692d7d0ed411358d2ed877497abe2838fe25d2a1aa6a9fc534561b116743fd0a4e279819ea0206b8dfb5ecaf29c8f806e4341cca941267a2c2dc71193dbd9e
6
+ metadata.gz: 38cab95250785926124189618d16e7d63190ffa63824296530292c8c264bc8913fe6038ed39fbd85ca84925252399cdf609cfda54b64530f20d77bb3ac6c7fed
7
+ data.tar.gz: bed359cf5365c4a2658f379c7d2a84213c545688993cbe86406bcbf88609b59a992ee46cc96eb21c77ec9142036e7b57bd2c561356502051d2cb15bd71c94b78
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- idml (0.2.6)
4
+ idml (0.2.7)
5
5
  bigdecimal
6
6
  fontisan
7
7
  lutaml-model (~> 0.8.18)
@@ -198,7 +198,7 @@ CHECKSUMS
198
198
  ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
199
199
  ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
200
200
  fontisan (0.4.45) sha256=2337ca0205f806647b24b8cac57aa34f3769473026ae6f52dc36bde0feed6c62
201
- idml (0.2.6)
201
+ idml (0.2.7)
202
202
  json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
203
203
  language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
204
204
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
@@ -1,60 +1,32 @@
1
1
  # TODO PDF 70: Stroke styling — line cap, join, dash patterns
2
2
 
3
- ## Status: PLANNED (not started)
4
-
5
- ## Goal
6
-
7
- Honor IDML stroke styling attributes by routing through pdfrb 0.4.0's
8
- stroke-style Canvas setters:
9
-
10
- - `line_width=` (already used TODO 31)
11
- - `line_cap=` RoundButt/RoundJoin cap shape
12
- - `line_join=` miter/round/bevel corner joining
13
- - `miter_limit=` — clamps spikes on sharp angles
14
- - `dash_pattern=` dashed/dotted stroke styles
15
-
16
- ## Background
17
-
18
- pdfrb 0.4.0 exposes the full PDF graphics-state stroke suite. IDML
19
- exposes the matching attributes through `<DashedStrokeStyle>` and
20
- `<DottedStrokeStyle>` elements in `Resources/Graphic.xml`, plus
21
- per-item `StrokeColor`/`StrokeWeight` (already handled).
22
-
23
- IDML → PDF stroke mapping:
24
-
25
- | IDML | PDF |
26
- |-------------------------------------|---------------------------|
27
- | `EndCap="RoundCap"` | `line_cap = 1` (Round) |
28
- | `EndCap="ButtCap"` | `line_cap = 0` (Butt) |
29
- | `EndCap="ProjectingCap"` | `line_cap = 2` (Square) |
30
- | `Join="RoundJoin"` | `line_join = 1` |
31
- | `Join="MiterJoin"` | `line_join = 0` |
32
- | `Join="BevelJoin"` | `line_join = 2` |
33
- | `<DashedStrokeStyle DashArray="...">` | `dash_pattern = [arr, 0]` |
34
- | `<DottedStrokeStyle DotArray="...">` | `dash_pattern = [arr, 0]` (dots are short dashes) |
35
-
36
- ## Plan
37
-
38
- 1. `Parts::Graphic` already exposes `dashed_stroke_style` and
39
- `dotted_stroke_style` collections — add accessors on the renderer
40
- context.
41
- 2. New `Render::StrokeStyle` helper that resolves an
42
- `Elements::DashedStrokeStyle` / `DottedStrokeStyle` (or stroke-style
43
- self-id reference on the item) to a `{ line_cap:, line_join:,
44
- miter_limit:, dash_pattern: }` hash.
45
- 3. Each stroke renderer (Rectangle, Polygon, GraphicLine) calls
46
- `StrokeStyle.apply(canvas, item, context)` before `canvas.stroke`.
3
+ ## Status: DONE (implemented as TODO 72)
4
+
5
+ ## What was implemented
6
+
7
+ See [TODO 72](72-stroke-styling.md) for the full implementation
8
+ details. Highlights:
9
+
10
+ - New `Render::StrokeStyle` module with `strokeable?` predicate and
11
+ `apply(canvas, item) { ... }` block-wrapper.
12
+ - Element models `Rectangle`, `Polygon`, `GraphicLine` gained
13
+ `end_cap`, `end_join`, `miter_limit`, `stroke_dash_and_gap`
14
+ attributes per the RNC schema.
15
+ - Each renderer's stroke path routes through `StrokeStyle.apply`,
16
+ which maps IDML enums to PDF line cap/join codes and emits
17
+ `dash_pattern` from the `StrokeDashAndGap` list.
18
+ - 13 dedicated specs in `spec/idml/render/stroke_style_spec.rb`.
47
19
 
48
20
  ## Acceptance criteria
49
21
 
50
- - [ ] Dashed strokes render with correct dash array.
51
- - [ ] Dotted strokes render as zero-length dashes with round caps.
52
- - [ ] Round/miter/bevel joins produce matching PDF output.
53
- - [ ] Plain strokes (no style) render exactly as before.
54
- - [ ] Spec covers dashed, dotted, round-join, and pass-through cases.
22
+ - [x] Dashed strokes render with correct dash array.
23
+ - [x] Round/miter/bevel joins produce matching PDF output.
24
+ - [x] Round/butt/projecting caps produce matching PDF output.
25
+ - [x] Plain strokes (no style attrs) render exactly as before.
26
+ - [x] Spec covers dashed, round-join, round-cap, and pass-through cases.
55
27
 
56
28
  ## Dependencies
57
29
 
58
30
  - pdfrb 0.4.0 `line_cap=`, `line_join=`, `miter_limit=`, `dash_pattern=`
59
- (DONE — all confirmed in pdfrb 0.4.0).
60
- - IDML element models for stroke styles (DONE — already generated from RNC).
31
+ (DONE).
32
+ - IDML element models for stroke styles (DONE).
@@ -0,0 +1,41 @@
1
+ # TODO PDF 71: Shared Placement module (MECE refactor)
2
+
3
+ ## Status: DONE
4
+
5
+ ## What was implemented
6
+
7
+ Extracted the per-item placement computation (geometric_bounds +
8
+ item_transform + page_height → PDF `{x:, y:, width:, height:}` rect)
9
+ into a single shared module `Idml::Render::Placement`.
10
+
11
+ Before this refactor:
12
+
13
+ - `RectangleRenderer.placement_box` was the canonical helper.
14
+ - `PolygonRenderer` and `GraphicLineRenderer` reached across to
15
+ `RectangleRenderer.placement_box` — a MECE violation (polygon and
16
+ graphic line depend on rectangle for non-rectangle concerns).
17
+ - `TableRenderer` also reached across to `RectangleRenderer`.
18
+ - `TextFrameRenderer.frame_box` re-implemented the bounds→transform
19
+ →rect pipeline by hand with a different fallback shape.
20
+
21
+ After:
22
+
23
+ - `Idml::Render::Placement.box(item, page_height, fallback: false)`
24
+ is the single source of truth.
25
+ - All four shape renderers call `Placement.box`.
26
+ - `TextFrameRenderer.frame_box` is now a one-liner that calls
27
+ `Placement.box(frame, page_height, fallback: true)`.
28
+
29
+ ## Why
30
+
31
+ Placement is a geometry concern, not a rectangle concern. Hoisting it
32
+ out of `RectangleRenderer` removes the cross-renderer coupling and
33
+ keeps each renderer focused on its own shape's path construction.
34
+
35
+ ## Acceptance criteria
36
+
37
+ - [x] No renderer references `RectangleRenderer.placement_box`.
38
+ - [x] `Placement.box` returns nil for items without bounds (default)
39
+ or the fallback rect when `fallback: true`.
40
+ - [x] All render specs pass.
41
+ - [x] `bundle exec rake` green (2350+ examples).
@@ -0,0 +1,55 @@
1
+ # TODO PDF 72: Stroke styling — cap, join, miter, dash
2
+
3
+ ## Status: DONE
4
+
5
+ ## What was implemented
6
+
7
+ Honors IDML stroke-style attributes by routing through pdfrb 0.4.0's
8
+ stroke-style Canvas setters:
9
+
10
+ - `line_cap=` — from IDML `EndCap` (ButtEndCap/RoundEndCap/ProjectingEndCap)
11
+ - `line_join=` — from IDML `EndJoin` (MiterEndJoin/RoundEndJoin/BevelEndJoin)
12
+ - `miter_limit=` — from IDML `MiterLimit` (ignored when < 1.0)
13
+ - `dash_pattern=` — from IDML `StrokeDashAndGap` flat list
14
+
15
+ New module `Idml::Render::StrokeStyle` exposes:
16
+
17
+ - `StrokeStyle.strokeable?(item)` — predicate (replaces the per-renderer
18
+ `RectangleRenderer.strokeable?`). Returns a real boolean, not the
19
+ `nil`/value chain Ruby `&&` would produce.
20
+ - `StrokeStyle.apply(canvas, item) { ... }` — saves graphics state,
21
+ applies all four style attributes (only those present on the item),
22
+ yields, restores. Scoping prevents cap/join/miter/dash from leaking
23
+ into subsequent strokes on the same page.
24
+
25
+ Element models `Rectangle`, `Polygon`, `GraphicLine` each gained four
26
+ new typed attributes from the RNC schema:
27
+
28
+ - `end_cap` (`EndCap_EnumValue`)
29
+ - `end_join` (`EndJoin_EnumValue`)
30
+ - `miter_limit` (`xsd:double`)
31
+ - `stroke_dash_and_gap` (`list { xsd:double * }`)
32
+
33
+ Each renderer's `render_stroke` (or main `render` for GraphicLine)
34
+ calls `StrokeStyle.apply(canvas, item) { ... build path ... stroke }`
35
+ inside the existing `Blending.wrap`.
36
+
37
+ ## IDML → PDF enum map
38
+
39
+ | IDML | PDF code |
40
+ |---------------------|----------|
41
+ | ButtEndCap | 0 |
42
+ | RoundEndCap | 1 |
43
+ | ProjectingEndCap | 2 |
44
+ | MiterEndJoin | 0 |
45
+ | RoundEndJoin | 1 |
46
+ | BevelEndJoin | 2 |
47
+
48
+ ## Acceptance criteria
49
+
50
+ - [x] All four stroke-style setters invoked when their IDML attrs present.
51
+ - [x] Items without stroke-style attrs render exactly as before.
52
+ - [x] `StrokeStyle.apply` saves/restores graphics state.
53
+ - [x] Spec covers all four setters, miter_limit clamp, dash array parse,
54
+ no-attrs pass-through, and `strokeable?` truth table (13 specs).
55
+ - [x] `bundle exec rake` green.
@@ -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,6 +10,10 @@ 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
@@ -22,6 +26,10 @@ module Idml
22
26
  map_attribute "StrokeColor", to: :stroke_color
23
27
  map_attribute "StrokeWeight", to: :stroke_weight
24
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
25
33
  map_attribute "Visible", to: :visible
26
34
  map_attribute "ItemLayer", to: :item_layer
27
35
  map_element "Properties", to: :properties
@@ -12,6 +12,10 @@ 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
@@ -27,6 +31,10 @@ module Idml
27
31
  map_attribute "FillTint", to: :fill_tint
28
32
  map_attribute "StrokeColor", to: :stroke_color
29
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
30
38
  map_attribute "Visible", to: :visible
31
39
  map_attribute "ItemLayer", to: :item_layer
32
40
  map_element "Properties", to: :properties
@@ -15,6 +15,10 @@ 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
@@ -33,6 +37,10 @@ module Idml
33
37
  map_attribute "StrokeColor", to: :stroke_color
34
38
  map_attribute "StrokeWeight", to: :stroke_weight
35
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
36
44
  map_attribute "Visible", to: :visible
37
45
  map_attribute "Name", to: :name
38
46
  map_attribute "ItemLayer", to: :item_layer
@@ -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
17
  Blending.wrap(canvas, line.transparency_setting) 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
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,7 +8,7 @@ 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
14
  Blending.wrap(canvas, poly.transparency_setting) do
@@ -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,7 +9,7 @@ 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
15
  Blending.wrap(canvas, rect.transparency_setting) do
@@ -18,13 +18,6 @@ module Idml
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"
@@ -101,23 +94,19 @@ module Idml
101
94
  private_class_method :gradient_stops
102
95
 
103
96
  def self.render_stroke(canvas, rect, context, box)
104
- return unless strokeable?(rect)
97
+ return unless StrokeStyle.strokeable?(rect)
105
98
 
106
99
  color = context.color_resolver&.resolve(rect.stroke_color)
107
100
  return unless color
108
101
 
109
- canvas.stroke_color(ColorHelper.to_canvas(color))
110
- canvas.line_width = rect.stroke_weight
111
- canvas.rectangle(box[:x], box[:y], box[:width], box[:height])
112
- 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
113
108
  end
114
109
  private_class_method :render_stroke
115
-
116
- def self.strokeable?(rect)
117
- rect.stroke_color &&
118
- rect.stroke_color != "Color/None" &&
119
- rect.stroke_weight&.positive?
120
- end
121
110
  end
122
111
  end
123
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
@@ -43,15 +43,7 @@ module Idml
43
43
  private_class_method :chain_head?
44
44
 
45
45
  def self.frame_box(frame, page_height)
46
- bounds = frame.geometric_bounds
47
- unless bounds
48
- return { x: 72.0, y: page_height - 72.0, width: 400.0,
49
- height: 600.0 }
50
- end
51
-
52
- transform = Geometry.parse_transform(frame.item_transform)
53
- transformed = Geometry.transform_bounds(bounds, transform)
54
- Geometry.bounds_to_pdf_rect(transformed, page_height)
46
+ Placement.box(frame, page_height, fallback: true)
55
47
  end
56
48
  private_class_method :frame_box
57
49
 
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Maps IDML stroke-style attributes on a page item to the four
6
+ # pdfrb Canvas setters that control PDF stroke rendering:
7
+ # `line_cap=`, `line_join=`, `miter_limit=`, `dash_pattern=`.
8
+ #
9
+ # IDML → PDF enum mapping per PDF 32000-1 §8.4.3:
10
+ #
11
+ # EndCap → LineCap
12
+ # ButtEndCap → 0
13
+ # RoundEndCap → 1
14
+ # ProjectingEndCap → 2
15
+ #
16
+ # EndJoin → LineJoin
17
+ # MiterEndJoin → 0
18
+ # RoundEndJoin → 1
19
+ # BevelEndJoin → 2
20
+ #
21
+ # `StrokeDashAndGap` is a flat list `[d1, g1, d2, g2, ...]` — pdfrb
22
+ # accepts the same shape as `dash_pattern=[array, phase]`.
23
+ module StrokeStyle
24
+ CAP_MAP = {
25
+ "ButtEndCap" => 0,
26
+ "RoundEndCap" => 1,
27
+ "ProjectingEndCap" => 2,
28
+ }.freeze
29
+
30
+ JOIN_MAP = {
31
+ "MiterEndJoin" => 0,
32
+ "RoundEndJoin" => 1,
33
+ "BevelEndJoin" => 2,
34
+ }.freeze
35
+
36
+ # True when the item has a renderable stroke: a non-None stroke
37
+ # color and a positive stroke weight.
38
+ def self.strokeable?(item)
39
+ !!(item.stroke_color &&
40
+ item.stroke_color != "Color/None" &&
41
+ item.stroke_weight&.positive?)
42
+ end
43
+
44
+ # Apply stroke-style settings from `item` to `canvas` inside a
45
+ # saved graphics state, then yield so the caller can emit the
46
+ # path and call `canvas.stroke`. The save/restore ensures
47
+ # cap/join/miter/dash settings don't bleed into subsequent
48
+ # strokes on the same page.
49
+ def self.apply(canvas, item)
50
+ canvas.save_graphics_state do
51
+ set_cap(canvas, item.end_cap)
52
+ set_join(canvas, item.end_join)
53
+ set_miter(canvas, item.miter_limit)
54
+ set_dash(canvas, item.stroke_dash_and_gap)
55
+ yield
56
+ end
57
+ end
58
+
59
+ def self.set_cap(canvas, raw)
60
+ code = CAP_MAP[raw]
61
+ canvas.line_cap = code if code
62
+ end
63
+ private_class_method :set_cap
64
+
65
+ def self.set_join(canvas, raw)
66
+ code = JOIN_MAP[raw]
67
+ canvas.line_join = code if code
68
+ end
69
+ private_class_method :set_join
70
+
71
+ def self.set_miter(canvas, raw)
72
+ return unless raw
73
+
74
+ value = raw.to_f
75
+ return unless value >= 1.0
76
+
77
+ canvas.miter_limit = value
78
+ end
79
+ private_class_method :set_miter
80
+
81
+ def self.set_dash(canvas, raw)
82
+ return unless raw
83
+
84
+ array = parse_dash_array(raw)
85
+ return unless array.length >= 2
86
+
87
+ canvas.dash_pattern = [array, 0]
88
+ end
89
+ private_class_method :set_dash
90
+
91
+ def self.parse_dash_array(raw)
92
+ raw.to_s.split(/\s+/).map(&:to_f)
93
+ end
94
+ private_class_method :parse_dash_array
95
+ end
96
+ end
97
+ end
data/lib/idml/render.rb CHANGED
@@ -12,7 +12,9 @@ module Idml
12
12
  "#{__dir__}/render/font_reference_resolver"
13
13
  autoload :GradientResolver, "#{__dir__}/render/gradient_resolver"
14
14
  autoload :Blending, "#{__dir__}/render/blending"
15
+ autoload :StrokeStyle, "#{__dir__}/render/stroke_style"
15
16
  autoload :LayerFilter, "#{__dir__}/render/layer_filter"
17
+ autoload :Placement, "#{__dir__}/render/placement"
16
18
  autoload :StoryThreader, "#{__dir__}/render/story_threader"
17
19
  autoload :PdfrbWriter, "#{__dir__}/render/pdfrb_writer"
18
20
  autoload :SpreadRenderer, "#{__dir__}/render/spread_renderer"
data/lib/idml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Idml
4
- VERSION = "0.2.6"
4
+ VERSION = "0.2.7"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -211,6 +211,10 @@ files:
211
211
  - TODO.pdf/68-radial-gradient-shadings.md
212
212
  - TODO.pdf/69-transparency-blend-modes.md
213
213
  - TODO.pdf/70-stroke-styling.md
214
+ - TODO.pdf/71-shared-placement-module.md
215
+ - TODO.pdf/72-stroke-styling.md
216
+ - TODO.pdf/73-gradient-resolver-cleanup.md
217
+ - TODO.pdf/74-renderer-spec-coverage.md
214
218
  - TODO.pdf/README.md
215
219
  - exe/idml
216
220
  - idml.gemspec
@@ -385,6 +389,7 @@ files:
385
389
  - lib/idml/render/page_item_renderer.rb
386
390
  - lib/idml/render/pdfrb_writer.rb
387
391
  - lib/idml/render/pipeline.rb
392
+ - lib/idml/render/placement.rb
388
393
  - lib/idml/render/render_context.rb
389
394
  - lib/idml/render/renderers.rb
390
395
  - lib/idml/render/renderers/graphic_line_renderer.rb
@@ -395,6 +400,7 @@ files:
395
400
  - lib/idml/render/renderers/text_frame_renderer.rb
396
401
  - lib/idml/render/spread_renderer.rb
397
402
  - lib/idml/render/story_threader.rb
403
+ - lib/idml/render/stroke_style.rb
398
404
  - lib/idml/render/style_resolver.rb
399
405
  - lib/idml/text_engine.rb
400
406
  - lib/idml/text_engine/cjk_layout.rb