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 +4 -4
- data/Gemfile.lock +2 -2
- data/TODO.pdf/78-hyperlinks.md +54 -74
- data/TODO.pdf/79-bookmarks-outline.md +41 -67
- data/TODO.pdf/80-paragraph-alignment.md +39 -0
- data/TODO.pdf/81-pdfa-icc-output-intent.md +51 -0
- data/TODO.pdf/82-table-cell-text.md +54 -0
- data/lib/idml/elements/bookmark.rb +25 -0
- data/lib/idml/elements/hyperlink.rb +27 -0
- data/lib/idml/elements/hyperlink_page_destination.rb +34 -0
- data/lib/idml/elements/hyperlink_url_destination.rb +25 -0
- data/lib/idml/elements/table_cell.rb +14 -1
- data/lib/idml/elements.rb +6 -0
- data/lib/idml/parts/designmap.rb +12 -0
- data/lib/idml/render/bookmark_resolver.rb +87 -0
- data/lib/idml/render/hyperlink_emitter.rb +74 -0
- data/lib/idml/render/hyperlink_resolver.rb +68 -0
- data/lib/idml/render/icc_profile.rb +50 -0
- data/lib/idml/render/pdfrb_writer.rb +10 -0
- data/lib/idml/render/pipeline.rb +33 -1
- data/lib/idml/render/renderers/table_renderer.rb +27 -3
- data/lib/idml/render/renderers/text_frame_renderer.rb +23 -17
- data/lib/idml/render/style_resolver.rb +23 -2
- data/lib/idml/render.rb +4 -0
- data/lib/idml/version.rb +1 -1
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d6cd95e24b0c6610673ad2c95308314a391471fa3912d28a660b8381d46f7b81
|
|
4
|
+
data.tar.gz: f33fdd9480345a9de8f8fa0e60536b87423978a09732031ab7a4587b99fae59e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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
|
data/TODO.pdf/78-hyperlinks.md
CHANGED
|
@@ -1,79 +1,59 @@
|
|
|
1
1
|
# TODO PDF 78: Hyperlink annotations
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
- [
|
|
69
|
-
- [
|
|
70
|
-
- [
|
|
71
|
-
- [
|
|
72
|
-
|
|
73
|
-
|
|
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:
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
- [
|
|
63
|
-
- [
|
|
64
|
-
- [
|
|
65
|
-
- [
|
|
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
|
|
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"
|
data/lib/idml/parts/designmap.rb
CHANGED
|
@@ -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
|
data/lib/idml/render/pipeline.rb
CHANGED
|
@@ -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
|
-
|
|
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 |
|
|
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.
|
|
62
|
-
#
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
canvas.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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,
|
|
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
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.
|
|
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
|