idml 0.6.0 → 0.6.2

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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +1 -3
  3. data/.gitignore +1 -0
  4. data/Gemfile +2 -1
  5. data/TODO.pdf/01-font-metrics.md +6 -0
  6. data/TODO.pdf/02-font-resolver.md +5 -0
  7. data/TODO.pdf/03-text-shaper.md +4 -0
  8. data/TODO.pdf/04-line-breaker-justifier.md +4 -0
  9. data/TODO.pdf/05-vertical-layout.md +4 -0
  10. data/TODO.pdf/06-pdf-color-operators.md +5 -0
  11. data/TODO.pdf/07-pdf-path-operators.md +5 -0
  12. data/TODO.pdf/08-pdf-text-operators.md +5 -0
  13. data/TODO.pdf/09-image-embedding.md +5 -0
  14. data/TODO.pdf/10-spread-renderer.md +5 -0
  15. data/TODO.pdf/100-character-level-styling.md +61 -0
  16. data/TODO.pdf/101-paragraph-rules-borders.md +38 -0
  17. data/TODO.pdf/102-anchored-objects.md +41 -0
  18. data/TODO.pdf/103-drop-caps.md +33 -0
  19. data/TODO.pdf/104-table-schema-completion.md +44 -0
  20. data/TODO.pdf/105-whole-package-round-trip.md +45 -0
  21. data/TODO.pdf/106-page-item-type-coverage.md +62 -0
  22. data/TODO.pdf/107-image-rendering-unification.md +42 -0
  23. data/TODO.pdf/11-font-embedding.md +6 -0
  24. data/TODO.pdf/12-idml-to-pdf-pipeline.md +6 -0
  25. data/TODO.pdf/13-cjk-text-layout.md +5 -0
  26. data/TODO.pdf/14-page-item-element-models.md +5 -0
  27. data/TODO.pdf/15-wire-spread-children.md +5 -0
  28. data/TODO.pdf/16-typed-model-pipeline.md +5 -0
  29. data/TODO.pdf/17-shape-rendering.md +5 -0
  30. data/TODO.pdf/18-text-rendering.md +5 -0
  31. data/TODO.pdf/19-page-dimensions.md +4 -0
  32. data/TODO.pdf/20-png-image-support.md +4 -0
  33. data/TODO.pdf/21-coordinate-transform.md +5 -0
  34. data/TODO.pdf/22-color-resolution.md +5 -0
  35. data/TODO.pdf/23-fontmetrics-ps-name-fix.md +5 -0
  36. data/TODO.pdf/24-antipattern-coverage.md +5 -0
  37. data/TODO.pdf/25-cli-render.md +4 -0
  38. data/TODO.pdf/26-renderer-registry.md +5 -0
  39. data/TODO.pdf/27-text-engine-integration.md +6 -0
  40. data/TODO.pdf/28-path-geometry-models.md +6 -0
  41. data/TODO.pdf/29-character-style-extraction.md +5 -0
  42. data/TODO.pdf/30-shape-geometry-rendering.md +6 -0
  43. data/TODO.pdf/31-stroke-rendering.md +6 -0
  44. data/TODO.pdf/32-master-spread-rendering.md +6 -0
  45. data/TODO.pdf/33-font-resolution-from-idml.md +7 -0
  46. data/TODO.pdf/34-story-threading.md +5 -0
  47. data/TODO.pdf/35-pdf-metadata.md +6 -0
  48. data/TODO.pdf/36-performance-caching.md +5 -0
  49. data/TODO.pdf/37-round-trip-specs.md +5 -0
  50. data/TODO.pdf/38-pdf-output-validation.md +5 -0
  51. data/TODO.pdf/41-graphic-line-rendering.md +5 -0
  52. data/TODO.pdf/42-group-transform-application.md +5 -0
  53. data/TODO.pdf/43-text-frame-insets.md +6 -0
  54. data/TODO.pdf/44-layer-support.md +6 -0
  55. data/TODO.pdf/45-image-clipping.md +4 -0
  56. data/TODO.pdf/46-spread-to-page-separation.md +5 -0
  57. data/TODO.pdf/47-pdf-bookmarks.md +6 -0
  58. data/TODO.pdf/48-table-rendering.md +7 -0
  59. data/TODO.pdf/49-gradient-fill-rendering.md +6 -0
  60. data/TODO.pdf/50-pdf-a-compliance.md +6 -0
  61. data/TODO.pdf/68-radial-gradient-shadings.md +4 -0
  62. data/TODO.pdf/69-transparency-blend-modes.md +6 -0
  63. data/TODO.pdf/94-text-frame-vertical-layout.md +63 -0
  64. data/TODO.pdf/95-paragraph-spacing-indents.md +50 -0
  65. data/TODO.pdf/96-text-frame-insets-actual.md +39 -0
  66. data/TODO.pdf/97-table-cell-rendering-fidelity.md +62 -0
  67. data/TODO.pdf/98-rowspan-columnspan.md +35 -0
  68. data/TODO.pdf/99-table-column-widths.md +40 -0
  69. data/TODO.pdf/README.md +6 -9
  70. data/idml.gemspec +2 -1
  71. data/lib/idml/elements/table_cell.rb +2 -2
  72. data/lib/idml/render/font_setup.rb +9 -0
  73. data/lib/idml/version.rb +1 -1
  74. metadata +39 -8
  75. data/Gemfile.lock +0 -235
@@ -1,5 +1,10 @@
1
1
  # TODO PDF 21: Coordinate transform utility
2
2
 
3
+ ## Status: DONE — Centralized in `Idml::Geometry` (Y-flip + matrix math)
4
+ and `Render::Placement` (item → PDF rect, with memoized
5
+ `geometric_bounds`). All renderers use `Placement.box`. See
6
+ `lib/idml/geometry.rb`, `lib/idml/render/placement.rb`, TODO 39.
7
+
3
8
  ## Goal
4
9
 
5
10
  Centralize IDML-to-PDF coordinate system conversion in a dedicated
@@ -1,5 +1,10 @@
1
1
  # TODO PDF 22: Color resolution from Resources
2
2
 
3
+ ## Status: DONE — `Render::ColorResolver` resolves `Color/...` and
4
+ `Swatch/...` references from `Parts::Graphic` into RGB/CMYK tuples.
5
+ `[None]` and `[Registration]` handled specially. See
6
+ `lib/idml/render/color_resolver.rb`.
7
+
3
8
  ## Goal
4
9
 
5
10
  Resolve IDML color references (e.g., "Color/Red", "Color/c0m100y100k0")
@@ -1,5 +1,10 @@
1
1
  # TODO PDF 23: FontMetrics PostScript name fix
2
2
 
3
+ ## Status: OBSOLETE — The original `FontMetrics` class (ttfunk-backed)
4
+ was replaced by `TextEngine::PdfrbFontMetrics`, which delegates to
5
+ pdfrb's typed Fonts API. PS name extraction is now pdfrb's
6
+ responsibility. See TODOs 63, 65.
7
+
3
8
  ## Goal
4
9
 
5
10
  Fix the `FontMetrics#postscript_name` method to prefer platform 3
@@ -1,5 +1,10 @@
1
1
  # TODO PDF 24: Anti-pattern coverage for render modules
2
2
 
3
+ ## Status: DONE — `spec/anti_patterns_spec.rb` auto-discovers every
4
+ `.rb` file under `lib/` and asserts no `nokogiri`, `rexml`,
5
+ `require_relative`, `instance_variable_set`, `.send(`, `respond_to?`,
6
+ `double()`, hand-rolled serializers, etc. Currently 279 files, 0 offenses.
7
+
3
8
  ## Goal
4
9
 
5
10
  Ensure all new render and text-engine modules pass the anti-pattern
@@ -1,5 +1,9 @@
1
1
  # TODO PDF 25: CLI render subcommand
2
2
 
3
+ ## Status: DONE — `idml render input.idml output.pdf` exists with
4
+ `--font-path`, `--pdfa`, `--tagged`, `--no-subset`, `--compress`, and
5
+ `--verbose` flags. See `lib/idml/cli.rb` and TODO 60.
6
+
3
7
  ## Goal
4
8
 
5
9
  Add a `render` subcommand to the `idml` CLI that converts an IDML
@@ -1,5 +1,10 @@
1
1
  # TODO PDF 26: Page-item renderer registry (OCP)
2
2
 
3
+ ## Status: DONE — `PageItemRenderer::RENDERER_MAP` is a class-level
4
+ Hash mapping element class → renderer class. Adding a new page-item
5
+ type = appending to RENDERER_MAP, not editing a switch statement.
6
+ See `lib/idml/render/page_item_renderer.rb`, `lib/idml/render/renderers/`.
7
+
3
8
  ## Goal
4
9
 
5
10
  Replace the `case/when` dispatch in `SpreadRenderer` with a registration-
@@ -1,5 +1,11 @@
1
1
  # TODO PDF 27: Text engine integration
2
2
 
3
+ ## Status: DONE — `TextFrameRenderer` runs each story through the
4
+ text engine: `StyleResolver` extracts runs → `Shaper` shapes glyphs →
5
+ `LineBreaker` wraps → `Justifier` aligns → `VerticalLayout` positions.
6
+ Falls back to pdfrb's `text_rich` when metrics are missing. See
7
+ `lib/idml/render/renderers/text_frame_renderer.rb` and TODO 67.
8
+
3
9
  ## Goal
4
10
 
5
11
  Wire the text engine (Shaper → LineBreaker → Justifier → VerticalLayout)
@@ -1,5 +1,11 @@
1
1
  # TODO PDF 28: Path geometry models
2
2
 
3
+ ## Status: DONE — `Idml::Elements::{PathPointType, PathPointArray,
4
+ GeometryPathType, PathGeometry, Properties}` all carry the RNC
5
+ attributes. Page items expose `geometric_bounds` via memoized helpers.
6
+ See `lib/idml/elements/{path_point_type,path_point_array,
7
+ geometry_path_type,path_geometry,properties}.rb`.
8
+
3
9
  ## Goal
4
10
 
5
11
  Create typed element models for IDML's path geometry hierarchy:
@@ -1,5 +1,10 @@
1
1
  # TODO PDF 29: Character style extraction
2
2
 
3
+ ## Status: DONE — `Render::StyleResolver` walks PSR → CSR and emits
4
+ `StyledRun` structs (text + font + size + color + style attributes).
5
+ Per-run font resolution is wired in `TextFrameRenderer#font_for_run`
6
+ via `font_map`. See `lib/idml/render/style_resolver.rb` and TODO 93.
7
+
3
8
  ## Goal
4
9
 
5
10
  Extract font family, size, color, and style attributes from
@@ -1,5 +1,11 @@
1
1
  # TODO PDF 30: Shape geometry rendering
2
2
 
3
+ ## Status: DONE — RectangleRenderer / PolygonRenderer / GraphicLineRenderer
4
+ derive bounds from `PathGeometry` and emit pdfrb path calls.
5
+ ItemTransform + Y-flip handled by `Placement.box`. See
6
+ `lib/idml/render/renderers/{rectangle,polygon,graphic_line}_renderer.rb`
7
+ and TODOs 21, 39.
8
+
3
9
  ## Goal
4
10
 
5
11
  Render Rectangle, Polygon, and GraphicLine shapes using their actual
@@ -1,5 +1,11 @@
1
1
  # TODO PDF 31: Stroke rendering
2
2
 
3
+ ## Status: DONE — `Render::StrokeStyle.apply(canvas, item)` saves
4
+ graphics state, applies line_cap / line_join / miter_limit / dash
5
+ patterns from DashedStrokeStyle / DottedStrokeStyle / StripedStrokeStyle,
6
+ and yields. Used by every shape renderer. See
7
+ `lib/idml/render/stroke_style.rb` and TODO 70.
8
+
3
9
  ## Goal
4
10
 
5
11
  Render stroke attributes on page items: stroke color, weight, end cap,
@@ -1,5 +1,11 @@
1
1
  # TODO PDF 32: Master spread rendering
2
2
 
3
+ ## Status: DONE — `SpreadRenderer#render_master_items` resolves each
4
+ Page's `AppliedMaster` via `Package#master_spread_by_id` and renders
5
+ the master's items first as a background layer. Layer visibility
6
+ applied via `LayerFilter`. See `lib/idml/render/spread_renderer.rb`,
7
+ TODO 44.
8
+
3
9
  ## Goal
4
10
 
5
11
  Render master spread page items as background on each page. Master
@@ -1,5 +1,12 @@
1
1
  # TODO PDF 33: Font resolution from IDML
2
2
 
3
+ ## Status: DONE — `Parts::Fonts` parses FontFamily → Font entries
4
+ (`postscript_name` accessor). `FontReferenceResolver.build(package)`
5
+ constructs the family → PSName lookup. `FontSetup` resolves to a file
6
+ path via pdfrb's FontResolver. See TODOs 40, 93 and
7
+ `lib/idml/render/font_reference_resolver.rb`,
8
+ `lib/idml/render/font_setup.rb`.
9
+
3
10
  ## Goal
4
11
 
5
12
  Resolve fonts referenced in IDML documents using Resources/Fonts.xml.
@@ -1,5 +1,10 @@
1
1
  # TODO PDF 34: Story threading
2
2
 
3
+ ## Status: DONE — `Render::StoryThreader` walks the
4
+ PreviousTextFrame / NextTextFrame chain, distributes story text
5
+ across linked frames, and guards against cycles. See
6
+ `lib/idml/story_threader.rb`.
7
+
3
8
  ## Goal
4
9
 
5
10
  Follow the PreviousTextFrame/NextTextFrame chain to thread stories
@@ -1,5 +1,11 @@
1
1
  # TODO PDF 35: PDF metadata
2
2
 
3
+ ## Status: DONE — `Render::MetadataBuilder` reads XMP from
4
+ `META-INF/metadata.xml` via `Parts::XmpMeta` (typed dc:/xmp:/rdf:
5
+ namespace composition), merges with defaults (Producer, CreationDate),
6
+ and writes via `PdfrbWriter#set_info`. See
7
+ `lib/idml/render/metadata_builder.rb`, `lib/idml/parts/xmp.rb`, TODO 75.
8
+
3
9
  ## Goal
4
10
 
5
11
  Embed document metadata (Title, Author, Subject, Keywords, Creator,
@@ -1,5 +1,10 @@
1
1
  # TODO PDF 36: Performance caching
2
2
 
3
+ ## Status: DONE — `PdfrbFontMetrics` is reused across the whole render
4
+ (a single pdfrb `Fonts` collection is shared). `Placement.box` memoizes
5
+ `geometric_bounds` per page item. `ColorResolver` and `Package#part`
6
+ memoize per instance. See TODO 86 for benchmarks.
7
+
3
8
  ## Goal
4
9
 
5
10
  Cache expensive operations: FontMetrics file parsing, FontResolver
@@ -1,5 +1,10 @@
1
1
  # TODO PDF 37: Round-trip specs for element models
2
2
 
3
+ ## Status: DONE — Every typed element class has round-trip specs under
4
+ `spec/idml/elements/` and `spec/idml/parts/` (parse → serialize →
5
+ xml-equivalent). The `sample-with-image` fixture's 14 parts are
6
+ validated against the RNC schemas; see also TODO 20 in TODO.complete.
7
+
3
8
  ## Goal
4
9
 
5
10
  Add round-trip specs for every page-item element model: `from_xml(xml)`
@@ -1,5 +1,10 @@
1
1
  # TODO PDF 38: PDF output validation
2
2
 
3
+ ## Status: DONE — pdfrb 0.6.0 emits spec-compliant xref (20-byte
4
+ entries), valid object streams, and matching `endobj`s. Pipeline specs
5
+ under `spec/idml/render/render_pdfrb_pipeline_spec.rb` round-trip
6
+ the PDF through pdfrb's parser to confirm validity. See TODOs 54, 92.
7
+
3
8
  ## Goal
4
9
 
5
10
  Validate that the PDF files produced by the Pipeline are structurally
@@ -1,5 +1,10 @@
1
1
  # TODO PDF 41: GraphicLine rendering
2
2
 
3
+ ## Status: DONE — `Render::GraphicLineRenderer` (registered in
4
+ `PageItemRenderer::RENDERER_MAP`) emits move-to / line-to via pdfrb
5
+ Canvas. StrokeColor and StrokeWeight via `StrokeStyle.apply`. See
6
+ `lib/idml/render/renderers/graphic_line_renderer.rb`.
7
+
3
8
  ## Goal
4
9
 
5
10
  Implement `GraphicLineRenderer` to render IDML `<GraphicLine>` elements as
@@ -1,5 +1,10 @@
1
1
  # TODO PDF 42: Group transform application
2
2
 
3
+ ## Status: DONE — `Render::GroupRenderer` saves graphics state,
4
+ applies the Group's ItemTransform via `canvas.concat_matrix`, renders
5
+ children, and restores. Nested groups recurse correctly. See
6
+ `lib/idml/render/renderers/group_renderer.rb`.
7
+
3
8
  ## Goal
4
9
 
5
10
  Apply a Group's ItemTransform to all child page items during rendering.
@@ -1,5 +1,11 @@
1
1
  # TODO PDF 43: Text frame insets
2
2
 
3
+ ## Status: DONE — `Elements::TextFramePreference` carries the inset
4
+ attributes (InsetTop / InsetLeft / InsetBottom / InsetRight).
5
+ `TextFrameRenderer` subtracts them from the frame bounds before
6
+ layout. See `lib/idml/elements/text_frame_preference.rb`,
7
+ `lib/idml/render/renderers/text_frame_renderer.rb`.
8
+
3
9
  ## Goal
4
10
 
5
11
  Respect `TextFramePreference` inset margins (InsetTop, InsetLeft,
@@ -1,5 +1,11 @@
1
1
  # TODO PDF 44: Layer support
2
2
 
3
+ ## Status: DONE — `Render::LayerFilter.from_designmap(designmap)`
4
+ constructs the layer visibility map from the designmap `<Layer>`
5
+ entries. `SpreadRenderer` consults `LayerFilter#visible?(item)` for
6
+ both master and spread items. See `lib/idml/render/layer_filter.rb`,
7
+ `lib/idml/elements/layer.rb`.
8
+
3
9
  ## Goal
4
10
 
5
11
  Parse Layer definitions from designmap.xml. Each page item's
@@ -1,5 +1,9 @@
1
1
  # TODO PDF 45: Image clipping
2
2
 
3
+ ## Status: DONE — `SpreadRenderer#apply_image_clip` emits a pdfrb
4
+ clip rectangle from the parent frame's `PathGeometry` before
5
+ `draw_image_matrix`. See `lib/idml/render/spread_renderer.rb`.
6
+
3
7
  ## Goal
4
8
 
5
9
  Clip placed images to their parent Rectangle's bounds. Currently
@@ -1,5 +1,10 @@
1
1
  # TODO PDF 46: Spread-to-page separation
2
2
 
3
+ ## Status: DONE — `Pipeline#render_spread` iterates each Page in the
4
+ Spread separately, calls `writer.add_page` with per-page dimensions,
5
+ and tracks the global `page_index` for hyperlink/destination wiring.
6
+ See `lib/idml/render/pipeline.rb` and TODO 19.
7
+
3
8
  ## Goal
4
9
 
5
10
  Render each IDML Page as a separate PDF page (currently one PDF page
@@ -1,5 +1,11 @@
1
1
  # TODO PDF 47: PDF bookmarks/outlines
2
2
 
3
+ ## Status: DONE — `Render::BookmarkResolver` walks the designmap
4
+ bookmark entries and yields `(title, page_index)` tuples;
5
+ `PdfrbWriter#add_bookmark` builds the Outlines tree. See
6
+ `lib/idml/bookmark_resolver.rb`, `lib/idml/render/pdfrb_writer.rb`,
7
+ and TODO 79.
8
+
3
9
  ## Goal
4
10
 
5
11
  Generate PDF outline (bookmark) entries from the document's XML
@@ -1,5 +1,12 @@
1
1
  # TODO PDF 48: Table rendering
2
2
 
3
+ ## Status: DONE — `Render::TableRenderer` handles both the
4
+ schema-faithful layout (Table > {Cell, Row} siblings, real IDML) and
5
+ the legacy nested model. SchemaLayout computes per-cell rects from
6
+ `Row#single_row_height` and `Cell#col_row`. Inline Tables discovered
7
+ via `CSR#table`. See `lib/idml/render/renderers/table_renderer.rb`,
8
+ `lib/idml/elements/{table,cell,row}.rb`, and TODOs 82, 84.
9
+
3
10
  ## Goal
4
11
 
5
12
  Render IDML tables (TableRow, TableCell elements within Stories) as
@@ -1,5 +1,11 @@
1
1
  # TODO PDF 49: Gradient fill rendering
2
2
 
3
+ ## Status: DONE — `Render::GradientResolver` maps IDML Gradient +
4
+ GradientStop entries to pdfrb shading dictionaries
5
+ (`Shadings#add_axial` / `add_radial`). RectangleRenderer / PolygonRenderer
6
+ route gradient fills through this resolver. See
7
+ `lib/idml/gradient_resolver.rb` and TODOs 66, 68.
8
+
3
9
  ## Goal
4
10
 
5
11
  Render gradient fills on page items. IDML supports linear and radial
@@ -1,5 +1,11 @@
1
1
  # TODO PDF 50: PDF/A compliance mode
2
2
 
3
+ ## Status: DONE — `Pipeline#apply_compliance` attaches the PDF/A-2a
4
+ XMP packet (`Render::PdfaPacket`) and embeds the sRGB ICC profile
5
+ (`Render::IccProfile`). Pipeline option `compliance: :pdfa2a`. CLI
6
+ exposes `--pdfa`. See `lib/idml/render/{pdfa_packet,icc_profile}.rb`,
7
+ TODOs 60, 77, 81.
8
+
3
9
  ## Goal
4
10
 
5
11
  Add a `--pdf-a` option that produces PDF/A-1a, PDF/A-2a, or PDF/A-3a
@@ -1,5 +1,9 @@
1
1
  # TODO PDF 68: Radial gradient shadings via pdfrb 0.4.0
2
2
 
3
+ ## Status: DONE — RectangleRenderer dispatches on `Gradient#type`:
4
+ `"Radial"` → `Shadings#add_radial`, otherwise → `Shadings#add_axial`.
5
+ Spec covers both branches.
6
+
3
7
  ## Goal
4
8
 
5
9
  RectangleRenderer routes gradient fills to either `Shadings#add_axial`
@@ -1,5 +1,11 @@
1
1
  # TODO PDF 69: Transparency and blend modes via pdfrb 0.4.0
2
2
 
3
+ ## Status: DONE — `Render::Blending.wrap(canvas, setting, &block)`
4
+ maps IDML `BlendingSetting` (BlendMode + Opacity) to pdfrb's
5
+ `canvas.with_transparency(opacity:, blend_mode:)`. Used by every
6
+ shape renderer. See `lib/idml/render/blending.rb`,
7
+ `lib/idml/elements/blending_setting.rb`.
8
+
3
9
  ## Goal
4
10
 
5
11
  Honor IDML `<BlendingSetting BlendMode="..." Opacity="..."/>` on page
@@ -0,0 +1,63 @@
1
+ # TODO PDF 94: TextFrameRenderer should delegate to VerticalLayout
2
+
3
+ ## Status: OPEN — gap identified in 2026-08-08 audit
4
+
5
+ ## Problem
6
+
7
+ `TextEngine::VerticalLayout` exists (lib/idml/text_engine/vertical_layout.rb)
8
+ with full support for insets, leading, paragraph spacing, and indents.
9
+ `TextFrameRenderer#render_run_lines` reinvents that math inline and
10
+ ignores every paragraph-level attribute:
11
+
12
+ - No TextFramePreference insets (InsetTop / InsetLeft / InsetBottom /
13
+ InsetRight) subtracted from the layout box.
14
+ - No SpaceBefore / SpaceAfter applied between paragraphs.
15
+ - No FirstLineIndent / LeftIndent / RightIndent applied within paragraphs.
16
+ - No AutoLeading honored — always uses `size * 1.2`.
17
+ - No DropCapLines / DropCapCharacters.
18
+
19
+ The current `engine_render` walks CSR runs as a flat list, so it
20
+ can't even see paragraph boundaries (PSR is the paragraph boundary;
21
+ CSR is a run within a paragraph).
22
+
23
+ ## What needs to happen
24
+
25
+ 1. **Group runs by paragraph** in `StyleResolver.extract_runs` —
26
+ return a list of `Paragraph` structs, each carrying:
27
+ - `runs:` — array of `StyledRun`
28
+ - `space_before:`, `space_after:` — from PSR
29
+ - `first_line_indent:`, `left_indent:`, `right_indent:` — from PSR
30
+ - `auto_leading:` — from PSR
31
+ - `alignment:` — already wired today
32
+ 2. **Replace `render_run_lines`** with a call to
33
+ `VerticalLayout.layout(lines:, frame:, font_size:, leading:,
34
+ space_before:, space_after:, first_line_indent:, left_indent:)`.
35
+ 3. **Subtract insets** from the frame box in `frame_box` —
36
+ read TextFramePreference InsetTop / InsetLeft / InsetBottom /
37
+ InsetRight and pass them to VerticalLayout::Frame.
38
+ 4. **Emit positioned glyphs** from VerticalLayout via
39
+ `canvas.text` per line (already the case today, but with
40
+ VerticalLayout-computed positions).
41
+
42
+ ## Architecture rationale
43
+
44
+ - **DRY**: removes duplicate line-positioning logic.
45
+ - **MECE**: VerticalLayout owns vertical placement; TextFrameRenderer
46
+ owns run extraction + canvas emission.
47
+ - **OCP**: new paragraph attributes (drop caps, gyoudori) extend
48
+ VerticalLayout, not TextFrameRenderer.
49
+
50
+ ## Acceptance criteria
51
+
52
+ - [ ] `StyleResolver.extract_runs` returns paragraphs (not flat runs)
53
+ when called with paragraph-aware mode.
54
+ - [ ] TextFramePreference insets shrink the layout box.
55
+ - [ ] PSR SpaceBefore adds space before the paragraph's first line.
56
+ - [ ] PSR FirstLineIndent shifts the first line right.
57
+ - [ ] PSR AutoLeading overrides the 1.2 factor.
58
+ - [ ] All existing specs still pass.
59
+ - [ ] New specs for each paragraph attribute.
60
+
61
+ ## Dependencies
62
+
63
+ - None — pure refactor on top of existing typed models.
@@ -0,0 +1,50 @@
1
+ # TODO PDF 95: Paragraph spacing and indents (SpaceBefore, SpaceAfter, FirstLineIndent)
2
+
3
+ ## Status: OPEN — gap identified in 2026-08-08 audit
4
+
5
+ ## Problem
6
+
7
+ `ParagraphStyleRange` carries 284 attributes from the RNC schema.
8
+ Today the renderer honors exactly one of them: `Justification`
9
+ (via `StyleResolver::ALIGNMENT_MAP`).
10
+
11
+ The following paragraph-level layout attributes are typed on the
12
+ model but never read by the renderer:
13
+
14
+ | Attribute | Effect on layout |
15
+ |-------------------------|---------------------------------------------------|
16
+ | `SpaceBefore` | vertical gap above the paragraph |
17
+ | `SpaceAfter` | vertical gap below the paragraph |
18
+ | `FirstLineIndent` | left offset for the first line only |
19
+ | `LeftIndent` | left offset for all lines |
20
+ | `RightIndent` | right offset (shrinks frame width) |
21
+ | `AutoLeading` | multiplier (1.2 default) for line height |
22
+ | `DropCapLines` | drop-cap height in lines |
23
+ | `DropCapCharacters` | drop-cap width in characters |
24
+ | `LastLineIndent` | right offset for the last line |
25
+ | `MinimumWordSpacing` | justification clamp |
26
+ | `MaximumWordSpacing` | justification clamp |
27
+ | `DesiredWordSpacing` | justification target |
28
+
29
+ ## What needs to happen
30
+
31
+ 1. Extend `StyledRun` (or wrap it in a `Paragraph` struct per TODO 94)
32
+ to carry these attributes from PSR.
33
+ 2. Wire `Justifier` to honor `Minimum/Maximum/DesiredWordSpacing`
34
+ when distributing slack in justified text.
35
+ 3. Wire `VerticalLayout` to honor `SpaceBefore` / `SpaceAfter` /
36
+ `AutoLeading` (already accepts them, but the renderer doesn't pass them).
37
+ 4. Wire `FirstLineIndent` / `LeftIndent` / `RightIndent` in VerticalLayout.
38
+
39
+ ## Acceptance criteria
40
+
41
+ - [ ] `psr.space_before = 12` produces 12pt gap before the paragraph.
42
+ - [ ] `psr.first_line_indent = 24` indents only the first line by 24pt.
43
+ - [ ] `psr.left_indent = 18` indents all lines by 18pt.
44
+ - [ ] `psr.right_indent = 18` shrinks the line-wrap width by 18pt.
45
+ - [ ] `psr.auto_leading = 1.5` produces 1.5× line height.
46
+ - [ ] Word-spacing clamps apply in justified mode.
47
+
48
+ ## Dependencies
49
+
50
+ - TODO 94 (VerticalLayout delegation).
@@ -0,0 +1,39 @@
1
+ # TODO PDF 96: TextFramePreference insets honored in layout
2
+
3
+ ## Status: OPEN — gap identified in 2026-08-08 audit
4
+
5
+ ## Problem
6
+
7
+ `Elements::TextFramePreference` carries the four text insets:
8
+
9
+ - `InsetTop`
10
+ - `InsetLeft`
11
+ - `InsetBottom`
12
+ - `InsetRight`
13
+
14
+ `TextFrame#text_frame_preference` exposes the typed child. But
15
+ `TextFrameRenderer#frame_box` reads only `Placement.box(frame, ...)`
16
+ — it doesn't subtract insets from the box before laying out text.
17
+
18
+ Result: text starts at the frame's geometric edge, ignoring the
19
+ inset margin the document declared. Visually wrong for any frame
20
+ that uses non-zero insets (which is most real-world IDML documents).
21
+
22
+ ## What needs to happen
23
+
24
+ 1. `frame_box` reads `frame.text_frame_preference` and extracts
25
+ `inset_top`, `inset_left`, `inset_bottom`, `inset_right`.
26
+ 2. Subtracts left/right from `box[:width]`, adds left to `box[:x]`.
27
+ 3. Subtracts top/bottom from `box[:height]`, adds bottom to `box[:y]`.
28
+ 4. Passes the resulting inset-aware box to VerticalLayout.
29
+
30
+ ## Acceptance criteria
31
+
32
+ - [ ] TextFrame with InsetTop=20 lays first line 20pt below frame top.
33
+ - [ ] TextFrame with InsetLeft=15 shifts first glyph 15pt right of frame left.
34
+ - [ ] TextFrame with no TextFramePreference child renders as today (zero insets).
35
+ - [ ] Spec: parse fixture TextFrame, verify insets honored.
36
+
37
+ ## Dependencies
38
+
39
+ - TODO 94 (VerticalLayout delegation uses the inset-adjusted box).
@@ -0,0 +1,62 @@
1
+ # TODO PDF 97: Table cell rendering — fill, stroke, insets, vertical justification
2
+
3
+ ## Status: OPEN — gap identified in 2026-08-08 audit
4
+
5
+ ## Problem
6
+
7
+ `TableRenderer#render_cell_text` is a stub:
8
+
9
+ ```ruby
10
+ def self.render_cell_text(canvas, cell, x, y, height, context)
11
+ text = cell.text_content
12
+ return if text.nil? || text.empty?
13
+
14
+ runs = [{
15
+ text: text,
16
+ font: context.font_ps_name, # ignores cell CSR
17
+ size: DEFAULT_SIZE, # 10.0 hardcoded
18
+ }]
19
+ baseline = y + (height / 2) # ignores VerticalJustification
20
+ canvas.text_rich(runs, at: [x + INSET, baseline]) # INSET=4.0 hardcoded
21
+ end
22
+ ```
23
+
24
+ The Cell model (`Elements::Cell`) has:
25
+ - `top_inset`, `left_inset`, `bottom_inset`, `right_inset` — text insets
26
+ - `fill_color`, `fill_tint` — cell background
27
+ - `vertical_justification` — Top/Center/Bottom/Justify
28
+ - `paragraph_style_range` — typed runs with their own fonts/sizes
29
+
30
+ None of these are honored today. Cells render as plain rectangles
31
+ with default font at center-baseline with 4pt fixed inset.
32
+
33
+ ## What needs to happen
34
+
35
+ 1. **Cell background fill**: when `cell.fill_color` is set and not
36
+ "Color/None", resolve via ColorResolver and emit a `canvas.fill`
37
+ before the cell border.
38
+ 2. **Cell stroke**: emit `canvas.stroke` per cell using the cell's
39
+ top/left/bottom/right stroke weights from `Cell` (currently
40
+ Table-level TopBorderWeight only).
41
+ 3. **Per-cell insets**: read Cell's TextTopInset/TextLeftInset/
42
+ TextBottomInset/TextRightInset instead of the fixed `INSET = 4.0`.
43
+ 4. **Vertical justification**: position text per
44
+ `cell.vertical_justification` (Top/Center/Bottom/Justify).
45
+ 5. **Per-cell CSR styling**: walk `cell.paragraph_style_range` →
46
+ `character_style_range` and emit one `text_rich` call with each
47
+ run's font + size, instead of collapsing all text into a single
48
+ DEFAULT_SIZE run.
49
+ 6. **RotationAngle**: cells can be rotated (Cell#rotation_angle);
50
+ emit `canvas.concat_matrix` if non-zero.
51
+
52
+ ## Acceptance criteria
53
+
54
+ - [ ] Cell with FillColor="Color/Blue" renders a blue background.
55
+ - [ ] Cell with TextTopInset=8 starts text 8pt below cell top.
56
+ - [ ] Cell with VerticalJustification="Top" puts text at cell top.
57
+ - [ ] Cell with bold CSR run renders bold (per-run font).
58
+ - [ ] Cell with PointSize=18 CSR uses 18pt text, not 10pt.
59
+
60
+ ## Dependencies
61
+
62
+ - None — pure feature gap on existing models.
@@ -0,0 +1,35 @@
1
+ # TODO PDF 98: RowSpan / ColumnSpan (merged cells) in TableRenderer
2
+
3
+ ## Status: OPEN — gap identified in 2026-08-08 audit
4
+
5
+ ## Problem
6
+
7
+ `Elements::Cell` already carries `row_span` and `column_span`
8
+ attributes (mapped from the RNC). The renderer's `SchemaLayout`
9
+ iterates `col_count × row_count` cells and looks up each cell by
10
+ `[col, row]` — it doesn't merge across spans.
11
+
12
+ For a 2×2 table where cell `0:0` has `ColumnSpan=2`, the layout
13
+ today emits a single cell-width rectangle at column 0 and an empty
14
+ cell at column 1, instead of one rectangle spanning both columns.
15
+
16
+ ## What needs to happen
17
+
18
+ 1. `SchemaLayout#each_cell` reads `cell.column_span` and `cell.row_span`
19
+ and computes the rect width = `cell_w * column_span`, height =
20
+ `cell_h * row_span`.
21
+ 2. Skip cells that are "covered" by a spanning cell (i.e. when
22
+ `[col, row]` falls within another cell's span). Track a
23
+ coverage set per iteration.
24
+ 3. Spanning cell's text is rendered once, centered on the merged rect.
25
+
26
+ ## Acceptance criteria
27
+
28
+ - [ ] Cell with ColumnSpan=2 renders a rect of width = 2 × column width.
29
+ - [ ] Cell with RowSpan=3 renders a rect of height = 3 × row height.
30
+ - [ ] Covered cells (within a span) don't render borders.
31
+ - [ ] Spec: 2x2 grid with cell 0:0 spanning both columns.
32
+
33
+ ## Dependencies
34
+
35
+ - TODO 97 (cell rendering fidelity).