idml 0.3.0 → 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 -18
- data/TODO.pdf/63-replace-fontmetrics-with-pdfrb.md +59 -58
- data/TODO.pdf/65-pdfrb-019-integration.md +57 -29
- data/TODO.pdf/67-text-rich-multi-run.md +35 -39
- 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/idml.gemspec +0 -1
- 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 +60 -18
- data/lib/idml/render/render_context.rb +1 -1
- data/lib/idml/render/renderers/group_renderer.rb +3 -1
- data/lib/idml/render/renderers/table_renderer.rb +27 -3
- data/lib/idml/render/renderers/text_frame_renderer.rb +54 -39
- data/lib/idml/render/spread_renderer.rb +3 -3
- data/lib/idml/render/style_resolver.rb +23 -2
- data/lib/idml/render.rb +4 -0
- data/lib/idml/text_engine/pdfrb_font_metrics.rb +81 -0
- data/lib/idml/text_engine.rb +1 -2
- data/lib/idml/version.rb +1 -1
- metadata +13 -17
- data/lib/idml/text_engine/font_metrics.rb +0 -226
- data/lib/idml/text_engine/font_resolver.rb +0 -105
|
@@ -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).
|
data/idml.gemspec
CHANGED
|
@@ -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
|