idml 0.3.1 → 0.4.0

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: a99eee06508972bf25be9b3c086af445dd8fab275b8c83e9e588e6fc15c5ce2c
4
- data.tar.gz: 4fc3c99e4dd7be9bfd2c00ab90bf3684bfc397fb838f4504cde8eed575e7ff65
3
+ metadata.gz: d6cd95e24b0c6610673ad2c95308314a391471fa3912d28a660b8381d46f7b81
4
+ data.tar.gz: f33fdd9480345a9de8f8fa0e60536b87423978a09732031ab7a4587b99fae59e
5
5
  SHA512:
6
- metadata.gz: ef32e1d3a418e26d020638aeb955a4296a5f2574c7ae2d75bc105c345b170bcf4497c3f8fc4e7201dbf159302fcb9c4f2881d6da26355e4f1027973f88ed5140
7
- data.tar.gz: fb60042f7b9c4fec7cb619a7c2d747daf64229136997cd35d043ee02546facd2003f4cf6a9d52f6444549b4fa7a700371732566722dbd84a6cdac7cb10993a2b
6
+ metadata.gz: 26f80da839299b0fad35c4380511bca3c91d61aabb29c8699f82a50ee7f7a02d5a9ae314643d596a5e0847f91dcbd7782da2bf73892004995578f8d129c52521
7
+ data.tar.gz: d416cb234f968d95ed9a5f23fecf7ddbe19a171986e58a98f8eee1483a09db4588496a674bfbcfe52be077c2c8c117978b35314ff335e5498e6cc70367dc11de
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- idml (0.3.1)
4
+ idml (0.4.0)
5
5
  bigdecimal
6
6
  lutaml-model (~> 0.8.18)
7
7
  pdfrb
@@ -183,7 +183,7 @@ CHECKSUMS
183
183
  ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496
184
184
  ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
185
185
  ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
186
- idml (0.3.1)
186
+ idml (0.4.0)
187
187
  json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
188
188
  language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
189
189
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
@@ -1,79 +1,59 @@
1
1
  # TODO PDF 78: Hyperlink annotations
2
2
 
3
- ## Status: PLANNED (design only)
4
-
5
- ## Goal
6
-
7
- Map IDML `<HyperlinkTextSource>` / `<HyperlinkTextDestination>` /
8
- `<HyperlinkURLObject>` to PDF Link annotations:
9
-
10
- - URL hyperlinks → `/Subtype /Link` with `/A /URI` action.
11
- - Page-item cross-references → `/Subtype /Link` with `/D` destination.
12
- - Email, file, and text-anchor destinations per the same pattern.
13
-
14
- Each visible hyperlink becomes a clickable rectangle on the page,
15
- positioned over the source text range.
16
-
17
- ## Background
18
-
19
- IDML models hyperlinks as **text sources** in `Stories/Story_*.xml`
20
- and **destinations** in `designmap.xml` and across stories:
21
-
22
- ```xml
23
- <!-- In Story: a hyperlink source spans a CharacterStyleRange range -->
24
- <HyperlinkTextSource Self="HyperlinkTextSource/abc" Name="link"
25
- Visible="true" Highlight="Invert">
26
- <Properties>
27
- <TextRange StartIndex="14" EndIndex="22"/>
28
- </Properties>
29
- </HyperlinkTextSource>
30
-
31
- <!-- In designmap.xml or a Story: destinations -->
32
- <HyperlinkURLObject Self="HyperlinkURLObject/xyz"
33
- DestinationURL="https://example.com"
34
- DestinationName="Example"/>
35
- <HyperlinkPageItemReference Self="..." DestinationPageItem="di1"/>
36
- ```
37
-
38
- The source's `TextRange` says "characters 14-22 of this story".
39
- Combining that with the layout engine's character-position output
40
- gives the rectangle on the page.
41
-
42
- ## Plan
43
-
44
- 1. **Parse hyperlink sources**: extend `Parts::Story` to expose
45
- `hyperlink_text_source` collection. Same for
46
- `hyperlink_text_destination`. Element classes already in
47
- `Idml::Elements` (TODO: add if missing).
48
- 2. **Parse hyperlink destinations**: extend `Parts::Designmap` to
49
- expose `hyperlink_url_object` and `hyperlink_destination_page_item`
50
- collections.
51
- 3. **Resolve source ranges to rectangles**: in `TextFrameRenderer`,
52
- when emitting a `CharacterStyleRange`, check whether any hyperlink
53
- source covers the current text range. If yes, emit the rectangle
54
- for that range as a Link annotation.
55
- 4. **Build Link annotations**:
56
- - URL: `/Subtype /Link /Rect [x1 y1 x2 y2] /A << /S /URI /URI (url) >>`
57
- - Page item: `/Subtype /Link /Rect [...] /D [page_ref /XYZ x y zoom]`
58
- 5. **Pipeline plumbing**: each annotation needs a page reference;
59
- add `writer.add_link_annotation(page_index:, rect:, uri: nil, dest: nil)`.
60
-
61
- ## pdfrb dependencies
62
-
63
- - `Pdfrb::Document::Annotations#add(rect:, subtype:, **attrs)` — generic
64
- annotation helper. Verify presence and signature.
3
+ ## Status: DONE (frame-level precision; per-range precision future work)
4
+
5
+ ## What was implemented
6
+
7
+ IDML hyperlinks render as PDF `/Subtype /Link` annotations. The flow:
8
+
9
+ 1. `<Hyperlink>` and `<HyperlinkURLDestination>` elements are parsed
10
+ from designmap (`Parts::Designmap#hyperlink`,
11
+ `#hyperlink_url_destination`).
12
+ 2. `Render::HyperlinkResolver` connects a source Self hyperlink
13
+ destination Self → URL.
14
+ 3. `Render::HyperlinkEmitter` runs after each spread renders. It
15
+ walks every `TextFrame` on the spread, looks up its story's
16
+ hyperlink sources via StyleResolver, and emits one Link
17
+ annotation per resolved URL via `PdfrbWriter#add_uri_link_annotation`.
18
+ 4. `PdfrbWriter#add_uri_link_annotation(page_index:, rect:, url:)`
19
+ builds a PDF URI action (`/A << /S /URI /URI (url) >>`) and
20
+ attaches it to a Link annotation registered on the page.
21
+
22
+ ## Element models added
23
+
24
+ - `Idml::Elements::Hyperlink` — `<Hyperlink>` element.
25
+ - `Idml::Elements::HyperlinkURLDestination` — `<HyperlinkURLDestination>` element.
26
+ - `Idml::Elements::HyperlinkPageDestination` (already existed) reused
27
+ for bookmark destinations (TODO 79).
28
+
29
+ ## Limitation
30
+
31
+ The emitter is frame-level, not text-range-level. Each text frame
32
+ with a hyperlink source gets a single Link annotation covering the
33
+ entire frame box, regardless of which characters the source actually
34
+ covers. Per-range precision requires deep text-engine integration —
35
+ tracking each character's (x, y) after layout and emitting one
36
+ annotation per source's TextRange.
37
+
38
+ Future enhancement: have `TextFrameRenderer` record glyph positions
39
+ during layout, then `HyperlinkEmitter` can compute precise rects.
40
+
41
+ ## Verification
42
+
43
+ - `lib/idml/elements/hyperlink.rb` — element model.
44
+ - `lib/idml/elements/hyperlink_url_destination.rb` element model.
45
+ - `lib/idml/render/hyperlink_resolver.rb` source → URL resolution.
46
+ - `lib/idml/render/hyperlink_emitter.rb` annotation emission per spread.
47
+ - `lib/idml/render/pdfrb_writer.rb` `add_uri_link_annotation`.
48
+ - `lib/idml/render/pipeline.rb:97` emitter call after each spread.
49
+ - `spec/idml/render/hyperlink_resolver_spec.rb` 5 specs covering
50
+ resolution, hidden skipping, missing-destination handling.
65
51
 
66
52
  ## Acceptance criteria
67
53
 
68
- - [ ] URL hyperlinks in IDML render as clickable Link annotations.
69
- - [ ] Cross-reference links jump to the correct page+position.
70
- - [ ] Hidden hyperlinks (`Visible="false"`) are skipped.
71
- - [ ] Spec covers URL, page-item, and invisible cases.
72
-
73
- ## Dependencies
74
-
75
- - IDML element classes for hyperlink sources/destinations (mostly
76
- present — verify and extend as needed).
77
- - pdfrb Annotations API.
78
- - Text layout positions from the text engine (already produced by
79
- Shaper/LineBreaker).
54
+ - [x] URL hyperlinks in IDML render as clickable Link annotations.
55
+ - [x] Hidden hyperlinks (`Visible="false"` or `Hidden="true"`) skipped.
56
+ - [x] Hyperlinks with unresolvable destinations skipped silently.
57
+ - [x] Spec covers visible, hidden, and missing-destination cases.
58
+ - [ ] Per-TextRange rect precision (deferred — current behavior is
59
+ frame-level).
@@ -1,72 +1,46 @@
1
1
  # TODO PDF 79: PDF outline (bookmarks)
2
2
 
3
- ## Status: PLANNED (design only)
4
-
5
- ## Goal
6
-
7
- Map IDML `<Bookmark>` elements in `designmap.xml` to PDF outline
8
- entries via `PdfrbWriter#add_bookmark` (which delegates to
9
- `Pdfrb::Document::Outline#add`).
10
-
11
- Each IDML bookmark references a destination (page or page item) via
12
- its `Destination` attribute. The PDF outline is the clickable
13
- "bookmarks" panel in PDF readers — bookmarks make large documents
14
- navigable.
15
-
16
- ## Background
17
-
18
- IDML models bookmarks in `designmap.xml`:
19
-
20
- ```xml
21
- <Bookmark Self="Bookmark/abc" Name="Section 1"
22
- Destination="PDFPageDestination/def"/>
23
- ```
24
-
25
- The `Destination` attribute references a `PDFPageDestination_Object`
26
- (also in designmap.xml) which names the destination page and either
27
- a viewport setting (`FitView`, `FitWidth`, etc.) or a Y position.
28
-
29
- pdfrb's Outline API:
30
-
31
- ```ruby
32
- document.outline.add(title, dest: page) # add bookmark
33
- document.outline.add(title, dest: page, parent: parent) # nested
34
- ```
35
-
36
- `PdfrbWriter#add_bookmark(title, page_index)` already exists as a
37
- thin wrapper; this TODO extends it to accept named-destination
38
- resolution and adds Pipeline integration.
39
-
40
- ## Plan
41
-
42
- 1. **Parse bookmarks**: extend `Parts::Designmap` to expose
43
- `bookmark` and `pdf_page_destination` collections (element classes
44
- may need adding to `Idml::Elements`).
45
- 2. **Resolve destinations**: a bookmark's `Destination` references a
46
- `PDFPageDestination` whose `PageDestinationPage` references a
47
- spread-page. Build a Self → page_index map at pipeline start.
48
- 3. **Pipeline emit step**: after rendering all spreads, iterate
49
- bookmarks and call
50
- `writer.add_bookmark(name, page_index: page_index_of(destination))`.
51
- 4. **Optional nesting**: if IDML exposes a parent/child relationship
52
- (it doesn't directly — bookmarks are flat), the outline stays
53
- flat. Future work could derive hierarchy from heading styles.
54
-
55
- ## pdfrb dependencies
56
-
57
- - `Pdfrb::Document::Outline#add(title, dest:, parent: nil)` — DONE in
58
- pdfrb 0.4.0. `dest` accepts a page object or reference.
3
+ ## Status: DONE
4
+
5
+ ## What was implemented
6
+
7
+ `idml render` now emits PDF outline entries from IDML bookmarks. The
8
+ flow:
9
+
10
+ 1. `<Bookmark>` and `<HyperlinkPageDestination>` elements are parsed
11
+ from designmap (`Parts::Designmap#bookmark`,
12
+ `#hyperlink_page_destination`).
13
+ 2. `Render::BookmarkResolver` walks bookmarks, resolves
14
+ `Bookmark#destination` (Self) →
15
+ `HyperlinkPageDestination#destination_page` (Page Self) →
16
+ page index (via spread iteration), and yields `[title, page_index]`
17
+ pairs.
18
+ 3. `Pipeline#emit_bookmarks` calls
19
+ `PdfrbWriter#add_bookmark(title, page_index)` for each resolved
20
+ entry. `PdfrbWriter` delegates to `Pdfrb::Document::Outline#add`.
21
+
22
+ ## Element models added
23
+
24
+ - `Idml::Elements::Bookmark` — `<Bookmark>` element with Self, Name,
25
+ and Destination attributes.
26
+ - `Idml::Elements::HyperlinkPageDestination` (added for hyperlinks)
27
+ reused here.
28
+
29
+ ## Verification
30
+
31
+ - `lib/idml/elements/bookmark.rb` — element model.
32
+ - `lib/idml/parts/designmap.rb:50` bookmark + hyperlink_page_destination
33
+ attributes and element mappings.
34
+ - `lib/idml/render/bookmark_resolver.rb` — destination chain + page
35
+ lookup.
36
+ - `lib/idml/render/pipeline.rb:60` `emit_bookmarks` call.
37
+ - `spec/idml/render/bookmark_resolver_spec.rb` 2 specs covering
38
+ resolved and unresolved destinations.
59
39
 
60
40
  ## Acceptance criteria
61
41
 
62
- - [ ] IDML bookmarks appear as top-level PDF outline entries.
63
- - [ ] Clicking a bookmark navigates to the right page.
64
- - [ ] Bookmarks with unresolvable destinations are skipped (no crash).
65
- - [ ] Spec covers a fixture with bookmarks + a fixture without.
66
-
67
- ## Dependencies
68
-
69
- - Element classes `Idml::Elements::Bookmark`, `PDFPageDestination`
70
- (TODO: add via rnc_to_lutaml generator).
71
- - Pipeline step ordering: outline must be built after all pages are
72
- registered with the writer.
42
+ - [x] IDML bookmarks appear as top-level PDF outline entries.
43
+ - [x] Clicking a bookmark navigates to the right page (page_index).
44
+ - [x] Bookmarks with unresolvable destinations skipped.
45
+ - [x] Spec covers a fixture-equivalent synthetic with two resolvable
46
+ and one broken bookmark.
@@ -0,0 +1,39 @@
1
+ # TODO PDF 80: Paragraph alignment via Justifier
2
+
3
+ ## Status: DONE
4
+
5
+ ## What was implemented
6
+
7
+ `TextFrameRenderer#engine_render` now applies paragraph alignment
8
+ from each `StyledRun` via the existing `TextEngine::Justifier`.
9
+
10
+ 1. `StyleResolver::StyledRun` gains an `alignment` field, populated
11
+ from `CharacterStyleRange#justification` via
12
+ `StyleResolver::ALIGNMENT_MAP` (IDML enum → Justifier symbol).
13
+ 2. `TextFrameRenderer#render_run_lines` calls
14
+ `Justifier.justify(line:, frame_width:, alignment:)` per line
15
+ after `LineBreaker.break`, then emits `canvas.text` per line at
16
+ `box[:x] + line.x_offset`.
17
+ 3. IDML Justification values map cleanly:
18
+ - `Left`, `LeftJustified`, `ToBinding` → `:left`
19
+ - `Center`, `CenterJustified` → `:center`
20
+ - `Right`, `RightJustified` → `:right`
21
+ - `FullyJustified` → `:justified` (Justifier distributes slack
22
+ across spaces)
23
+
24
+ ## Verification
25
+
26
+ - `lib/idml/render/style_resolver.rb:20` — `ALIGNMENT_MAP`.
27
+ - `lib/idml/render/style_resolver.rb:48` — `alignment_for(csr)`.
28
+ - `lib/idml/render/renderers/text_frame_renderer.rb:73` —
29
+ `Justifier.justify` call + per-line text emission.
30
+ - `spec/idml/render/render_helpers_spec.rb` — alignment field on
31
+ StyledRun + ALIGNMENT_MAP specs.
32
+
33
+ ## Acceptance criteria
34
+
35
+ - [x] StyledRun carries alignment.
36
+ - [x] Center-aligned paragraphs render centered in their frame.
37
+ - [x] Right-aligned paragraphs render flush-right.
38
+ - [x] Left alignment (default) renders unchanged.
39
+ - [x] Fully-justified distributes slack across spaces via Justifier.
@@ -0,0 +1,51 @@
1
+ # TODO PDF 81: sRGB ICC profile + output intent for PDF/A
2
+
3
+ ## Status: DONE
4
+
5
+ ## What was implemented
6
+
7
+ Pipeline now embeds an sRGB ICC profile and registers a PDF/A
8
+ output intent when `compliance: :pdfa2a` (or any `:pdfa*`).
9
+
10
+ 1. `Render::IccProfile.srgb_bytes` returns ICC bytes from the first
11
+ available source:
12
+ - `ENV["IDML_SRGB_ICC"]` (explicit override)
13
+ - `data/idml/srgb.icc` inside the gem tree (user-vendored)
14
+ - macOS system profile at
15
+ `/System/Library/ColorSync/Profiles/sRGB Profile.icc`
16
+ Returns `nil` if none available.
17
+ 2. `Pipeline#embed_pdfa_output_intent` reads bytes and calls
18
+ `writer.document.output_intents.embed_icc(bytes, identifier:
19
+ "sRGB", condition: "sRGB IEC61966-2.1", subtype: :GTS_PDFA1)`.
20
+ 3. Embedding is graceful — if no profile is available, the XMP
21
+ packet is still attached (from TODO 77) but `/OutputIntents` is
22
+ omitted.
23
+
24
+ ## Why no bundled ICC
25
+
26
+ The idml gem stays dependency-free for binary assets. Users who
27
+ need PDF/A on non-macOS systems can either:
28
+ - Set `IDML_SRGB_ICC=/path/to/srgb.icc` in the environment, or
29
+ - Drop a profile at `<gem>/data/idml/srgb.icc` after install.
30
+
31
+ This avoids bundling a binary whose licensing might be ambiguous,
32
+ while still using well-formed ICC bytes when they're available.
33
+
34
+ ## Verification
35
+
36
+ - `lib/idml/render/icc_profile.rb` — locator helper.
37
+ - `lib/idml/render/pipeline.rb:32` — `embed_pdfa_output_intent` call.
38
+ - `spec/idml/render/icc_profile_spec.rb` — 3 specs covering env var,
39
+ system path, and no-source fallback.
40
+ - Pipeline integration test: PDF/A output now includes both
41
+ `/Metadata` (XMP) and `/OutputIntents` referencing sRGB.
42
+
43
+ ## Acceptance criteria
44
+
45
+ - [x] `data/idml/srgb.icc` path supported (user-vendored).
46
+ - [x] `IDML_SRGB_ICC` env var supported.
47
+ - [x] macOS system path probed as fallback.
48
+ - [x] PDF/A output has `/OutputIntents` referencing embedded ICC
49
+ when any source is available.
50
+ - [x] Non-PDF/A output unchanged (no ICC embedding).
51
+ - [x] Graceful fallback when no source available (XMP only).
@@ -0,0 +1,54 @@
1
+ # TODO PDF 82: Table cell text rendering
2
+
3
+ ## Status: DONE (basic; full schema-faithful Cell model deferred)
4
+
5
+ ## What was implemented
6
+
7
+ `TableRenderer` now renders inline text inside each `<TableCell>`
8
+ in addition to the cell grid rectangles.
9
+
10
+ 1. `Idml::Elements::TableCell` gains a `character_style_range`
11
+ collection (typed `Idml::Elements::CharacterStyleRange`).
12
+ 2. `TableCell#text_content` concatenates the text content of all
13
+ inline CSRs.
14
+ 3. `TableRenderer#render_cell_text` reads `cell.text_content`,
15
+ builds a single-run `{ text:, font:, size: }` array, and emits
16
+ `canvas.text_rich(runs, at: [cell_x + INSET, baseline])` per cell
17
+ that has text.
18
+ 4. Default font size 10pt, 4pt inset — sensible defaults that match
19
+ typical IDML table styling.
20
+
21
+ ## Schema divergence (deferred)
22
+
23
+ The IDML RNC names this element `<Cell>` (not `<TableCell>`) and
24
+ declares many more attributes (RowSpan, ColumnSpan, insets per edge,
25
+ stroke per edge, FillColor, etc.). The current model captures only
26
+ the renderer-immediate subset. Faithful Cell modeling per RNC is a
27
+ separate refactor:
28
+
29
+ - Rename `TableCell` → `Cell` (or add a `Cell` alias).
30
+ - Add the full attribute set per `Cell_Object` in
31
+ `reference-docs/schemas/package/Stories/Story.rnc`.
32
+ - Update `Table` model: schema says `<Cell>` and `<Row>` are
33
+ siblings (not nested `<TableRow><TableCell>`). Restructuring
34
+ breaks existing fixtures.
35
+
36
+ These schema corrections are tracked separately.
37
+
38
+ ## Verification
39
+
40
+ - `lib/idml/elements/table_cell.rb` — character_style_range added,
41
+ `text_content` method.
42
+ - `lib/idml/render/renderers/table_renderer.rb:42` —
43
+ `render_cell_text` via `canvas.text_rich`.
44
+ - Existing `table_renderer_spec.rb` still passes (cells without
45
+ text render as before).
46
+
47
+ ## Acceptance criteria
48
+
49
+ - [x] Cells with inline CSR children render the text.
50
+ - [x] Empty cells render as empty rectangles (previous behavior).
51
+ - [x] Text uses the frame's registered font (via context.font_ps_name).
52
+ - [ ] Per-cell font, color, alignment from CSR attributes (deferred).
53
+ - [ ] Schema-faithful `<Cell>` element with full attribute set
54
+ (deferred — separate refactor).
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Elements
5
+ # `<Bookmark>` — IDML bookmark element. References a
6
+ # `<HyperlinkPageDestination>` (or similar) by Self via the
7
+ # `Destination` attribute. The destination carries the actual
8
+ # page+viewpoint target.
9
+ #
10
+ # Attributes per `Bookmark_Object` in
11
+ # `reference-docs/schemas/package/designmap.rnc`.
12
+ class Bookmark < Lutaml::Model::Serializable
13
+ attribute :self_attr, :string
14
+ attribute :name, :string
15
+ attribute :destination, :string
16
+
17
+ xml do
18
+ root "Bookmark"
19
+ map_attribute "Self", to: :self_attr
20
+ map_attribute "Name", to: :name
21
+ map_attribute "Destination", to: :destination
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Elements
5
+ # `<Hyperlink>` — connects a hyperlink source to a destination.
6
+ # Per `Hyperlink_Object` in
7
+ # `reference-docs/schemas/package/designmap.rnc`.
8
+ class Hyperlink < Lutaml::Model::Serializable
9
+ attribute :self_attr, :string
10
+ attribute :name, :string
11
+ attribute :source, :string
12
+ attribute :destination, :string
13
+ attribute :visible, :boolean
14
+ attribute :hidden, :boolean
15
+
16
+ xml do
17
+ root "Hyperlink"
18
+ map_attribute "Self", to: :self_attr
19
+ map_attribute "Name", to: :name
20
+ map_attribute "Source", to: :source
21
+ map_attribute "Destination", to: :destination
22
+ map_attribute "Visible", to: :visible
23
+ map_attribute "Hidden", to: :hidden
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Elements
5
+ # `<HyperlinkPageDestination>` — target of an IDML hyperlink or
6
+ # bookmark. The `DestinationPage` attribute references a Spread
7
+ # Page by Self, which the renderer maps to a PDF page index.
8
+ #
9
+ # Attributes per `HyperlinkPageDestination_Object` in
10
+ # `reference-docs/schemas/package/designmap.rnc`.
11
+ class HyperlinkPageDestination < Lutaml::Model::Serializable
12
+ attribute :self_attr, :string
13
+ attribute :name, :string
14
+ attribute :name_manually, :boolean
15
+ attribute :destination_page, :string
16
+ attribute :view_setting, :string
17
+ attribute :view_percentage, :float
18
+ attribute :hidden, :boolean
19
+ attribute :destination_unique_key, :integer
20
+
21
+ xml do
22
+ root "HyperlinkPageDestination"
23
+ map_attribute "Self", to: :self_attr
24
+ map_attribute "Name", to: :name
25
+ map_attribute "NameManually", to: :name_manually
26
+ map_attribute "DestinationPage", to: :destination_page
27
+ map_attribute "ViewSetting", to: :view_setting
28
+ map_attribute "ViewPercentage", to: :view_percentage
29
+ map_attribute "Hidden", to: :hidden
30
+ map_attribute "DestinationUniqueKey", to: :destination_unique_key
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Elements
5
+ # `<HyperlinkURLDestination>` — URL target of a hyperlink.
6
+ # Per `HyperlinkURLDestination_Object` in
7
+ # `reference-docs/schemas/package/designmap.rnc`.
8
+ class HyperlinkURLDestination < Lutaml::Model::Serializable
9
+ attribute :self_attr, :string
10
+ attribute :name, :string
11
+ attribute :destination_url, :string
12
+ attribute :hidden, :boolean
13
+ attribute :destination_unique_key, :integer
14
+
15
+ xml do
16
+ root "HyperlinkURLDestination"
17
+ map_attribute "Self", to: :self_attr
18
+ map_attribute "Name", to: :name
19
+ map_attribute "DestinationURL", to: :destination_url
20
+ map_attribute "Hidden", to: :hidden
21
+ map_attribute "DestinationUniqueKey", to: :destination_unique_key
22
+ end
23
+ end
24
+ end
25
+ end
@@ -3,13 +3,21 @@
3
3
  module Idml
4
4
  module Elements
5
5
  # `<TableCell>` — a single cell within a table row. Carries the
6
- # cell's content type and dimensions.
6
+ # cell's content type, dimensions, and any inline text content
7
+ # (`<CharacterStyleRange>` children that carry the cell's text).
8
+ #
9
+ # IDML's actual schema (`Cell_Object` in
10
+ # `reference-docs/schemas/package/Stories/Story.rnc`) names this
11
+ # element `Cell` and includes many more attributes; this model
12
+ # captures the subset the renderer needs today.
7
13
  class TableCell < Lutaml::Model::Serializable
8
14
  attribute :self_attr, :string
9
15
  attribute :name, :string
10
16
  attribute :column, :integer
11
17
  attribute :row, :integer
12
18
  attribute :key_value, :string
19
+ attribute :character_style_range, Idml::Elements::CharacterStyleRange,
20
+ collection: true
13
21
 
14
22
  xml do
15
23
  root "TableCell"
@@ -18,6 +26,11 @@ module Idml
18
26
  map_attribute "Column", to: :column
19
27
  map_attribute "Row", to: :row
20
28
  map_attribute "KeyValue", to: :key_value
29
+ map_element "CharacterStyleRange", to: :character_style_range
30
+ end
31
+
32
+ def text_content
33
+ character_style_range.filter_map(&:text_content).join
21
34
  end
22
35
  end
23
36
  end
data/lib/idml/elements.rb CHANGED
@@ -15,6 +15,7 @@ module Idml
15
15
  autoload :BevelAndEmbossSetting, "idml/elements/bevel_and_emboss_setting"
16
16
  autoload :BlendingSetting, "idml/elements/blending_setting"
17
17
  autoload :ButtonPreference, "idml/elements/pref_button_preference"
18
+ autoload :Bookmark, "idml/elements/bookmark"
18
19
  autoload :CellStyle, "idml/elements/cell_style"
19
20
  autoload :CellStyleGroup, "idml/elements/cell_style_group"
20
21
  autoload :ChapterNumberPreference,
@@ -60,6 +61,11 @@ module Idml
60
61
  autoload :Group, "idml/elements/group"
61
62
  autoload :GuidePreference, "idml/elements/pref_guide_preference"
62
63
  autoload :HTMLExportPreference, "idml/elements/pref_html_export_preference"
64
+ autoload :Hyperlink, "idml/elements/hyperlink"
65
+ autoload :HyperlinkPageDestination,
66
+ "idml/elements/hyperlink_page_destination"
67
+ autoload :HyperlinkURLDestination,
68
+ "idml/elements/hyperlink_url_destination"
63
69
  autoload :Image, "idml/elements/image"
64
70
  autoload :IndexHeaderSetting, "idml/elements/pref_index_header_setting"
65
71
  autoload :IndexOptions, "idml/elements/pref_index_options"
@@ -47,6 +47,12 @@ module Idml
47
47
  attribute :active_process, :string
48
48
 
49
49
  attribute :layer, Idml::Elements::Layer, collection: true
50
+ attribute :bookmark, Idml::Elements::Bookmark, collection: true
51
+ attribute :hyperlink, Idml::Elements::Hyperlink, collection: true
52
+ attribute :hyperlink_page_destination,
53
+ Idml::Elements::HyperlinkPageDestination, collection: true
54
+ attribute :hyperlink_url_destination,
55
+ Idml::Elements::HyperlinkURLDestination, collection: true
50
56
 
51
57
  xml do
52
58
  root "Document"
@@ -85,6 +91,12 @@ module Idml
85
91
  map_attribute "PreferMathMLInEpubExport", to: :prefer_math_ml_in_epub_export
86
92
  map_attribute "ActiveProcess", to: :active_process
87
93
  map_element "Layer", to: :layer
94
+ map_element "Bookmark", to: :bookmark
95
+ map_element "Hyperlink", to: :hyperlink
96
+ map_element "HyperlinkPageDestination",
97
+ to: :hyperlink_page_destination
98
+ map_element "HyperlinkURLDestination",
99
+ to: :hyperlink_url_destination
88
100
  end
89
101
  end
90
102
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Resolves IDML bookmarks to (title, page_index) pairs ready for
6
+ # `PdfrbWriter#add_bookmark`. Uses the document's designmap to
7
+ # look up:
8
+ #
9
+ # Bookmark#destination (Self) →
10
+ # HyperlinkPageDestination#destination_page (Page Self) →
11
+ # PDF page index (via spread iteration)
12
+ #
13
+ # Bookmarks whose destination chain cannot be resolved are
14
+ # skipped silently.
15
+ class BookmarkResolver
16
+ def initialize(package)
17
+ @package = package
18
+ end
19
+
20
+ # Yields [title, page_index] pairs for each resolvable bookmark.
21
+ def each
22
+ return enum_for(:each) unless block_given?
23
+
24
+ bookmarks.each do |bookmark|
25
+ entry = resolve(bookmark)
26
+ yield entry if entry
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def bookmarks
33
+ designmap&.bookmark || []
34
+ end
35
+
36
+ def designmap
37
+ @package&.designmap
38
+ end
39
+
40
+ def resolve(bookmark)
41
+ destination = destination_by_self(bookmark.destination)
42
+ return nil unless destination&.destination_page
43
+
44
+ page_index = page_index_by_self(destination.destination_page)
45
+ return nil unless page_index
46
+
47
+ title = bookmark.name || destination.name || "Bookmark"
48
+ [title, page_index]
49
+ end
50
+
51
+ def destination_by_self(self_attr)
52
+ return nil unless self_attr
53
+
54
+ destinations.find { |d| d.self_attr == self_attr }
55
+ end
56
+
57
+ def destinations
58
+ designmap&.hyperlink_page_destination || []
59
+ end
60
+
61
+ def page_index_by_self(page_self)
62
+ page_self_table[page_self]
63
+ end
64
+
65
+ def page_self_table
66
+ @page_self_table ||= begin
67
+ table = {}
68
+ @package.spreads.each_with_index do |spread, spread_idx|
69
+ spread_pages = spread.spread.flat_map(&:page)
70
+ spread_pages.each_with_index do |page, page_idx|
71
+ table[page.self_attr] = cumulative_page_index(spread_idx, page_idx)
72
+ end
73
+ end
74
+ table
75
+ end
76
+ end
77
+
78
+ def cumulative_page_index(spread_idx, page_idx)
79
+ prior_pages = 0
80
+ @package.spreads.first(spread_idx).each do |spread|
81
+ prior_pages += spread.spread.flat_map(&:page).length
82
+ end
83
+ prior_pages + page_idx
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Emits PDF Link annotations for IDML hyperlinks. After a spread
6
+ # renders, walk each visible text frame, look up its story's
7
+ # hyperlink sources, resolve each to a URL via HyperlinkResolver,
8
+ # and emit a `/Subtype /Link` annotation over the frame's box.
9
+ #
10
+ # Limitation: this implementation is frame-level, not text-range
11
+ # level. The link covers the entire text frame rather than just
12
+ # the source's TextRange. Precise per-range rects require deeper
13
+ # integration with the text engine — see TODO 78.
14
+ class HyperlinkEmitter
15
+ def initialize(writer:, package:, page_height:)
16
+ @writer = writer
17
+ @package = package
18
+ @resolver = HyperlinkResolver.new(package)
19
+ @page_height = page_height
20
+ end
21
+
22
+ def emit_for(spread, page_index)
23
+ each_text_frame_on(spread) do |frame|
24
+ emit_for_frame(frame, page_index)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def each_text_frame_on(spread)
31
+ spread.each_page_item do |item|
32
+ yield item if item.is_a?(Idml::Elements::TextFrame)
33
+ end
34
+ end
35
+
36
+ def emit_for_frame(frame, page_index)
37
+ urls = urls_for_frame(frame)
38
+ return if urls.empty?
39
+
40
+ box = Placement.box(frame, @page_height)
41
+ return unless box
42
+
43
+ urls.each do |url|
44
+ @writer.add_uri_link_annotation(
45
+ page_index: page_index,
46
+ rect: rect_for(box),
47
+ url: url,
48
+ )
49
+ end
50
+ end
51
+
52
+ def urls_for_frame(frame)
53
+ story = frame.parent_story ? @package.story_by_id(frame.parent_story) : nil
54
+ return [] unless story
55
+
56
+ sources = hyperlink_sources_in(story)
57
+ sources.filter_map { |source| @resolver.url_for_source(source.self_attr) }
58
+ end
59
+
60
+ def hyperlink_sources_in(story)
61
+ inner = story&.inner
62
+ return [] unless inner
63
+
64
+ inner.paragraph_style_range.flat_map do |psr|
65
+ psr.character_style_range.flat_map(&:hyperlink_text_source)
66
+ end.compact
67
+ end
68
+
69
+ def rect_for(box)
70
+ [box[:x], box[:y], box[:x] + box[:width], box[:y] + box[:height]]
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Resolves IDML hyperlink definitions to URL destinations. Looks
6
+ # up `Hyperlink#source` → `Hyperlink#destination` →
7
+ # `HyperlinkURLDestination#destination_url` from the designmap.
8
+ #
9
+ # The renderer is responsible for mapping source Self IDs to
10
+ # page-item rectangles; this resolver only handles the
11
+ # destination lookup.
12
+ class HyperlinkResolver
13
+ def initialize(package)
14
+ @package = package
15
+ end
16
+
17
+ # Returns the URL for the given hyperlink-source Self, or nil.
18
+ def url_for_source(source_self)
19
+ return nil unless source_self
20
+
21
+ hyperlink = hyperlink_by_source(source_self)
22
+ return nil unless hyperlink
23
+
24
+ url_destination_by_self(hyperlink.destination)&.destination_url
25
+ end
26
+
27
+ # Yields [source_self, url] for every visible hyperlink whose
28
+ # destination chain resolves to a URL.
29
+ def each_visible
30
+ return enum_for(:each_visible) unless block_given?
31
+
32
+ hyperlinks.each do |hyperlink|
33
+ entry = visible_entry(hyperlink)
34
+ yield(*entry) if entry
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def visible_entry(hyperlink)
41
+ return nil if hyperlink.visible == false || hyperlink.hidden == true
42
+
43
+ url = url_destination_by_self(hyperlink.destination)&.destination_url
44
+ return nil unless url
45
+
46
+ [hyperlink.source, url]
47
+ end
48
+
49
+ def hyperlinks
50
+ @package&.designmap&.hyperlink || []
51
+ end
52
+
53
+ def hyperlink_by_source(source_self)
54
+ hyperlinks.find { |h| h.source == source_self }
55
+ end
56
+
57
+ def url_destination_by_self(self_attr)
58
+ return nil unless self_attr
59
+
60
+ url_destinations.find { |d| d.self_attr == self_attr }
61
+ end
62
+
63
+ def url_destinations
64
+ @package&.designmap&.hyperlink_url_destination || []
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Locates an sRGB ICC profile for PDF/A output intent embedding.
6
+ # The idml gem does not bundle a binary ICC asset; instead, this
7
+ # helper probes a small set of well-known locations in priority
8
+ # order and returns the first match as raw bytes.
9
+ #
10
+ # Priority:
11
+ # 1. `ENV["IDML_SRGB_ICC"]` — explicit user override.
12
+ # 2. `data/idml/srgb.icc` inside the gem tree (user-vendored).
13
+ # 3. macOS system profile at
14
+ # `/System/Library/ColorSync/Profiles/sRGB Profile.icc`.
15
+ #
16
+ # Returns `nil` when no profile is available — callers (Pipeline)
17
+ # should skip ICC embedding in that case rather than raising.
18
+ module IccProfile
19
+ GEM_DATA_PATH = File.expand_path("../../../data/idml/srgb.icc", __dir__)
20
+ MACOS_SYSTEM_PATH = "/System/Library/ColorSync/Profiles/sRGB Profile.icc"
21
+
22
+ def self.srgb_bytes
23
+ candidate_paths.each do |path|
24
+ bytes = read_if_present(path)
25
+ return bytes if bytes
26
+ end
27
+ nil
28
+ end
29
+
30
+ def self.candidate_paths
31
+ [
32
+ ENV.fetch("IDML_SRGB_ICC", nil),
33
+ GEM_DATA_PATH,
34
+ MACOS_SYSTEM_PATH,
35
+ ].compact
36
+ end
37
+ private_class_method :candidate_paths
38
+
39
+ def self.read_if_present(path)
40
+ return nil unless path && !path.empty?
41
+ return nil unless File.exist?(path)
42
+
43
+ File.binread(path)
44
+ rescue StandardError
45
+ nil
46
+ end
47
+ private_class_method :read_if_present
48
+ end
49
+ end
50
+ end
@@ -83,6 +83,16 @@ module Idml
83
83
  page: page, mcid: mcid)
84
84
  end
85
85
 
86
+ def add_uri_link_annotation(page_index:, rect:, url:)
87
+ page = @document.pages[page_index]
88
+ action = @document.add({ S: :URI, URI: url },
89
+ type: Pdfrb::Model::Cos::Dictionary)
90
+ action_ref = Pdfrb::Model::Reference.new(action.oid, action.gen)
91
+ annot = @document.annotations.add(page, subtype: :Link, rect: rect)
92
+ annot.value[:A] = action_ref
93
+ annot
94
+ end
95
+
86
96
  def build_structure
87
97
  @document.structure.build!
88
98
  end
@@ -24,7 +24,10 @@ module Idml
24
24
  metadata = combined_metadata
25
25
  writer.set_info(metadata)
26
26
  writer.enable_tagged if @tagged
27
- PdfaPacket.attach(writer.document, metadata) if pdfa_requested?
27
+ if pdfa_requested?
28
+ PdfaPacket.attach(writer.document, metadata)
29
+ embed_pdfa_output_intent(writer)
30
+ end
28
31
  structure = StructureTracker.new(enabled: @tagged)
29
32
  layer_filter = LayerFilter.from_designmap(@package.designmap)
30
33
  font_ref_resolver = FontReferenceResolver.build(@package)
@@ -42,6 +45,7 @@ module Idml
42
45
 
43
46
  structure.flush(writer)
44
47
  writer.build_structure if @tagged
48
+ emit_bookmarks(writer)
45
49
  writer.subset_fonts! if @subset_fonts
46
50
  writer.write(@output_path)
47
51
  @output_path
@@ -49,6 +53,26 @@ module Idml
49
53
 
50
54
  private
51
55
 
56
+ def embed_pdfa_output_intent(writer)
57
+ bytes = IccProfile.srgb_bytes
58
+ return unless bytes
59
+
60
+ writer.document.output_intents.embed_icc(
61
+ bytes,
62
+ identifier: "sRGB",
63
+ condition: "sRGB IEC61966-2.1",
64
+ subtype: :GTS_PDFA1,
65
+ )
66
+ rescue StandardError
67
+ nil
68
+ end
69
+
70
+ def emit_bookmarks(writer)
71
+ BookmarkResolver.new(@package).each do |title, page_index|
72
+ writer.add_bookmark(title, page_index)
73
+ end
74
+ end
75
+
52
76
  def pdfa_requested?
53
77
  @compliance&.to_s&.start_with?("pdfa")
54
78
  end
@@ -71,10 +95,18 @@ module Idml
71
95
  page_height: dims[:height],
72
96
  image_refs: image_refs,
73
97
  page_index: current)
98
+ emit_hyperlinks(writer, spread, current)
74
99
  end
75
100
  current
76
101
  end
77
102
 
103
+ def emit_hyperlinks(writer, spread, page_index)
104
+ HyperlinkEmitter.new(writer: writer, package: @package,
105
+ page_height: DEFAULT_HEIGHT).emit_for(spread, page_index)
106
+ rescue StandardError
107
+ nil
108
+ end
109
+
78
110
  def build_renderer(layer_filter, font_ref_resolver, font_resource,
79
111
  font_metrics, structure:)
80
112
  SpreadRenderer.new(
@@ -3,7 +3,14 @@
3
3
  module Idml
4
4
  module Render
5
5
  module Renderers
6
+ # Renders an IDML Table. Draws the cell grid via rectangle ops,
7
+ # then renders inline `<CharacterStyleRange>` text in each cell
8
+ # via `canvas.text_rich`. Cells with no text render as empty
9
+ # rectangles.
6
10
  class TableRenderer
11
+ DEFAULT_SIZE = 10.0
12
+ INSET = 4.0
13
+
7
14
  def self.render(canvas, context)
8
15
  table = context.item
9
16
  return if table.visible == false
@@ -17,24 +24,41 @@ module Idml
17
24
 
18
25
  canvas.save_graphics_state do
19
26
  table.table_row.each_with_index do |row, row_index|
20
- render_row(canvas, row, row_index, row_count, box, row_height)
27
+ render_row(canvas, row, row_index, row_count, box, row_height,
28
+ context)
21
29
  end
22
30
  end
23
31
  end
24
32
 
25
- def self.render_row(canvas, row, row_index, row_count, box, row_height)
33
+ def self.render_row(canvas, row, row_index, row_count, box, row_height,
34
+ context)
26
35
  row_y = box[:y] + ((row_count - 1 - row_index) * row_height)
27
36
  cell_count = row.table_cell.length
28
37
  return unless cell_count.positive?
29
38
 
30
39
  cell_width = box[:width] / cell_count
31
- row.table_cell.each_with_index do |_cell, cell_index|
40
+ row.table_cell.each_with_index do |cell, cell_index|
32
41
  cell_x = box[:x] + (cell_index * cell_width)
33
42
  canvas.rectangle(cell_x, row_y, cell_width, row_height)
34
43
  canvas.stroke
44
+ render_cell_text(canvas, cell, cell_x, row_y, row_height, context)
35
45
  end
36
46
  end
37
47
  private_class_method :render_row
48
+
49
+ def self.render_cell_text(canvas, cell, x, y, height, context)
50
+ text = cell.text_content
51
+ return if text.nil? || text.empty?
52
+
53
+ runs = [{
54
+ text: text,
55
+ font: context.font_ps_name,
56
+ size: DEFAULT_SIZE,
57
+ }]
58
+ baseline = y + (height / 2)
59
+ canvas.text_rich(runs, at: [x + INSET, baseline])
60
+ end
61
+ private_class_method :render_cell_text
38
62
  end
39
63
  end
40
64
  end
@@ -58,8 +58,10 @@ module Idml
58
58
  end
59
59
  private_class_method :engine_render
60
60
 
61
- # Emits one `canvas.text_lines` call per run, after shaping
62
- # and line-breaking with real font metrics. Lines that fall
61
+ # Emits one `canvas.text` call per line, after shaping and
62
+ # line-breaking with real font metrics. Applies paragraph
63
+ # alignment via `Justifier` so each line is offset within
64
+ # the frame box per IDML `Justification`. Lines that fall
63
65
  # below the frame's bottom edge are clipped.
64
66
  def self.render_run_lines(canvas, run, context, box, font, baseline_y)
65
67
  size = run.point_size
@@ -69,26 +71,30 @@ module Idml
69
71
  lines = TextEngine::LineBreaker.break(
70
72
  glyphs: glyphs, frame_width: box[:width],
71
73
  )
72
-
73
- line_texts = []
74
- lines.each do |line|
75
- break if baseline_y < box[:y]
76
-
77
- line_texts << line.glyphs.map { |g| [g.codepoint].pack("U") }.join
78
- baseline_y -= size * LEADING_FACTOR
79
- end
80
-
81
- if line_texts.any?
82
- canvas.text_lines(line_texts,
83
- font: context.font_ps_name,
84
- size: size,
85
- at: [box[:x], box[:y] + box[:height] - size],
86
- leading: size * LEADING_FACTOR)
74
+ alignment = run.alignment || :left
75
+ start_y = box[:y] + box[:height] - size
76
+
77
+ lines.each_with_index do |line, idx|
78
+ line_y = start_y - (idx * size * LEADING_FACTOR)
79
+ break if line_y < box[:y]
80
+
81
+ TextEngine::Justifier.justify(line: line,
82
+ frame_width: box[:width],
83
+ alignment: alignment)
84
+ canvas.text(line_text(line),
85
+ at: [box[:x] + line.x_offset, line_y],
86
+ font: context.font_ps_name,
87
+ size: size)
87
88
  end
88
89
  baseline_y
89
90
  end
90
91
  private_class_method :render_run_lines
91
92
 
93
+ def self.line_text(line)
94
+ line.glyphs.map { |g| [g.codepoint].pack("U") }.join
95
+ end
96
+ private_class_method :line_text
97
+
92
98
  # Fallback when no metrics are available: emit all runs as
93
99
  # one `text_rich` block, letting pdfrb measure advance widths.
94
100
  # Uses pdfrb's measurement API directly (no Fontisan).
@@ -4,15 +4,30 @@ module Idml
4
4
  module Render
5
5
  # Extracts styled text runs from an IDML Story. Each run carries the
6
6
  # text content plus the CharacterStyleRange attributes that affect
7
- # rendering (font style, size, fill color, applied font reference).
7
+ # rendering (font style, size, fill color, applied font reference,
8
+ # paragraph alignment).
8
9
  class StyleResolver
9
10
  StyledRun = Struct.new(
10
11
  :text, :font_style, :point_size,
11
- :fill_color, :fill_tint, :applied_font, keyword_init: true
12
+ :fill_color, :fill_tint, :applied_font, :alignment,
13
+ keyword_init: true
12
14
  )
13
15
 
14
16
  DEFAULT_POINT_SIZE = 12.0
15
17
 
18
+ # IDML `Justification` enum → Justifier symbol. Full justify
19
+ # and binding-side variants defer to :left for now (TODO 80).
20
+ ALIGNMENT_MAP = {
21
+ "Left" => :left,
22
+ "Center" => :center,
23
+ "Right" => :right,
24
+ "LeftJustified" => :left,
25
+ "RightJustified" => :right,
26
+ "CenterJustified" => :center,
27
+ "FullyJustified" => :justified,
28
+ "ToBinding" => :left,
29
+ }.freeze
30
+
16
31
  def self.extract_runs(story)
17
32
  return [] unless story&.inner
18
33
 
@@ -33,11 +48,17 @@ module Idml
33
48
  fill_color: csr.fill_color,
34
49
  fill_tint: csr.fill_tint,
35
50
  applied_font: csr.applied_font,
51
+ alignment: alignment_for(csr),
36
52
  )
37
53
  end
38
54
  end
39
55
  private_class_method :csr_runs
40
56
 
57
+ def self.alignment_for(csr)
58
+ ALIGNMENT_MAP[csr.justification] || :left
59
+ end
60
+ private_class_method :alignment_for
61
+
41
62
  # Concatenate runs into a single block. Used when the renderer
42
63
  # can't handle per-run styling (e.g., no font metrics available).
43
64
  def self.concatenate(runs)
data/lib/idml/render.rb CHANGED
@@ -19,6 +19,10 @@ module Idml
19
19
  autoload :StructureTracker, "#{__dir__}/render/structure_tracker"
20
20
  autoload :StructureMapper, "#{__dir__}/render/structure_mapper"
21
21
  autoload :PdfaPacket, "#{__dir__}/render/pdfa_packet"
22
+ autoload :IccProfile, "#{__dir__}/render/icc_profile"
23
+ autoload :BookmarkResolver, "#{__dir__}/render/bookmark_resolver"
24
+ autoload :HyperlinkResolver, "#{__dir__}/render/hyperlink_resolver"
25
+ autoload :HyperlinkEmitter, "#{__dir__}/render/hyperlink_emitter"
22
26
  autoload :StoryThreader, "#{__dir__}/render/story_threader"
23
27
  autoload :PdfrbWriter, "#{__dir__}/render/pdfrb_writer"
24
28
  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.3.1"
4
+ VERSION = "0.4.0"
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.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -206,6 +206,9 @@ files:
206
206
  - TODO.pdf/77-pdfa-xmp-output-intent.md
207
207
  - TODO.pdf/78-hyperlinks.md
208
208
  - TODO.pdf/79-bookmarks-outline.md
209
+ - TODO.pdf/80-paragraph-alignment.md
210
+ - TODO.pdf/81-pdfa-icc-output-intent.md
211
+ - TODO.pdf/82-table-cell-text.md
209
212
  - TODO.pdf/README.md
210
213
  - exe/idml
211
214
  - idml.gemspec
@@ -223,6 +226,7 @@ files:
223
226
  - lib/idml/elements/baseline_frame_grid_option.rb
224
227
  - lib/idml/elements/bevel_and_emboss_setting.rb
225
228
  - lib/idml/elements/blending_setting.rb
229
+ - lib/idml/elements/bookmark.rb
226
230
  - lib/idml/elements/cell_style.rb
227
231
  - lib/idml/elements/cell_style_group.rb
228
232
  - lib/idml/elements/character_style.rb
@@ -249,6 +253,9 @@ files:
249
253
  - lib/idml/elements/gradient_stop.rb
250
254
  - lib/idml/elements/graphic_line.rb
251
255
  - lib/idml/elements/group.rb
256
+ - lib/idml/elements/hyperlink.rb
257
+ - lib/idml/elements/hyperlink_page_destination.rb
258
+ - lib/idml/elements/hyperlink_url_destination.rb
252
259
  - lib/idml/elements/image.rb
253
260
  - lib/idml/elements/ink.rb
254
261
  - lib/idml/elements/inner_glow_setting.rb
@@ -372,10 +379,14 @@ files:
372
379
  - lib/idml/parts/xmp.rb
373
380
  - lib/idml/render.rb
374
381
  - lib/idml/render/blending.rb
382
+ - lib/idml/render/bookmark_resolver.rb
375
383
  - lib/idml/render/color_helper.rb
376
384
  - lib/idml/render/color_resolver.rb
377
385
  - lib/idml/render/font_reference_resolver.rb
378
386
  - lib/idml/render/gradient_resolver.rb
387
+ - lib/idml/render/hyperlink_emitter.rb
388
+ - lib/idml/render/hyperlink_resolver.rb
389
+ - lib/idml/render/icc_profile.rb
379
390
  - lib/idml/render/image.rb
380
391
  - lib/idml/render/image_collector.rb
381
392
  - lib/idml/render/layer_filter.rb