idml 0.6.2 → 0.7.1
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 +1 -1
- data/TODO.pdf/100-character-level-styling.md +11 -1
- data/TODO.pdf/101-paragraph-rules-borders.md +10 -1
- data/TODO.pdf/104-table-schema-completion.md +6 -1
- data/TODO.pdf/105-whole-package-round-trip.md +4 -1
- data/TODO.pdf/108-multi-frame-story-flow.md +50 -0
- data/TODO.pdf/109-dry-color-tint.md +42 -0
- data/TODO.pdf/110-conditional-text.md +37 -0
- data/TODO.pdf/111-footnote-support.md +40 -0
- data/TODO.pdf/94-text-frame-vertical-layout.md +5 -1
- data/TODO.pdf/95-paragraph-spacing-indents.md +2 -1
- data/TODO.pdf/96-text-frame-insets-actual.md +3 -1
- data/TODO.pdf/97-table-cell-rendering-fidelity.md +4 -1
- data/TODO.pdf/98-rowspan-columnspan.md +4 -1
- data/TODO.pdf/99-table-column-widths.md +5 -1
- data/idml.gemspec +1 -1
- data/lib/idml/elements/table.rb +48 -4
- data/lib/idml/render/character_style.rb +148 -0
- data/lib/idml/render/color_helper.rb +27 -3
- data/lib/idml/render/paragraph_rules.rb +125 -0
- data/lib/idml/render/renderers/table_renderer.rb +188 -27
- data/lib/idml/render/renderers/text_frame_renderer.rb +217 -59
- data/lib/idml/render/style_resolver.rb +123 -7
- data/lib/idml/render.rb +2 -0
- data/lib/idml/text_engine/vertical_layout.rb +50 -45
- data/lib/idml/text_engine.rb +3 -2
- data/lib/idml/version.rb +1 -1
- metadata +10 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f43caa091c83d9d8288f22f3913d21760bf5a5dac986f76b72521bb19f5110e0
|
|
4
|
+
data.tar.gz: 4e33ef7a0c119936d16c0eecb687d153e04d9dc29d789511633fb6fff7f1a152
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16b0f93a7c8993c5f98ad84c6e186ed61ebcaaabcdda63b26c9d1e4bfadd1c3986bef933d2a832d2b3463ab5b23324778a37e92e62cbeb8ce0876dbc26bebcbe
|
|
7
|
+
data.tar.gz: 9950c7afceda42f377e0d2bef226c4caa346c9d79ac4b3b61dcba99612a321c646eecd6b341be88fd7f8a46c695ecc2449d1bf4d900e4338ed1a4c9f76ea4d45
|
data/Gemfile
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# TODO PDF 100: Character-level styling (underline, strikethrough, caps, color)
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: DONE — new `Render::CharacterStyle` helper applies
|
|
4
|
+
per-run fill color, draws underline / strike-through rules, and
|
|
5
|
+
handles Capitalization (AllCaps/SmallCaps → uppercase) and Position
|
|
6
|
+
(Superscript/Subscript → shrunk font + baseline offset).
|
|
7
|
+
TextFrameRenderer.emit_line wraps canvas.text via CharacterStyle.
|
|
8
|
+
StyledRun extended with underline_offset/weight, strike_through_offset/weight
|
|
9
|
+
attrs from CSR.
|
|
10
|
+
|
|
11
|
+
Open follow-ups (lower priority): glyph horizontal/vertical scaling
|
|
12
|
+
(needs pdfrb text-matrix support), tracking (needs Shaper-level
|
|
13
|
+
width adjustment), text stroke outline.
|
|
4
14
|
|
|
5
15
|
## Problem
|
|
6
16
|
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# TODO PDF 101: Paragraph borders and rules (RuleAbove, RuleBelow, ParagraphBorder)
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: PARTIAL — RuleAbove and RuleBelow implemented via new
|
|
4
|
+
`Render::ParagraphRules` helper; TextFrameRenderer emits them at
|
|
5
|
+
paragraph top/bottom. Rule color comes from PSR#stroke_color (IDML's
|
|
6
|
+
PSR doesn't have a dedicated RuleAboveColor attribute). RuleAboveWidth
|
|
7
|
+
"Text" vs "Column" honored. Tint scaling + line weight + offset honored.
|
|
8
|
+
|
|
9
|
+
ParagraphBorder and ParagraphShading remain open — they need the
|
|
10
|
+
paragraph's bounding rect, which the renderer doesn't currently
|
|
11
|
+
expose to ParagraphRules. Will add when a real document fixture
|
|
12
|
+
exercises them.
|
|
4
13
|
|
|
5
14
|
## Problem
|
|
6
15
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# TODO PDF 104: Schema-faithful Table model completion
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: PARTIAL — added the rendering-relevant subset (column_count,
|
|
4
|
+
single_column_width, header/body/footer row counts, table_direction,
|
|
5
|
+
outer-border strokes). The remaining ~150 schema attributes (alternating
|
|
6
|
+
stroke rows, column rules, table-flavor bits) aren't needed for
|
|
7
|
+
rendering and would be pure model bloat. Add incrementally as use
|
|
8
|
+
cases demand.
|
|
4
9
|
|
|
5
10
|
## Problem
|
|
6
11
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# TODO PDF 105: Whole-package round-trip suite (byte-equivalent)
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: DONE — `spec/idml/round_trip_all_fixtures_spec.rb` already
|
|
4
|
+
runs byte-equivalent round-trip for every fixture .idml (28 examples,
|
|
5
|
+
all passing). `spec/idml/package_spec.rb:140` verifies the mimetype
|
|
6
|
+
entry is stored and first.
|
|
4
7
|
|
|
5
8
|
## Problem
|
|
6
9
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# TODO PDF 108: Multi-frame story flow (use StoryThreader)
|
|
2
|
+
|
|
3
|
+
## Status: OPEN — gap identified in 2026-08-09 audit
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
`Render::StoryThreader` exists with specs (`spec/idml/render_layer_threader_spec.rb`).
|
|
8
|
+
It builds text-frame chains from PreviousTextFrame/NextTextFrame
|
|
9
|
+
links. The intent: a story flows across linked frames, with each
|
|
10
|
+
frame getting the lines that fit and overflow going to the next.
|
|
11
|
+
|
|
12
|
+
But `TextFrameRenderer` doesn't use it. The renderer treats each
|
|
13
|
+
frame independently:
|
|
14
|
+
- `chain_head?` filter skips non-head frames (correct — they'd
|
|
15
|
+
double-render)
|
|
16
|
+
- Each chain-head frame renders its entire story from the top
|
|
17
|
+
- Overflow that doesn't fit is silently dropped
|
|
18
|
+
|
|
19
|
+
For real documents with multi-page stories (books, magazines,
|
|
20
|
+
newspapers), this loses text. The PDF shows only what fits in the
|
|
21
|
+
first frame.
|
|
22
|
+
|
|
23
|
+
## What needs to happen
|
|
24
|
+
|
|
25
|
+
1. TextFrameRenderer accepts the chain (list of frames) instead
|
|
26
|
+
of one frame at a time.
|
|
27
|
+
2. For each chain:
|
|
28
|
+
- Lay out story text starting at frame 1's top
|
|
29
|
+
- When text overflows frame 1's bottom, continue at frame 2's top
|
|
30
|
+
- Repeat until either text ends or chain ends
|
|
31
|
+
3. The PageItemRenderer dispatch needs to know about chains, not
|
|
32
|
+
just per-item rendering.
|
|
33
|
+
|
|
34
|
+
Architecture: the chain context needs to live in RenderContext
|
|
35
|
+
(or a new RenderChainContext). Each frame's render call passes the
|
|
36
|
+
current y cursor + remaining paragraphs/runs/lines, and the next
|
|
37
|
+
frame's render call picks up where the previous left off.
|
|
38
|
+
|
|
39
|
+
## Acceptance criteria
|
|
40
|
+
|
|
41
|
+
- [ ] Story that overflows frame 1 continues in frame 2.
|
|
42
|
+
- [ ] Hyperlink rects resolve correctly across frames.
|
|
43
|
+
- [ ] Spec: 2 linked TextFrames, story of 50 lines, each frame fits
|
|
44
|
+
20 lines → frame 1 shows lines 1-20, frame 2 shows 21-40,
|
|
45
|
+
frame 3 (if present) shows 41-50.
|
|
46
|
+
|
|
47
|
+
## Dependencies
|
|
48
|
+
|
|
49
|
+
- None — independent feature work, but changes the renderer's
|
|
50
|
+
per-frame abstraction significantly.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# TODO PDF 109: DRY color tint application (ColorHelper vs CharacterStyle vs ParagraphRules)
|
|
2
|
+
|
|
3
|
+
## Status: DONE — `ColorHelper.apply_tint(color, tint)` is the single
|
|
4
|
+
canonical tint helper. CharacterStyle and ParagraphRules both delegate
|
|
5
|
+
to it. 7 new specs cover nil tint, tint >= 1.0, RGB/CMYK scaling,
|
|
6
|
+
tint 0.0, unknown models, nil input.
|
|
7
|
+
|
|
8
|
+
## Problem
|
|
9
|
+
|
|
10
|
+
Color tint (scaling a color's components toward zero) is implemented
|
|
11
|
+
in three places with slightly different shapes:
|
|
12
|
+
|
|
13
|
+
1. `Render::CharacterStyle.apply_tint(color, tint)` — used for
|
|
14
|
+
text fill color.
|
|
15
|
+
2. `Render::ParagraphRules.scale_color(color, tint_value)` — used
|
|
16
|
+
for paragraph rule colors.
|
|
17
|
+
3. (Implicit) `Render::ColorResolver` resolves tints on its own
|
|
18
|
+
when reading ColorValue, before returning the color hash.
|
|
19
|
+
|
|
20
|
+
The first two are essentially the same logic with different method
|
|
21
|
+
names. The third means a `ColorResolver.resolve` result might or
|
|
22
|
+
might not have tint already applied — callers can't tell.
|
|
23
|
+
|
|
24
|
+
## What needs to happen
|
|
25
|
+
|
|
26
|
+
1. Pick one canonical tint helper. Either:
|
|
27
|
+
- Move to `ColorHelper.apply_tint(color, tint)`, or
|
|
28
|
+
- Create `Render::ColorTint.apply(color, tint)`.
|
|
29
|
+
2. CharacterStyle and ParagraphRules call the canonical helper.
|
|
30
|
+
3. ColorResolver documents whether it applies tint or returns raw
|
|
31
|
+
colors (today it appears to apply tint internally).
|
|
32
|
+
|
|
33
|
+
## Acceptance criteria
|
|
34
|
+
|
|
35
|
+
- [ ] Single tint helper in one place.
|
|
36
|
+
- [ ] CharacterStyle and ParagraphRules both delegate to it.
|
|
37
|
+
- [ ] ColorResolver's tint behavior is documented.
|
|
38
|
+
- [ ] Spec coverage for tint 0.0, 0.5, 1.0 on RGB and CMYK colors.
|
|
39
|
+
|
|
40
|
+
## Dependencies
|
|
41
|
+
|
|
42
|
+
- None — pure refactor.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# TODO PDF 110: Conditional text (AppliedConditions)
|
|
2
|
+
|
|
3
|
+
## Status: OPEN — gap identified in 2026-08-09 audit
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
PSR and CSR both carry an `AppliedConditions` attribute (a list of
|
|
8
|
+
condition Self IDs). In InDesign, conditions let authors mark text
|
|
9
|
+
as conditional (e.g., "show only in instructor edition", "draft
|
|
10
|
+
review comments"). Each `<Condition>` in Resources/Styles.xml
|
|
11
|
+
declares whether it's visible, its highlight color, etc.
|
|
12
|
+
|
|
13
|
+
Today the renderer ignores `AppliedConditions` — all conditional
|
|
14
|
+
text renders regardless of condition visibility. For documents that
|
|
15
|
+
use conditions heavily (textbooks, legal, regulatory), this means
|
|
16
|
+
the PDF always shows every variant instead of the chosen subset.
|
|
17
|
+
|
|
18
|
+
## What needs to happen
|
|
19
|
+
|
|
20
|
+
1. Verify `Elements::Condition` exists in the model (Resources/Styles.rnc).
|
|
21
|
+
2. Add `Render::ConditionFilter` that reads visible conditions from
|
|
22
|
+
Resources/Styles.xml.
|
|
23
|
+
3. StyleResolver filters PSRs/CSRs whose AppliedConditions reference
|
|
24
|
+
a hidden condition.
|
|
25
|
+
4. Spec: PSR with condition A (hidden) doesn't render; condition B
|
|
26
|
+
(visible) renders.
|
|
27
|
+
|
|
28
|
+
## Acceptance criteria
|
|
29
|
+
|
|
30
|
+
- [ ] Condition model from RNC.
|
|
31
|
+
- [ ] ConditionFilter.from_styles(graphic_or_styles).
|
|
32
|
+
- [ ] StyleResolver.extract_paragraphs takes optional ConditionFilter.
|
|
33
|
+
- [ ] Hidden-condition runs filtered out.
|
|
34
|
+
|
|
35
|
+
## Dependencies
|
|
36
|
+
|
|
37
|
+
- None — pure feature work on existing models.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# TODO PDF 111: Footnote support
|
|
2
|
+
|
|
3
|
+
## Status: OPEN — gap identified in 2026-08-09 audit
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
IDML has full footnote support:
|
|
8
|
+
- `<Footnote>` elements in Stories (inline footnote markers + text)
|
|
9
|
+
- `<FootnoteOption>` on PSR (numbering rules, layout)
|
|
10
|
+
- `TextFramePreference.FootnotesEnableOverrides`,
|
|
11
|
+
`FootnotesSpanAcrossColumns`, `FootnotesMinimumSpacing`,
|
|
12
|
+
`FootnotesSpaceBetween` — frame-level footnote layout
|
|
13
|
+
- `<TextFrameFootnoteOptionsObject>` — per-frame footnote config
|
|
14
|
+
|
|
15
|
+
The renderer doesn't model footnote content or lay it out.
|
|
16
|
+
Documents with footnotes (academic, legal, technical) currently
|
|
17
|
+
lose footnote text in the PDF.
|
|
18
|
+
|
|
19
|
+
## What needs to happen
|
|
20
|
+
|
|
21
|
+
1. Model `<Footnote>` element (story-embedded footnote text +
|
|
22
|
+
reference marker).
|
|
23
|
+
2. Story parsing extracts footnotes alongside main text.
|
|
24
|
+
3. Footnote text renders at the bottom of the page that contains
|
|
25
|
+
the reference marker.
|
|
26
|
+
4. Footnote numbering rules from FootnoteOption honored.
|
|
27
|
+
5. Continuous footnotes flow across pages.
|
|
28
|
+
|
|
29
|
+
This is a large feature — defer until needed by a real fixture.
|
|
30
|
+
|
|
31
|
+
## Acceptance criteria
|
|
32
|
+
|
|
33
|
+
- [ ] Footnote text rendered at bottom of containing page.
|
|
34
|
+
- [ ] Footnote marker in body text links to footnote text.
|
|
35
|
+
- [ ] Numbering restarts per document section per FootnoteOption.
|
|
36
|
+
|
|
37
|
+
## Dependencies
|
|
38
|
+
|
|
39
|
+
- TODO 108 (multi-frame story flow) — footnotes need to know which
|
|
40
|
+
page a reference appears on.
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# TODO PDF 94: TextFrameRenderer should delegate to VerticalLayout
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: DONE — VerticalLayout refactored to block-style API
|
|
4
|
+
(`layout_block` returns positioned lines + next cursor y);
|
|
5
|
+
TextFrameRenderer now uses StyleResolver.extract_paragraphs +
|
|
6
|
+
VerticalLayout.layout_block for proper vertical stacking across
|
|
7
|
+
paragraphs and runs.
|
|
4
8
|
|
|
5
9
|
## Problem
|
|
6
10
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# TODO PDF 95: Paragraph spacing and indents (SpaceBefore, SpaceAfter, FirstLineIndent)
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: DONE — Paragraph struct carries these from PSR; renderer
|
|
4
|
+
passes them to VerticalLayout.layout_block.
|
|
4
5
|
|
|
5
6
|
## Problem
|
|
6
7
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# TODO PDF 96: TextFramePreference insets honored in layout
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: DONE — layout_frame helper reads TextFramePreference
|
|
4
|
+
insets and passes them to the Frame struct; VerticalLayout honors
|
|
5
|
+
them in positioning and wrap_width.
|
|
4
6
|
|
|
5
7
|
## Problem
|
|
6
8
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# TODO PDF 97: Table cell rendering — fill, stroke, insets, vertical justification
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: DONE — cell fill_color renders as background; per-cell
|
|
4
|
+
TextTopInset/Left/Bottom/Right honored (was hardcoded 4.0);
|
|
5
|
+
VerticalJustification positions text Top/Bottom/Center; per-cell
|
|
6
|
+
PSR walks typed runs with their own font/size.
|
|
4
7
|
|
|
5
8
|
## Problem
|
|
6
9
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# TODO PDF 98: RowSpan / ColumnSpan (merged cells) in TableRenderer
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: DONE — SchemaLayout reads cell.column_span and cell.row_span,
|
|
4
|
+
multiplies width/height by the span, tracks a coverage set, and skips
|
|
5
|
+
positions covered by a spanning cell. Spanning cell renders once at
|
|
6
|
+
its top-left corner.
|
|
4
7
|
|
|
5
8
|
## Problem
|
|
6
9
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# TODO PDF 99: Table column widths from ColumnAttributes
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: DONE — Table now models `column_count` and
|
|
4
|
+
`single_column_width`; SchemaLayout uses SingleColumnWidth when
|
|
5
|
+
declared, else falls back to even division. (IDML has no separate
|
|
6
|
+
ColumnAttributes element — column widths live on Table directly
|
|
7
|
+
when uniform.)
|
|
4
8
|
|
|
5
9
|
## Problem
|
|
6
10
|
|
data/idml.gemspec
CHANGED
|
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
|
|
|
31
31
|
spec.add_dependency "bigdecimal"
|
|
32
32
|
spec.add_dependency "logger"
|
|
33
33
|
spec.add_dependency "lutaml-model", "~> 0.8.18"
|
|
34
|
-
spec.add_dependency "pdfrb", "~> 0.
|
|
34
|
+
spec.add_dependency "pdfrb", "~> 0.7"
|
|
35
35
|
spec.add_dependency "rubyzip"
|
|
36
36
|
spec.add_dependency "thor"
|
|
37
37
|
|
data/lib/idml/elements/table.rb
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Idml
|
|
4
4
|
module Elements
|
|
5
|
-
# `<Table>` — a table page item. Carries geometry
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
5
|
+
# `<Table>` — a table page item. Carries geometry, body/header
|
|
6
|
+
# row counts, column layout, and border styling. Two child
|
|
7
|
+
# collections hold the table's content:
|
|
8
|
+
#
|
|
9
|
+
# - `row` — schema-faithful `<Row>` siblings (real IDML).
|
|
10
|
+
# - `cell` — schema-faithful `<Cell>` siblings (real IDML).
|
|
11
|
+
# - `table_row` — legacy nested `<TableRow><TableCell>` form
|
|
12
|
+
# (synthetic test fixture only; see TODO 84).
|
|
13
|
+
#
|
|
14
|
+
# The renderer auto-detects which layout is present.
|
|
9
15
|
class Table < Lutaml::Model::Serializable
|
|
10
16
|
attribute :self_attr, :string
|
|
11
17
|
attribute :name, :string
|
|
@@ -15,6 +21,28 @@ module Idml
|
|
|
15
21
|
attribute :fill_color, :string
|
|
16
22
|
attribute :stroke_color, :string
|
|
17
23
|
attribute :stroke_weight, :float
|
|
24
|
+
|
|
25
|
+
# Schema-faithful layout attributes (from Table_Object in
|
|
26
|
+
# Stories/Story.rnc). The renderer consults these to compute
|
|
27
|
+
# column widths and to clip body/header/footer regions.
|
|
28
|
+
attribute :header_row_count, :integer
|
|
29
|
+
attribute :footer_row_count, :integer
|
|
30
|
+
attribute :body_row_count, :integer
|
|
31
|
+
attribute :column_count, :integer
|
|
32
|
+
attribute :single_column_width, :float
|
|
33
|
+
attribute :table_direction, :string
|
|
34
|
+
|
|
35
|
+
# Outer-border strokes (TopBorderStrokeWeight, etc.). Useful
|
|
36
|
+
# when the renderer draws the table's bounding rectangle.
|
|
37
|
+
attribute :top_border_stroke_weight, :float
|
|
38
|
+
attribute :top_border_stroke_color, :string
|
|
39
|
+
attribute :left_border_stroke_weight, :float
|
|
40
|
+
attribute :left_border_stroke_color, :string
|
|
41
|
+
attribute :bottom_border_stroke_weight, :float
|
|
42
|
+
attribute :bottom_border_stroke_color, :string
|
|
43
|
+
attribute :right_border_stroke_weight, :float
|
|
44
|
+
attribute :right_border_stroke_color, :string
|
|
45
|
+
|
|
18
46
|
attribute :table_row, Idml::Elements::TableRow, collection: true
|
|
19
47
|
attribute :cell, Idml::Elements::Cell, collection: true
|
|
20
48
|
attribute :row, Idml::Elements::Row, collection: true
|
|
@@ -30,6 +58,22 @@ module Idml
|
|
|
30
58
|
map_attribute "FillColor", to: :fill_color
|
|
31
59
|
map_attribute "StrokeColor", to: :stroke_color
|
|
32
60
|
map_attribute "StrokeWeight", to: :stroke_weight
|
|
61
|
+
map_attribute "HeaderRowCount", to: :header_row_count
|
|
62
|
+
map_attribute "FooterRowCount", to: :footer_row_count
|
|
63
|
+
map_attribute "BodyRowCount", to: :body_row_count
|
|
64
|
+
map_attribute "ColumnCount", to: :column_count
|
|
65
|
+
map_attribute "SingleColumnWidth", to: :single_column_width
|
|
66
|
+
map_attribute "TableDirection", to: :table_direction
|
|
67
|
+
map_attribute "TopBorderStrokeWeight", to: :top_border_stroke_weight
|
|
68
|
+
map_attribute "TopBorderStrokeColor", to: :top_border_stroke_color
|
|
69
|
+
map_attribute "LeftBorderStrokeWeight", to: :left_border_stroke_weight
|
|
70
|
+
map_attribute "LeftBorderStrokeColor", to: :left_border_stroke_color
|
|
71
|
+
map_attribute "BottomBorderStrokeWeight",
|
|
72
|
+
to: :bottom_border_stroke_weight
|
|
73
|
+
map_attribute "BottomBorderStrokeColor",
|
|
74
|
+
to: :bottom_border_stroke_color
|
|
75
|
+
map_attribute "RightBorderStrokeWeight", to: :right_border_stroke_weight
|
|
76
|
+
map_attribute "RightBorderStrokeColor", to: :right_border_stroke_color
|
|
33
77
|
map_element "TableRow", to: :table_row
|
|
34
78
|
map_element "Cell", to: :cell
|
|
35
79
|
map_element "Row", to: :row
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Render
|
|
5
|
+
# Applies character-level styling from a `StyleResolver::StyledRun`
|
|
6
|
+
# around a text-emitting block. Centralizes the IDML → PDF mapping
|
|
7
|
+
# for run-level visual properties (text color, underline rule,
|
|
8
|
+
# strike-through rule, capitalization, super/subscript position).
|
|
9
|
+
#
|
|
10
|
+
# Used by `TextFrameRenderer` so character-level effects live in
|
|
11
|
+
# one place (DRY). The block typically calls `canvas.text` (or
|
|
12
|
+
# `canvas.text_rich`); this helper sets the surrounding graphics
|
|
13
|
+
# state and emits the rule lines after the block returns.
|
|
14
|
+
module CharacterStyle
|
|
15
|
+
UNDERLINE_THICKNESS_FACTOR = 0.05
|
|
16
|
+
UNDERLINE_DROP_FACTOR = 0.10
|
|
17
|
+
STRIKE_THICKNESS_FACTOR = 0.05
|
|
18
|
+
STRIKE_RISE_FACTOR = 0.25
|
|
19
|
+
|
|
20
|
+
# Yields with the run's fill color applied, then draws underline
|
|
21
|
+
# and/or strike-through rules above/below the text baseline.
|
|
22
|
+
# `x`, `y`, `width`, `size` describe the line's geometry so the
|
|
23
|
+
# rules can be positioned correctly. The block's emitted text
|
|
24
|
+
# gets the run's character-level adjustments baked in via
|
|
25
|
+
# `text_kwargs` (caller passes the canvas.text kwargs in, and
|
|
26
|
+
# CharacterStyle adds tracking/scale on top).
|
|
27
|
+
def self.apply(canvas, run, context, x:, y:, width:, size:)
|
|
28
|
+
apply_fill_color(canvas, run, context)
|
|
29
|
+
yield
|
|
30
|
+
draw_underline(canvas, run, x, y, width, size)
|
|
31
|
+
draw_strike_through(canvas, run, x, y, width, size)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns the canvas.text kwargs augmented with the run's
|
|
35
|
+
# Tracking (PDF char_spacing). IDML Tracking is in points;
|
|
36
|
+
# pdfrb's char_spacing is also in text-space units, so the
|
|
37
|
+
# mapping is 1:1.
|
|
38
|
+
def self.text_kwargs(run, base_kwargs)
|
|
39
|
+
return base_kwargs unless run.tracking
|
|
40
|
+
|
|
41
|
+
base_kwargs.merge(char_spacing: run.tracking.to_f)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Yields within a transformed coordinate space when the run
|
|
45
|
+
# declares glyph scaling. IDML HorizontalScale/VerticalScale
|
|
46
|
+
# are percentages (100 = no scaling). When both are 100 / nil,
|
|
47
|
+
# yields without transformation (no graphics-state push).
|
|
48
|
+
def self.with_glyph_scaling(canvas, run, &)
|
|
49
|
+
sx = scale_factor(run.horizontal_scale)
|
|
50
|
+
sy = scale_factor(run.vertical_scale)
|
|
51
|
+
return yield if scaling_identity?(sx, sy)
|
|
52
|
+
|
|
53
|
+
canvas.scale(sx, sy, &)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.scaling_identity?(sx, sy)
|
|
57
|
+
(sx - 1.0).abs < FLOAT_EPSILON && (sy - 1.0).abs < FLOAT_EPSILON
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
FLOAT_EPSILON = 1e-9
|
|
61
|
+
private_constant :FLOAT_EPSILON
|
|
62
|
+
|
|
63
|
+
# Returns the run's baseline shift (in PDF units). Positive
|
|
64
|
+
# values shift up; negative shift down. Used to offset the
|
|
65
|
+
# text's `at: y` for CSR's BaselineShift.
|
|
66
|
+
def self.baseline_offset(run)
|
|
67
|
+
run.baseline_shift&.nonzero? ? run.baseline_shift.to_f : 0.0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Transforms the text per the CSR's Capitalization attribute.
|
|
71
|
+
# AllCaps/SmallCaps → uppercase (true small-caps would require
|
|
72
|
+
# an OpenType feature; deferred). Other values pass through.
|
|
73
|
+
def self.transform_text(text, capitalization)
|
|
74
|
+
return text unless %w[AllCaps SmallCaps].include?(capitalization)
|
|
75
|
+
|
|
76
|
+
text.upcase
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Returns `[font_size, baseline_offset]` for the CSR's Position
|
|
80
|
+
# attribute. Superscript/subscript shrink the font and shift
|
|
81
|
+
# the baseline. Normal / nil returns the size unchanged.
|
|
82
|
+
def self.position_scale(position, font_size)
|
|
83
|
+
case position
|
|
84
|
+
when "Superscript" then [font_size * 0.583, font_size * 0.333]
|
|
85
|
+
when "Subscript" then [font_size * 0.583, -font_size * 0.0833]
|
|
86
|
+
else [font_size, 0.0]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.apply_fill_color(canvas, run, context)
|
|
91
|
+
return unless run.fill_color
|
|
92
|
+
return if run.fill_color == "Color/None"
|
|
93
|
+
|
|
94
|
+
color = context.color_resolver&.resolve(run.fill_color)
|
|
95
|
+
return unless color
|
|
96
|
+
|
|
97
|
+
tinted = ColorHelper.apply_tint(color, run.fill_tint)
|
|
98
|
+
canvas.fill_color(ColorHelper.to_canvas(tinted))
|
|
99
|
+
end
|
|
100
|
+
private_class_method :apply_fill_color
|
|
101
|
+
|
|
102
|
+
def self.scale_factor(declared)
|
|
103
|
+
return 1.0 unless declared
|
|
104
|
+
return 1.0 if declared <= 0
|
|
105
|
+
|
|
106
|
+
declared.to_f / 100.0
|
|
107
|
+
end
|
|
108
|
+
private_class_method :scale_factor
|
|
109
|
+
|
|
110
|
+
def self.draw_underline(canvas, run, x, y, width, size)
|
|
111
|
+
return unless run.underline
|
|
112
|
+
|
|
113
|
+
thickness = rule_thickness(run.underline_weight, size,
|
|
114
|
+
UNDERLINE_THICKNESS_FACTOR)
|
|
115
|
+
drop = rule_offset(run.underline_offset, size,
|
|
116
|
+
UNDERLINE_DROP_FACTOR)
|
|
117
|
+
canvas.rectangle(x, y - drop, width, thickness)
|
|
118
|
+
canvas.fill
|
|
119
|
+
end
|
|
120
|
+
private_class_method :draw_underline
|
|
121
|
+
|
|
122
|
+
def self.draw_strike_through(canvas, run, x, y, width, size)
|
|
123
|
+
return unless run.strike_thru
|
|
124
|
+
|
|
125
|
+
thickness = rule_thickness(run.strike_through_weight, size,
|
|
126
|
+
STRIKE_THICKNESS_FACTOR)
|
|
127
|
+
rise = rule_offset(run.strike_through_offset, size,
|
|
128
|
+
STRIKE_RISE_FACTOR)
|
|
129
|
+
canvas.rectangle(x, y + rise - thickness, width, thickness)
|
|
130
|
+
canvas.fill
|
|
131
|
+
end
|
|
132
|
+
private_class_method :draw_strike_through
|
|
133
|
+
|
|
134
|
+
def self.rule_thickness(declared, size, factor)
|
|
135
|
+
declared&.positive? ? declared : (size * factor)
|
|
136
|
+
end
|
|
137
|
+
private_class_method :rule_thickness
|
|
138
|
+
|
|
139
|
+
def self.rule_offset(declared, size, factor)
|
|
140
|
+
return size * factor unless declared
|
|
141
|
+
return size * factor if declared.zero?
|
|
142
|
+
|
|
143
|
+
declared
|
|
144
|
+
end
|
|
145
|
+
private_class_method :rule_offset
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# frozen_string: true
|
|
2
2
|
|
|
3
3
|
module Idml
|
|
4
4
|
module Render
|
|
5
|
-
# Converts IDML color hashes to pdfrb Canvas color arrays
|
|
6
|
-
# Centralizes the mapping so all renderers
|
|
5
|
+
# Converts IDML color hashes to pdfrb Canvas color arrays and
|
|
6
|
+
# applies tint scaling. Centralizes the mapping so all renderers
|
|
7
|
+
# use a consistent format.
|
|
7
8
|
module ColorHelper
|
|
8
9
|
module_function
|
|
9
10
|
|
|
@@ -18,6 +19,29 @@ module Idml
|
|
|
18
19
|
when :cmyk then [:cmyk, color[:c], color[:m], color[:y], color[:k]]
|
|
19
20
|
end
|
|
20
21
|
end
|
|
22
|
+
|
|
23
|
+
# Scales a color's components by `tint` (1.0 = full strength,
|
|
24
|
+
# 0.5 = halved). Tint is the IDML mechanism for lightening a
|
|
25
|
+
# color without defining a new one. Returns the original color
|
|
26
|
+
# unchanged when tint is nil or >= 1.0. Single canonical
|
|
27
|
+
# implementation — CharacterStyle, ParagraphRules, and any
|
|
28
|
+
# future caller should all delegate here.
|
|
29
|
+
def apply_tint(color, tint)
|
|
30
|
+
return color unless color
|
|
31
|
+
return color unless tint
|
|
32
|
+
return color if tint >= 1.0
|
|
33
|
+
|
|
34
|
+
case color[:model]
|
|
35
|
+
when :rgb
|
|
36
|
+
{ model: :rgb, r: color[:r] * tint, g: color[:g] * tint,
|
|
37
|
+
b: color[:b] * tint }
|
|
38
|
+
when :cmyk
|
|
39
|
+
{ model: :cmyk, c: color[:c] * tint, m: color[:m] * tint,
|
|
40
|
+
y: color[:y] * tint, k: color[:k] * tint }
|
|
41
|
+
else
|
|
42
|
+
color
|
|
43
|
+
end
|
|
44
|
+
end
|
|
21
45
|
end
|
|
22
46
|
end
|
|
23
47
|
end
|