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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d870be70e870df44bf1b0fcd658e5d5cc04b877e00d195f40666027c4a64b12
4
- data.tar.gz: f3b5e9f8ea4e3eb181f66795746a090d4994c5ce7ea32cbf210b8366c9d801fa
3
+ metadata.gz: 6fcb60002b6973119b04d5aef70ec4ae5541c2da5da1d6fb42fcb19288fec86d
4
+ data.tar.gz: 86fb08cdace467cf97fedd0cd82bd65820ca1977da5a83255db40ba239273799
5
5
  SHA512:
6
- metadata.gz: 2bbbd17c0ef7958732c45ffc848f4e0ad89ac27406f79f42af769d49ce9f5dc88584b5e7b45e462a77d85c663b383113c11b8cd824942320de7baa57bf3ec77d
7
- data.tar.gz: b6e86c5e3d233aa80ea12302db92781dac9d08e6cc55c28aa83858bc73b18875f78f7bebdf67e077c9c7fa462d617e8401763b421cf45a9c7e21d0663516eb8a
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.5)
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.5)
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,30 +1,42 @@
1
1
  # TODO PDF 40: Font PS name resolution from Fonts.xml
2
2
 
3
- ## Goal
4
-
5
- Connect the IDML document's Fonts.xml (FontFamily/Font entries with
6
- PostScriptName) to the FontResolver so text rendering uses the correct
7
- font, not the hardcoded "Helvetica" default.
8
-
9
- ## Status: PARTIALLY DONE
10
-
11
- ## What was done
12
-
13
- - Verified `Parts::Fonts` typed model parses FontFamily → Font entries.
14
- - `FontFamily` and `Font` element models exist and expose PostScriptName.
15
- - The FontResolver can find fonts by family name + style.
16
-
17
- ## What remains
18
-
19
- - Map CharacterStyleRange#applied_font FontFamily/Font → PostScriptName
20
- FontResolver search.
21
- - Add PostScriptName-based search to FontResolver (current search is
22
- family-name based).
23
- - Pipeline: pass document font references to the font resolver.
24
- - When a document font can't be found on disk, log a warning and fall
25
- back to the default font.
26
-
27
- ## Dependencies
28
-
29
- - TODO 29 (StyleResolver for applied_font extraction).
30
- - TODO 33 (font resolution from IDML).
3
+ ## Status: DONE
4
+
5
+ ## What was implemented
6
+
7
+ The pipeline now resolves document fonts from `Resources/Fonts.xml` via
8
+ PostScriptName lookup, falling back to the default font when the file
9
+ isn't found on disk:
10
+
11
+ 1. `Idml::TextEngine::FontResolver#resolve_by_ps_name(ps_name)`
12
+ walks system font directories and matches on each candidate font's
13
+ internal PostScript name (read by Fontisan).
14
+ 2. `Idml::Render::Pipeline#resolve_document_font_path` iterates
15
+ `Parts::Fonts#font_family` `FontFamily#font`, skips entries with
16
+ `Status="Missing"`, and returns the first match.
17
+ 3. `Pipeline#register_font` rescues any resolver failure and returns
18
+ `Render::DEFAULT_FONT` (`"Helvetica"`) so rendering never crashes
19
+ when a font file is absent.
20
+ 4. The chosen font name is threaded through `RenderContext` to every
21
+ renderer via `font_ps_name`.
22
+
23
+ ## Verification
24
+
25
+ - `lib/idml/render/pipeline.rb:170` `resolve_document_font_path`
26
+ - `lib/idml/render/pipeline.rb:185` — `resolve_by_ps_name` call
27
+ - `lib/idml/text_engine/font_resolver.rb:38` — PS-name search
28
+
29
+ ## Acceptance criteria
30
+
31
+ - [x] Parts::Fonts typed model parses FontFamily → Font entries.
32
+ - [x] FontFamily and Font element models expose PostScriptName.
33
+ - [x] PostScriptName-based search added to FontResolver.
34
+ - [x] Pipeline passes document font references to the resolver.
35
+ - [x] Falls back to default font when document font can't be found.
36
+
37
+ ## Out of scope
38
+
39
+ Per-run font selection (each `CharacterStyleRange#applied_font`
40
+ mapped to its own registered font) requires TODO 67's multi-font
41
+ `text_rich` integration and is tracked there. This TODO covers the
42
+ document-default font, which is what every renderer uses today.
@@ -1,12 +1,44 @@
1
1
  # TODO PDF 52: Font subsetting via pdfrb
2
2
 
3
+ ## Status: BLOCKED (pdfrb has no subsetting API)
4
+
3
5
  ## Goal
4
6
 
5
7
  Subset embedded TrueType fonts to include only the glyphs actually used
6
8
  in the document. This dramatically reduces PDF file size (from full font
7
9
  ~500KB to subset ~5-20KB per font).
8
10
 
9
- ## Acceptance criteria
11
+ ## Blocker
12
+
13
+ pdfrb 0.4.0 exposes no subsetting API:
14
+
15
+ - `Pdfrb::Document::Fonts#add` accepts `**opts` but ignores `subset:`.
16
+ - No `used_codepoints` collector.
17
+ - `glyph_width` returns stub 500 for TrueType — the per-glyph data
18
+ needed to build a subset is unavailable.
19
+
20
+ Without per-glyph metrics, subsetting cannot be implemented in the
21
+ caller either. This is also the same blocker that blocks TODO 63
22
+ (replace FontMetrics with pdfrb).
23
+
24
+ ## Path forward
25
+
26
+ Either:
27
+
28
+ 1. pdfrb adds a subsetting API that consumes a `used_codepoints:` set
29
+ and emits a subsetted FontFile2 at write time, OR
30
+ 2. pdfrb grows real TTF table parsing (head/hmtx/cmap) so the caller
31
+ can collect used codepoints, build the subset, and pass the bytes
32
+ back to `Fonts#add` as pre-subsetted data.
33
+
34
+ Once pdfrb unblocks, the idml side needs to:
35
+
36
+ 1. Pipeline collects all Unicode codepoints across all stories.
37
+ 2. Pipeline passes `used_codepoints: set` to `Fonts#add(subset: true, ...)`.
38
+ 3. Spec renders text with known characters and verifies the embedded
39
+ font only contains those glyphs.
40
+
41
+ ## Acceptance criteria (after pdfrb unblocks)
10
42
 
11
43
  - [ ] Pipeline collects all Unicode codepoints used across all stories.
12
44
  - [ ] For each embedded font, only the used glyphs are included.
@@ -17,6 +49,4 @@ in the document. This dramatically reduces PDF file size (from full font
17
49
 
18
50
  ## Dependencies
19
51
 
20
- - pdfrb font embedding support (partial font registration works,
21
- FontFile2 embedding may require pdfrb enhancement).
22
- - TODO 27 (text engine for glyph collection).
52
+ - pdfrb subsetting API (NOT YET AVAILABLE in 0.4.0).
@@ -1,30 +1,24 @@
1
1
  # TODO PDF 55: Dead code deprecation markers
2
2
 
3
- ## Goal
3
+ ## Status: DONE (modules already removed)
4
4
 
5
- Mark old hand-rolled modules as deprecated. These modules are
6
- superseded by pdfrb but cannot be deleted per the project's
7
- "never delete source files" policy. Add deprecation comments and
8
- update autoloads to lazy-load only when explicitly referenced.
5
+ ## What was done
9
6
 
10
- ## Modules to deprecate
7
+ The old hand-rolled modules have already been deleted from the codebase
8
+ during the pdfrb migration (TODO 51 + TODO 62). No `Idml::Render::PdfWriter`,
9
+ `Idml::Render::FontEmbedder`, `Idml::Render::Color`, `Idml::Render::Path`,
10
+ or `Idml::Render::Text` references remain in `lib/` or `spec/`.
11
11
 
12
- - `Idml::Render::PdfWriter`replaced by `PdfrbWriter`
13
- - `Idml::Render::FontEmbedder` replaced by `PdfrbWriter.register_font`
14
- - `Idml::Render::Color` — replaced by `ColorHelper` + Canvas fill_color
15
- - `Idml::Render::Path` — replaced by Canvas drawing methods
16
- - `Idml::Render::Text` — replaced by Canvas text method
12
+ The original goal deprecation comments on dead modules — is moot:
13
+ there are no dead modules to deprecate. This TODO is closed without
14
+ further work.
17
15
 
18
- ## Acceptance criteria
16
+ ## Verification
19
17
 
20
- - [ ] Each deprecated module has a `# DEPRECATED` header comment pointing
21
- to its pdfrb replacement.
22
- - [ ] No active code path (Pipeline, SpreadRenderer, renderers) references
23
- deprecated modules.
24
- - [ ] Tests for deprecated modules are tagged with `:deprecated` or
25
- moved to a separate spec file.
26
- - [ ] `render.rb` autoloads keep the old modules for backward compatibility.
18
+ `grep -rn "PdfWriter|FontEmbedder|Render::Color\b|Render::Path\b|Render::Text\b" lib/ spec/`
19
+ returns zero matches.
27
20
 
28
- ## Dependencies
21
+ ## Acceptance criteria
29
22
 
30
- - pdfrb migration (DONE).
23
+ - [x] No live code references the old hand-rolled modules.
24
+ - [x] `bundle exec rake` is green (2314 examples, 0 failures).
@@ -1,31 +1,73 @@
1
1
  # TODO PDF 63: Replace FontMetrics with pdfrb measurement API
2
2
 
3
+ ## Status: BLOCKED (pdfrb measurement APIs are stubs)
4
+
3
5
  ## Goal
4
6
 
5
7
  Replace `Idml::TextEngine::FontMetrics` (200+ lines of TTF binary parsing
6
- via Fontisan) with pdfrb 0.15.0's native `Fonts#measure_text` and
7
- `Fonts#metrics_for`. Eliminates the Fontisan dependency for text layout.
8
+ via Fontisan) with pdfrb's native `Fonts#measure_text`, `#glyph_width`,
9
+ and `#metrics_for`. Eliminates the Fontisan dependency for text layout.
10
+
11
+ ## Blocker
12
+
13
+ pdfrb 0.4.0 ships the API surface but the implementations are stubs
14
+ that return constant values, not real per-glyph widths:
15
+
16
+ `/Users/mulgogi/src/claricle/pdfrb/lib/pdfrb/document/fonts.rb`:
17
+
18
+ ```ruby
19
+ def measure_text(text, font:, size:)
20
+ return 0 unless text
21
+ # TODO: use font metrics for per-glyph width lookup
22
+ text.to_s.length * (size || 0).to_f * 0.5
23
+ end
24
+
25
+ def glyph_width(_char, _resource)
26
+ 500
27
+ end
28
+
29
+ def metrics_for(_resource)
30
+ nil
31
+ end
32
+ ```
33
+
34
+ The Shaper and LineBreaker depend on accurate per-glyph widths for
35
+ correct word-wrap. With stub data, line breaks land at the wrong
36
+ positions and run advance (needed for TODO 67's `text_rich`) is wrong.
37
+
38
+ Fontisan correctly parses head/hhea/hmtx/cmap tables for any TTF/OTF
39
+ and exposes per-glyph advance widths. Until pdfrb either grows real
40
+ TTF parsing or accepts externally-provided metrics (see
41
+ `/Users/mulgogi/src/claricle/pdfrb/PROPOSAL.external-font-metrics.md`),
42
+ Fontisan stays.
43
+
44
+ ## Path forward
8
45
 
9
- ## Motivation
46
+ Proposal at `~/src/claricle/pdfrb/PROPOSAL.external-font-metrics.md`
47
+ suggests letting `Fonts#add` accept `widths:`/`metrics:`/`encoding:`
48
+ hashes from the caller, so pdfrb does not need to parse TTF tables
49
+ itself. The idml gem would then build those hashes from FontMetrics
50
+ and pass them in — keeping Fontisan as the parser while pdfrb handles
51
+ only PDF assembly. This is the Unix-philosophy split.
10
52
 
11
- pdfrb 0.15.0 now provides:
12
- - `document.fonts.measure_text(text, font:, size:)` → width in points
13
- - `document.fonts.metrics_for(font)` → { ascent:, descent:, cap_height:, ... }
53
+ Once that proposal lands:
14
54
 
15
- The idml gem's FontMetrics class duplicates this by parsing TTF tables
16
- (head, hhea, hmtx, cmap) via Fontisan. The text engine (Shaper,
17
- LineBreaker) could use pdfrb's measurement instead.
55
+ 1. `PdfrbWriter#register_font_with_metrics(path, widths:, metrics:, ...)`.
56
+ 2. Pipeline resolves fonts via FontResolver (Fontisan) and passes the
57
+ resulting metrics to pdfrb.
58
+ 3. Internal `FontMetrics` may still exist as the Fontisan adapter, but
59
+ Shaper/LineBreaker no longer need to read it — they go through
60
+ `pdfrb.measure_text`.
18
61
 
19
- ## Plan
62
+ ## Acceptance criteria (after pdfrb unblocks)
20
63
 
21
- 1. Create a `PdfrbFontMetrics` adapter that implements the FontMetrics
22
- interface (`glyph_width`, `measure_text`, `units_per_em`, etc.) using
23
- pdfrb's `Fonts#measure_text` and `#metrics_for`.
24
- 2. Update `FontResolver` to return `PdfrbFontMetrics` instances.
25
- 3. Remove `fontisan` from gemspec dependencies.
26
- 4. Remove `text_engine/font_metrics.rb` (200+ lines).
64
+ - [ ] Pipeline registers fonts with externally-provided metrics.
65
+ - [ ] Shaper/LineBreaker call pdfrb's measurement API, not FontMetrics.
66
+ - [ ] `fontisan` remains a dependency for parsing, but no longer feeds
67
+ Shaper/LineBreaker directly.
27
68
 
28
69
  ## Dependencies
29
70
 
30
- - pdfrb 0.15.0 (DONE — has measure_text and metrics_for)
31
- - TODO 62 (pdfrb 0.15.0 migration — DONE)
71
+ - pdfrb 0.4.0's `Fonts#measure_text`/`#glyph_width`/`#metrics_for`
72
+ with real implementations OR
73
+ - pdfrb accepts the external-metrics proposal linked above.
@@ -1,29 +1,29 @@
1
1
  # TODO PDF 65: pdfrb 0.19.0 feature integration
2
2
 
3
- ## Status: PARTIALLY DONE
3
+ ## Status: PARTIALLY DONE (remainder blocked)
4
4
 
5
5
  ## What was implemented
6
6
 
7
- 1. **Canvas#text_lines**: TextFrameRenderer uses `canvas.text_lines`
7
+ 1. **Canvas#text_lines**: `TextFrameRenderer` uses `canvas.text_lines`
8
8
  instead of N separate `canvas.text` calls per line. Single batch
9
9
  call for all lines within a run.
10
10
 
11
- ## What remains (pdfrb bugs)
11
+ ## What remains
12
12
 
13
- 2. **glyph_width for TTF**: pdfrb 0.19.0 has `Fonts#glyph_width` but
14
- returns 0 for TrueType fonts (bug: "undefined method glyph_id_for
15
- for String"). Standard 14 Type1 fonts work correctly. Cannot
16
- replace FontMetrics/Fontisan until this is fixed.
13
+ Blocked by pdfrb 0.4.0 stubs in `Fonts#measure_text` (returns
14
+ `length * 0.5 * size`) and `#glyph_width` (returns 500). Until real
15
+ per-glyph widths land:
17
16
 
18
- 3. **Pdfrb::FontResolver**: Returns nil for common fonts (Helvetica.ttc
19
- on macOS). Cannot replace Fontisan-based resolver yet.
20
-
21
- 4. **Font subsetting**: `Fonts#add` accepts `**opts` but `subset: true`
22
- behavior unverified. Standard 14 fonts don't need FontFile embedding.
17
+ 2. **`text_rich` for multi-run text**: tracked in TODO 67. Cannot
18
+ advance between runs without real measurement.
19
+ 3. **Replace FontMetrics with pdfrb measurement**: tracked in TODO 63.
20
+ 4. **Font subsetting**: tracked in TODO 52. `Fonts#add` accepts `**opts`
21
+ but `subset:` has no effect.
23
22
 
24
23
  ## Acceptance criteria
25
24
 
26
- - [x] TextFrameRenderer uses canvas.text_lines
27
- - [ ] FontMetrics replaced with pdfrb glyph_width (blocked by TTF bug)
28
- - [ ] FontResolver replaced with Pdfrb::FontResolver (blocked by .ttc)
29
- - [ ] Font subsetting verified
25
+ - [x] TextFrameRenderer uses `canvas.text_lines`
26
+ - [ ] FontMetrics replaced with pdfrb `glyph_width` (blocked TODO 63)
27
+ - [ ] FontResolver replaced with `Pdfrb::FontResolver` (blocked TODO 63)
28
+ - [ ] Font subsetting verified (blocked — TODO 52)
29
+ - [ ] `text_rich` used for multi-run frames (blocked — TODO 67)
@@ -0,0 +1,51 @@
1
+ # TODO PDF 67: Multi-run text batching via Canvas#text_rich
2
+
3
+ ## Status: BLOCKED (pdfrb measure_text is a stub)
4
+
5
+ ## Goal
6
+
7
+ Replace the per-run `Canvas#text` calls in `TextFrameRenderer#simple_render`
8
+ with a single `Canvas#text_rich` call per text frame. `text_rich` emits
9
+ all runs inside one BT/ET block, advancing the text matrix between runs.
10
+
11
+ ## Motivation
12
+
13
+ `text_rich` is the right primitive for multi-run text — one begin/end
14
+ text block per frame instead of N. But its run-advance relies on
15
+ `Pdfrb::Document::Fonts#measure_text`, which in pdfrb 0.4.0 returns a
16
+ stub `length * 0.5 * size`. Without accurate advance, runs overwrite
17
+ each other on the line.
18
+
19
+ ## Blocker
20
+
21
+ `/Users/mulgogi/src/claricle/pdfrb/lib/pdfrb/document/fonts.rb:65`:
22
+
23
+ ```ruby
24
+ def measure_text(text, font:, size:)
25
+ return 0 unless text
26
+ # TODO: use font metrics for per-glyph width lookup
27
+ text.to_s.length * (size || 0).to_f * 0.5
28
+ end
29
+ ```
30
+
31
+ `glyph_width` (line 79) is also a stub returning 500, and `metrics_for`
32
+ (line 87) returns nil. These need real Fontisan-backed implementations
33
+ before text_rich produces correct output.
34
+
35
+ ## Plan (after pdfrb unblocks)
36
+
37
+ 1. Build `runs` array of `{ text:, font:, size:, color: }` per line.
38
+ 2. Replace `simple_render` body with `canvas.text_lines(..., rich: true)`
39
+ or `canvas.text_rich(runs, at: [x, y])`.
40
+ 3. Verify rendered PDF: runs no longer overwrite, multi-color lines work.
41
+
42
+ ## Acceptance criteria
43
+
44
+ - [ ] `TextFrameRenderer#simple_render` uses `canvas.text_rich`.
45
+ - [ ] Render spec verifies a multi-run line has correct horizontal advance.
46
+ - [ ] Single BT/ET block per frame (assertion on PDF content stream).
47
+
48
+ ## Dependencies
49
+
50
+ - pdfrb 0.4.0 `Fonts#measure_text` returns real per-glyph widths.
51
+ - pdfrb 0.4.0 `Fonts#glyph_width` not returning stub 500.
@@ -0,0 +1,36 @@
1
+ # TODO PDF 68: Radial gradient shadings via pdfrb 0.4.0
2
+
3
+ ## Goal
4
+
5
+ RectangleRenderer routes gradient fills to either `Shadings#add_axial`
6
+ (linear) or `Shadings#add_radial` (radial) based on the IDML
7
+ `Gradient.type` attribute (`"Linear"` vs `"Radial"`).
8
+
9
+ ## Background
10
+
11
+ IDML `<Gradient Type="...">` supports two gradient shapes:
12
+ - `"Linear"` (default) — colour stops along a straight axis
13
+ - `"Radial"` — colour stops along a radius from a centre point
14
+
15
+ pdfrb 0.4.0 exposes both:
16
+ - `document.shadings.add_axial(from:, to:, stops:, extend:)`
17
+ - `document.shadings.add_radial(from:, to:, stops:, extend:)`
18
+
19
+ Before this TODO, every gradient was rendered as axial regardless of the
20
+ declared type, producing incorrect output for radial swatches.
21
+
22
+ ## Implementation
23
+
24
+ 1. Read `Gradient#type` (already modelled on `Elements::Gradient`).
25
+ 2. In `RectangleRenderer.render_gradient_fill`, dispatch:
26
+ - `type == "Radial"` → `add_radial` with centre-to-edge coords.
27
+ - otherwise → `add_axial` (existing behaviour).
28
+ 3. Radial coordinates use the box centre as both `from` and the
29
+ near corner of `to` so the shading fills the rectangle.
30
+
31
+ ## Acceptance criteria
32
+
33
+ - [x] `Gradient.type == "Radial"` produces a pdfrb radial shading.
34
+ - [x] `Gradient.type == "Linear"` (or nil) keeps producing an axial shading.
35
+ - [x] Spec covers both branches.
36
+ - [x] All existing specs still pass.
@@ -0,0 +1,84 @@
1
+ # TODO PDF 69: Transparency and blend modes via pdfrb 0.4.0
2
+
3
+ ## Goal
4
+
5
+ Honor IDML `<BlendingSetting BlendMode="..." Opacity="..."/>` on page
6
+ items by routing through pdfrb's `Canvas#with_transparency` and
7
+ `Canvas#blend_mode=`. Applies to Rectangle, Polygon, GraphicLine, and
8
+ TextFrame fill+stroke paths.
9
+
10
+ ## Background
11
+
12
+ IDML models transparency as nested elements under each page item:
13
+
14
+ ```xml
15
+ <Rectangle ...>
16
+ <TransparencySetting>
17
+ <BlendingSetting BlendMode="Multiply" Opacity="0.5"
18
+ KnockoutGroup="true" IsolateBlending="false"/>
19
+ <DropShadowSetting .../> <!-- not handled here -->
20
+ </TransparencySetting>
21
+ </Rectangle>
22
+ ```
23
+
24
+ `TransparencySetting` lives as a child of the page item (sibling of
25
+ `Properties`). Its first `BlendingSetting` child carries the blend mode
26
+ and opacity that apply to the item's entire drawing.
27
+
28
+ pdfrb 0.4.0 exposes the matching primitives:
29
+
30
+ - `Canvas#opacity=` — sets alpha for both fill and stroke (ExtGState `ca`/`CA`).
31
+ - `Canvas#blend_mode=` — sets BM name.
32
+ - `Canvas#with_transparency(opacity:, blend_mode:) { ... }` — saves graphics state, applies both, restores on exit.
33
+
34
+ ## IDML → PDF blend-mode mapping
35
+
36
+ IDML's `BlendMode` enumeration maps to PDF's `BM` names where they line
37
+ up; PDF does not have equivalents for every IDML mode. Initial mapping:
38
+
39
+ | IDML | PDF |
40
+ |--------------------|---------------|
41
+ | `Normal` | `Normal` |
42
+ | `Multiply` | `Multiply` |
43
+ | `Screen` | `Screen` |
44
+ | `Overlay` | `Overlay` |
45
+ | `SoftLight` | `SoftLight` |
46
+ | `HardLight` | `HardLight` |
47
+ | `Darken` | `Darken` |
48
+ | `Lighten` | `Lighten` |
49
+ | `ColorDodge` | `ColorDodge` |
50
+ | `ColorBurn` | `ColorBurn` |
51
+ | `Difference` | `Difference` |
52
+ | `Exclusion` | `Exclusion` |
53
+ | `Hue` | `Hue` |
54
+ | `Saturation` | `Saturation` |
55
+ | `Color` | `Color` |
56
+ | `Luminosity` | `Luminosity` |
57
+
58
+ Unknown modes fall back to `Normal`.
59
+
60
+ ## Implementation
61
+
62
+ 1. Add `transparency_setting` attribute to `Rectangle`, `Polygon`,
63
+ `GraphicLine`, `TextFrame`, `Group` (RNC lists it on every page item).
64
+ 2. Model `TransparencySetting` with a `blending_setting` child.
65
+ 3. New helper `Render::Blending` that maps an `Elements::BlendingSetting`
66
+ to a `{ opacity:, blend_mode: }` hash.
67
+ 4. In each renderer, wrap the existing fill/stroke block in
68
+ `canvas.with_transparency(**blending_args) { ... }` when a setting is
69
+ present.
70
+ 5. Reuse the existing `save_graphics_state` block — `with_transparency`
71
+ already saves/restores, so renderers call it in place of the plain
72
+ save block when blending applies.
73
+
74
+ ## Acceptance criteria
75
+
76
+ - [ ] IDML `<BlendingSetting Opacity="0.5"/>` halves the item's alpha in the output PDF.
77
+ - [ ] IDML `<BlendingSetting BlendMode="Multiply"/>` sets `/BM /Multiply` in ExtGState.
78
+ - [ ] Items without BlendingSetting render exactly as before (no ExtGState emitted).
79
+ - [ ] Spec covers opacity, blend_mode, and pass-through cases.
80
+
81
+ ## Dependencies
82
+
83
+ - pdfrb 0.4.0 `Canvas#with_transparency` (DONE — confirmed in pdfrb 0.4.0).
84
+ - pdfrb 0.4.0 `Canvas#blend_mode=` (DONE).
@@ -0,0 +1,32 @@
1
+ # TODO PDF 70: Stroke styling — line cap, join, dash patterns
2
+
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`.
19
+
20
+ ## Acceptance criteria
21
+
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.
27
+
28
+ ## Dependencies
29
+
30
+ - pdfrb 0.4.0 `line_cap=`, `line_join=`, `miter_limit=`, `dash_pattern=`
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.