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 +4 -4
- data/Gemfile.lock +2 -2
- data/TODO.pdf/70-stroke-styling.md +23 -51
- 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 +8 -0
- data/lib/idml/elements/polygon.rb +8 -0
- data/lib/idml/elements/rectangle.rb +8 -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 +9 -7
- data/lib/idml/render/renderers/polygon_renderer.rb +8 -6
- data/lib/idml/render/renderers/rectangle_renderer.rb +8 -19
- 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 +2 -0
- data/lib/idml/version.rb +1 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6fcb60002b6973119b04d5aef70ec4ae5541c2da5da1d6fb42fcb19288fec86d
|
|
4
|
+
data.tar.gz: 86fb08cdace467cf97fedd0cd82bd65820ca1977da5a83255db40ba239273799
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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:
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- `
|
|
11
|
-
|
|
12
|
-
- `
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
- [
|
|
51
|
-
- [
|
|
52
|
-
- [
|
|
53
|
-
- [
|
|
54
|
-
- [
|
|
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
|
|
60
|
-
- IDML element models for stroke styles (DONE
|
|
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
|
-
#
|
|
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
17
|
Blending.wrap(canvas, line.transparency_setting) do
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 =
|
|
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
|
|
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,7 +9,7 @@ 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
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 =
|
|
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
|
-
|
|
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
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.
|
|
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
|