idml 0.10.10 → 0.10.12
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/TODO.pdf/149-architecture-round-two.md +53 -0
- data/TODO.pdf/150-architecture-round-three.md +41 -0
- data/lib/idml/render/pipeline.rb +10 -8
- data/lib/idml/render/renderers/frame_metrics.rb +160 -0
- data/lib/idml/render/renderers/keep_policy.rb +118 -0
- data/lib/idml/render/renderers/text_frame_renderer.rb +10 -284
- data/lib/idml/render/text_wrap_resolver.rb +0 -29
- data/lib/idml/render.rb +8 -0
- data/lib/idml/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 49122b9c9f5974361567afb8fc2e063602e370141f0d5990f8de156275c312c4
|
|
4
|
+
data.tar.gz: c4f21424d6cd5d2f9345801aeb53be0b78d26f3d0c423b8ad20b179dff4ef38f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c06eb1149c2fed295382a4ce65504d12c821d884ffa6cd145f9ac96e0837c82b1dbfdd16abd082e84366dd6c0bfd6edc8670e187d71785f1de0e7a415cc9892
|
|
7
|
+
data.tar.gz: 12352d559b36427ec1be4ce71e31cfc149e24ec52a1fe70b65c3ecc720575c1a4f7a9aab8b96e67329747f33bc2005494c6f0c2034508b642edf37ba917a45ca
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# TODO PDF 149: Architecture review round two — policy modules + test seam
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-29
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
Follow-up architecture review (hot-spot: the renderer) after
|
|
8
|
+
TODO 148's wing split:
|
|
9
|
+
|
|
10
|
+
1. The keep-options family (paragraph deferral: KeepAllLines /
|
|
11
|
+
KeepFirstLines / KeepLastLines windows, KeepWithNext,
|
|
12
|
+
StartParagraph breaks) was ten private predicates buried in
|
|
13
|
+
the engine — only testable through full end-to-end renders.
|
|
14
|
+
2. Frame-positioning policy (FirstBaselineOffset modes, vertical
|
|
15
|
+
justification, content-height estimation) was interleaved with
|
|
16
|
+
run emission, and `first_paragraph_leading(state)` /
|
|
17
|
+
`first_paragraph_leading_for(paragraph)` were near-duplicate
|
|
18
|
+
leading helpers maintained apart.
|
|
19
|
+
3. The render specs' real interface — the PDF content stream —
|
|
20
|
+
was parsed by copy-pasted regexes (99 scan() sites, five
|
|
21
|
+
private helper reimplementations); two spec bugs this week
|
|
22
|
+
came from exactly this duplication.
|
|
23
|
+
|
|
24
|
+
## Solution
|
|
25
|
+
|
|
26
|
+
- **`Renderers::KeepPolicy`** (~118 lines, module_function): the
|
|
27
|
+
deferral predicates extracted as a standalone pure module. The
|
|
28
|
+
engine calls one entry point,
|
|
29
|
+
`KeepPolicy.paragraph_deferred?(...)`. New direct unit spec
|
|
30
|
+
(`keep_policy_spec.rb`, 14 examples) drives the window
|
|
31
|
+
arithmetic with real `StyleResolver::Paragraph` structs — no
|
|
32
|
+
canvas, writer, or fonts.
|
|
33
|
+
- **`Renderers::FrameMetrics`** (~160 lines, module_function):
|
|
34
|
+
baseline modes, vertical-justify offsets, and content-height
|
|
35
|
+
estimation in one home. The duplicate leading helpers are
|
|
36
|
+
consolidated onto `FrameMetrics.leading_for(paragraph)`, which
|
|
37
|
+
KeepPolicy also reuses.
|
|
38
|
+
- **`PdfStream`** (spec_helper): one named parser for the content
|
|
39
|
+
stream (`text_positions`, `text_ys`, `bt_count`, `stroke_count`,
|
|
40
|
+
`rect_count`); the five private spec helpers and all inline
|
|
41
|
+
scan() idioms replaced.
|
|
42
|
+
|
|
43
|
+
TextFrameRenderer: 1235 → 960 lines (flow + emission only). Suite
|
|
44
|
+
3222 → 3256 examples. Zero behavior change.
|
|
45
|
+
|
|
46
|
+
## Files
|
|
47
|
+
|
|
48
|
+
- `lib/idml/render/renderers/keep_policy.rb` (new)
|
|
49
|
+
- `lib/idml/render/renderers/frame_metrics.rb` (new)
|
|
50
|
+
- `lib/idml/render/renderers/text_frame_renderer.rb`
|
|
51
|
+
- `lib/idml/render.rb`
|
|
52
|
+
- `spec/spec_helper.rb` + 10 render specs
|
|
53
|
+
- `spec/idml/render/renderers/keep_policy_spec.rb` (new)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# TODO PDF 150: Architecture review round three — seam repairs
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-30
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
Round-three review findings (after TODOs 148-149):
|
|
8
|
+
|
|
9
|
+
1. **Page-dimensions seam leak (bug)**: `ImageCollector` and
|
|
10
|
+
`HyperlinkEmitter` received a hardcoded `DEFAULT_HEIGHT = 792`
|
|
11
|
+
while pages rendered at per-page dimensions — both use the
|
|
12
|
+
height for the IDML→PDF y-flip, so images and link rectangles
|
|
13
|
+
misplaced on any non-letter page.
|
|
14
|
+
2. **Dead interface kept alive by its tests**:
|
|
15
|
+
`TextWrapResolver#overlap_width` (+ `line_overlap`) had zero
|
|
16
|
+
lib callers since TODO 138 — only its own spec — and encoded
|
|
17
|
+
sum-semantics for multiple contours where the real interface
|
|
18
|
+
(`wrap_adjustment`) uses max. The spec suite asserted behavior
|
|
19
|
+
the renderer no longer had.
|
|
20
|
+
3. **Thirteen copies of the synthetic-package triplet**: every
|
|
21
|
+
engine-path spec repeated `Dir.mktmpdir → Package.write →
|
|
22
|
+
Package.new`, with drifting names and inconsistent part sets.
|
|
23
|
+
|
|
24
|
+
## Solution
|
|
25
|
+
|
|
26
|
+
- Pipeline threads per-page `dims[:height]` into both consumers;
|
|
27
|
+
the constants stay only as `page_dimensions_for` fallbacks.
|
|
28
|
+
- `overlap_width` / `line_overlap` deleted; all 15 spec call
|
|
29
|
+
sites migrated to `wrap_adjustment` (the "sums contours" spec
|
|
30
|
+
now documents max-combination honestly). The polygon variant
|
|
31
|
+
`WrapContour.overlap_width` stays — it is load-bearing.
|
|
32
|
+
- `build_package(parts, name)` helper in spec_helper; thirteen
|
|
33
|
+
triplet copies replaced; three local helpers that shadowed the
|
|
34
|
+
global renamed (`anchored_fixture`, `decorated_fixture`,
|
|
35
|
+
`footnote_fixture`, `endnote_fixture`, `footnote_package`).
|
|
36
|
+
|
|
37
|
+
## Files
|
|
38
|
+
|
|
39
|
+
- `lib/idml/render/pipeline.rb`
|
|
40
|
+
- `lib/idml/render/text_wrap_resolver.rb`
|
|
41
|
+
- `spec/spec_helper.rb` + 13 spec files
|
data/lib/idml/render/pipeline.rb
CHANGED
|
@@ -96,9 +96,6 @@ module Idml
|
|
|
96
96
|
position_tracker, condition_filter, style_lookup,
|
|
97
97
|
page_offset)
|
|
98
98
|
pages = spread.spread.flat_map(&:page)
|
|
99
|
-
image_refs = ImageCollector.new(writer: writer,
|
|
100
|
-
base_dir: File.dirname(@package.path),
|
|
101
|
-
page_height: DEFAULT_HEIGHT).collect(spread)
|
|
102
99
|
renderer = build_renderer(layer_filter, font_ref_resolver,
|
|
103
100
|
font_resource, font_metrics, font_map,
|
|
104
101
|
structure, position_tracker, condition_filter,
|
|
@@ -108,21 +105,26 @@ module Idml
|
|
|
108
105
|
pages.each do |page|
|
|
109
106
|
current += 1
|
|
110
107
|
dims = page_dimensions_for(page)
|
|
108
|
+
image_refs = ImageCollector.new(
|
|
109
|
+
writer: writer,
|
|
110
|
+
base_dir: File.dirname(@package.path),
|
|
111
|
+
page_height: dims[:height],
|
|
112
|
+
).collect(spread)
|
|
111
113
|
canvas = writer.add_page(width: dims[:width], height: dims[:height])
|
|
112
114
|
renderer.render(canvas, spread, page_width: dims[:width],
|
|
113
115
|
page_height: dims[:height],
|
|
114
116
|
image_refs: image_refs,
|
|
115
117
|
page_index: current)
|
|
116
|
-
emit_hyperlinks(writer, spread, current,
|
|
117
|
-
position_tracker)
|
|
118
|
+
emit_hyperlinks(writer, spread, current, dims[:height],
|
|
119
|
+
layer_filter, position_tracker)
|
|
118
120
|
end
|
|
119
121
|
current
|
|
120
122
|
end
|
|
121
123
|
|
|
122
|
-
def emit_hyperlinks(writer, spread, page_index,
|
|
123
|
-
position_tracker)
|
|
124
|
+
def emit_hyperlinks(writer, spread, page_index, page_height,
|
|
125
|
+
layer_filter, position_tracker)
|
|
124
126
|
HyperlinkEmitter.new(writer: writer, package: @package,
|
|
125
|
-
page_height:
|
|
127
|
+
page_height: page_height,
|
|
126
128
|
layer_filter: layer_filter,
|
|
127
129
|
position_tracker: position_tracker)
|
|
128
130
|
.emit_for(spread, page_index)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Render
|
|
5
|
+
module Renderers
|
|
6
|
+
# Frame positioning policy for the text engine: where the
|
|
7
|
+
# first baseline sits under each FirstBaselineOffset mode,
|
|
8
|
+
# where the content block starts under vertical
|
|
9
|
+
# justification, and how tall the pending content is
|
|
10
|
+
# estimated to be. Pure functions of the TextFrame's
|
|
11
|
+
# preference, the chain state, and font metrics — no canvas,
|
|
12
|
+
# no emission (MECE: this module owns frame-positioning
|
|
13
|
+
# policy; TextFrameRenderer owns flow and emission;
|
|
14
|
+
# KeepPolicy owns paragraph deferral).
|
|
15
|
+
module FrameMetrics
|
|
16
|
+
FIRST_BASELINE_MODES = %w[
|
|
17
|
+
AscentOffset FixedHeight CapHeight XHeight EmboxHeight
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
# pdfrb 0.7.1 never fills the cap_height metric, so
|
|
21
|
+
# CapHeight / XHeight fall back to standard font
|
|
22
|
+
# proportions (cap ≈ 0.72 em, x ≈ 0.52 em — Arial and
|
|
23
|
+
# Helvetica both sit within 1% of these).
|
|
24
|
+
CAP_HEIGHT_RATIO = 0.72
|
|
25
|
+
X_HEIGHT_RATIO = 0.52
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# Top y for the content block under the frame's vertical
|
|
30
|
+
# justification: TopAlign keeps the frame top;
|
|
31
|
+
# CenterAlign / BottomAlign offset by the slack between the
|
|
32
|
+
# estimated content height and the available space
|
|
33
|
+
# (JustifyAlign falls back to TopAlign behavior).
|
|
34
|
+
def vertical_justify_top(top_y, bottom_limit, state, context)
|
|
35
|
+
justification = text_frame_vertical_justification(context)
|
|
36
|
+
return top_y unless %w[CenterAlign BottomAlign].include?(justification)
|
|
37
|
+
|
|
38
|
+
available = top_y - bottom_limit
|
|
39
|
+
slack = [available - estimate_content_height(state), 0].max
|
|
40
|
+
return top_y if slack.zero?
|
|
41
|
+
|
|
42
|
+
case justification
|
|
43
|
+
when "CenterAlign" then top_y - (slack / 2)
|
|
44
|
+
when "BottomAlign" then top_y - slack
|
|
45
|
+
else top_y
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# FirstBaselineOffset: how far below the frame's top inset
|
|
50
|
+
# the first line's baseline sits, relative to the layout's
|
|
51
|
+
# leading-based default. AscentOffset shifts by (ascent −
|
|
52
|
+
# leading), FixedHeight by (MinimumFirstBaselineOffset −
|
|
53
|
+
# leading), CapHeight by the cap-height proportion, XHeight
|
|
54
|
+
# by the x-height proportion, EmboxHeight by the em box
|
|
55
|
+
# (the first paragraph's point size).
|
|
56
|
+
def first_baseline_offset(context, state, font)
|
|
57
|
+
pref = context.item&.text_frame_preference&.first
|
|
58
|
+
mode = pref&.first_baseline_offset
|
|
59
|
+
return 0.0 unless FIRST_BASELINE_MODES.include?(mode)
|
|
60
|
+
|
|
61
|
+
baseline_target(pref, mode, state, font) -
|
|
62
|
+
first_paragraph_leading(state)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Distance from the top inset to the first baseline under
|
|
66
|
+
# the given mode.
|
|
67
|
+
def baseline_target(pref, mode, state, font)
|
|
68
|
+
leading = first_paragraph_leading(state)
|
|
69
|
+
case mode
|
|
70
|
+
when "AscentOffset"
|
|
71
|
+
ratio_scaled(font.ascent, font) * leading
|
|
72
|
+
when "CapHeight"
|
|
73
|
+
cap_ratio(font) * leading
|
|
74
|
+
when "XHeight"
|
|
75
|
+
X_HEIGHT_RATIO * leading
|
|
76
|
+
when "EmboxHeight"
|
|
77
|
+
first_paragraph_size(state)
|
|
78
|
+
else
|
|
79
|
+
pref.minimum_first_baseline_offset || leading
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def cap_ratio(font)
|
|
84
|
+
cap = font.cap_height
|
|
85
|
+
return CAP_HEIGHT_RATIO unless cap
|
|
86
|
+
|
|
87
|
+
ratio_scaled(cap, font)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def ratio_scaled(metric, font)
|
|
91
|
+
upem = font.units_per_em
|
|
92
|
+
return 1.0 if upem.zero?
|
|
93
|
+
|
|
94
|
+
metric / upem
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def first_paragraph_size(state)
|
|
98
|
+
paragraph = state.current_paragraph || state.paragraphs.first
|
|
99
|
+
first_run = paragraph&.runs&.first
|
|
100
|
+
first_run&.point_size || TextFrameRenderer::DEFAULT_SIZE
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Leading of a paragraph computed from its first run's
|
|
104
|
+
# point size (the default size when it has no runs yet).
|
|
105
|
+
def leading_for(paragraph)
|
|
106
|
+
size = paragraph.runs.first&.point_size ||
|
|
107
|
+
TextFrameRenderer::DEFAULT_SIZE
|
|
108
|
+
TextEngine::VerticalLayout.leading_for(paragraph.auto_leading,
|
|
109
|
+
size)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def first_paragraph_leading(state)
|
|
113
|
+
paragraph = state.current_paragraph || state.paragraphs.first
|
|
114
|
+
unless paragraph
|
|
115
|
+
return TextFrameRenderer::DEFAULT_SIZE *
|
|
116
|
+
TextEngine::VerticalLayout::DEFAULT_LEADING_FACTOR
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
leading_for(paragraph)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def text_frame_vertical_justification(context)
|
|
123
|
+
pref = context.item&.text_frame_preference&.first
|
|
124
|
+
pref&.vertical_justification
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Estimates total renderable content height for vertical
|
|
128
|
+
# justification. Walks paragraphs and runs, summing leading
|
|
129
|
+
# per run plus paragraph space_before/after. Approximation:
|
|
130
|
+
# treats each run as one line (wrap may produce more lines,
|
|
131
|
+
# which would over-estimate slack; acceptable for
|
|
132
|
+
# justification since we'd rather under-offset than
|
|
133
|
+
# overflow).
|
|
134
|
+
def estimate_content_height(state)
|
|
135
|
+
paragraphs_for_estimate(state).sum { |p| paragraph_height(p) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def paragraphs_for_estimate(state)
|
|
139
|
+
result = []
|
|
140
|
+
result << state.current_paragraph if state.current_paragraph
|
|
141
|
+
result + state.paragraphs
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def paragraph_height(paragraph)
|
|
145
|
+
size = paragraph.runs.first&.point_size ||
|
|
146
|
+
TextFrameRenderer::DEFAULT_SIZE
|
|
147
|
+
leading = TextEngine::VerticalLayout.leading_for(
|
|
148
|
+
paragraph.auto_leading, size
|
|
149
|
+
)
|
|
150
|
+
line_count = [paragraph.runs.length, 1].max
|
|
151
|
+
(leading * line_count) + space_before_after(paragraph)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def space_before_after(paragraph)
|
|
155
|
+
(paragraph.space_before || 0) + (paragraph.space_after || 0)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Render
|
|
5
|
+
module Renderers
|
|
6
|
+
# Paragraph deferral policy (keep options + forced breaks):
|
|
7
|
+
# decides whether a paragraph moves wholly to the next
|
|
8
|
+
# frame/column instead of being split. Honors StartParagraph
|
|
9
|
+
# forced breaks, KeepAllLinesTogether, the KeepFirstLines /
|
|
10
|
+
# KeepLastLines partial windows (measured-line-count
|
|
11
|
+
# approximation), and KeepWithNext. Pure predicates over the
|
|
12
|
+
# paragraph chain state and frame geometry — no canvas, no
|
|
13
|
+
# emission (MECE: TextFrameRenderer owns flow and emission;
|
|
14
|
+
# FrameMetrics owns frame positioning).
|
|
15
|
+
module KeepPolicy
|
|
16
|
+
BREAK_MODES = %w[
|
|
17
|
+
NextPage NextColumn NextFrame NextOddPage NextEvenPage
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# True when the paragraph should move wholly to the next
|
|
23
|
+
# frame: a StartParagraph forced break, or a keep rule that
|
|
24
|
+
# the remaining space violates (never for the frame's
|
|
25
|
+
# first paragraph, so progress is always made).
|
|
26
|
+
def paragraph_deferred?(paragraph, next_paragraph,
|
|
27
|
+
layout_frame, font, cursor_y,
|
|
28
|
+
bottom_limit, placed_any)
|
|
29
|
+
return true if paragraph_break?(paragraph) && placed_any
|
|
30
|
+
return false unless placed_any
|
|
31
|
+
return true if keep_with_next_break?(paragraph, next_paragraph,
|
|
32
|
+
layout_frame, font,
|
|
33
|
+
cursor_y, bottom_limit)
|
|
34
|
+
|
|
35
|
+
keep_all_lines_break?(paragraph, layout_frame, font,
|
|
36
|
+
cursor_y, bottom_limit) ||
|
|
37
|
+
keep_windows_break?(paragraph, layout_frame, font,
|
|
38
|
+
cursor_y, bottom_limit)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# KeepAllLinesTogether: defer when the paragraph cannot
|
|
42
|
+
# fully fit the remaining space.
|
|
43
|
+
def keep_all_lines_break?(paragraph, layout_frame, font,
|
|
44
|
+
cursor_y, bottom_limit)
|
|
45
|
+
return false unless paragraph.keep_all_lines_together
|
|
46
|
+
|
|
47
|
+
height = paragraph_block_height(paragraph, layout_frame, font)
|
|
48
|
+
(cursor_y - height) < bottom_limit
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Partial keep windows, approximated by measured line
|
|
52
|
+
# counts: KeepFirstLines defers when fewer than N lines
|
|
53
|
+
# fit; KeepLastLines defers when the overflow tail for the
|
|
54
|
+
# next frame would strand fewer than N lines.
|
|
55
|
+
def keep_windows_break?(paragraph, layout_frame, font,
|
|
56
|
+
cursor_y, bottom_limit)
|
|
57
|
+
return false unless paragraph.keep_first_lines ||
|
|
58
|
+
paragraph.keep_last_lines
|
|
59
|
+
|
|
60
|
+
height = paragraph_block_height(paragraph, layout_frame, font)
|
|
61
|
+
return false if (cursor_y - height) >= bottom_limit
|
|
62
|
+
|
|
63
|
+
leading = FrameMetrics.leading_for(paragraph)
|
|
64
|
+
lines_fit = [((cursor_y - bottom_limit) / leading).floor, 0].max
|
|
65
|
+
first_window_break?(paragraph, lines_fit) ||
|
|
66
|
+
last_window_break?(paragraph, lines_fit, height, leading)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def first_window_break?(paragraph, lines_fit)
|
|
70
|
+
paragraph.keep_first_lines &&
|
|
71
|
+
lines_fit < paragraph.keep_first_lines
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def last_window_break?(paragraph, lines_fit, height, leading)
|
|
75
|
+
return false unless paragraph.keep_last_lines
|
|
76
|
+
|
|
77
|
+
total_lines = [(height / leading).ceil, 1].max
|
|
78
|
+
lines_fit.positive? &&
|
|
79
|
+
(total_lines - lines_fit) < paragraph.keep_last_lines
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# KeepWithNext: this paragraph defers when the next
|
|
83
|
+
# paragraph is forced to the next frame, or when the next
|
|
84
|
+
# paragraph's first line would not fit after this one.
|
|
85
|
+
def keep_with_next_break?(paragraph, next_paragraph,
|
|
86
|
+
layout_frame, font, cursor_y,
|
|
87
|
+
bottom_limit)
|
|
88
|
+
return false unless keepable_pair?(paragraph, next_paragraph)
|
|
89
|
+
return true if paragraph_break?(next_paragraph)
|
|
90
|
+
|
|
91
|
+
height = paragraph_block_height(paragraph, layout_frame, font)
|
|
92
|
+
(cursor_y - height - FrameMetrics.leading_for(next_paragraph)) <
|
|
93
|
+
bottom_limit
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def keepable_pair?(paragraph, next_paragraph)
|
|
97
|
+
!paragraph.keep_with_next.nil? && !next_paragraph.nil?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# True when the paragraph requests a forced break to the
|
|
101
|
+
# next frame/column (StartParagraph). All break flavors act
|
|
102
|
+
# at frame/column granularity.
|
|
103
|
+
def paragraph_break?(paragraph)
|
|
104
|
+
BREAK_MODES.include?(paragraph.start_paragraph)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def paragraph_block_height(paragraph, layout_frame, font)
|
|
108
|
+
wrap_width = TextEngine::VerticalLayout.wrap_width(
|
|
109
|
+
layout_frame, paragraph.right_indent || 0
|
|
110
|
+
)
|
|
111
|
+
TextEngine::Measurement.paragraph_height(
|
|
112
|
+
paragraph, font, wrap_width
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -284,8 +284,10 @@ module Idml
|
|
|
284
284
|
(layout_frame.inset_top || 0)
|
|
285
285
|
bottom_limit = TextEngine::VerticalLayout.bottom_limit(layout_frame)
|
|
286
286
|
footnotes = []
|
|
287
|
-
cursor_y = vertical_justify_top(top_y, bottom_limit,
|
|
288
|
-
|
|
287
|
+
cursor_y = FrameMetrics.vertical_justify_top(top_y, bottom_limit,
|
|
288
|
+
state, context)
|
|
289
|
+
cursor_y -= FrameMetrics.first_baseline_offset(context, state,
|
|
290
|
+
font)
|
|
289
291
|
char_cursor = state.char_cursor
|
|
290
292
|
|
|
291
293
|
if state.current_paragraph
|
|
@@ -326,7 +328,9 @@ module Idml
|
|
|
326
328
|
top_y = col_frame.y + col_frame.height - (col_frame.inset_top || 0)
|
|
327
329
|
bottom_limit = TextEngine::VerticalLayout.bottom_limit(col_frame)
|
|
328
330
|
footnotes = []
|
|
329
|
-
cursor_y = top_y - first_baseline_offset(
|
|
331
|
+
cursor_y = top_y - FrameMetrics.first_baseline_offset(
|
|
332
|
+
context, state, font
|
|
333
|
+
)
|
|
330
334
|
char_cursor = state.char_cursor
|
|
331
335
|
|
|
332
336
|
if state.current_paragraph
|
|
@@ -389,160 +393,6 @@ module Idml
|
|
|
389
393
|
private_class_method :build_column_frames
|
|
390
394
|
|
|
391
395
|
# Computes the starting cursor_y for vertical justification.
|
|
392
|
-
# IDML's TextFramePreference.VerticalJustification can be:
|
|
393
|
-
# - TopAlign (default): start at frame top, no offset
|
|
394
|
-
# - CenterAlign: center the content block vertically
|
|
395
|
-
# - BottomAlign: align content to the frame bottom
|
|
396
|
-
# - JustifyAlign: distribute extra space between paragraphs
|
|
397
|
-
# (deferred — falls back to TopAlign behavior)
|
|
398
|
-
#
|
|
399
|
-
# Content height is estimated by walking paragraphs and
|
|
400
|
-
# summing leading × lines + space_before/after. Without
|
|
401
|
-
# shaping/wrapping we can't know exact line count, so we
|
|
402
|
-
# approximate by treating each run as one line.
|
|
403
|
-
def self.vertical_justify_top(top_y, bottom_limit, state, context)
|
|
404
|
-
justification = text_frame_vertical_justification(context)
|
|
405
|
-
return top_y unless %w[CenterAlign BottomAlign].include?(justification)
|
|
406
|
-
|
|
407
|
-
content_height = estimate_content_height(state)
|
|
408
|
-
available = top_y - bottom_limit
|
|
409
|
-
slack = [available - content_height, 0].max
|
|
410
|
-
return top_y if slack.zero?
|
|
411
|
-
|
|
412
|
-
case justification
|
|
413
|
-
when "CenterAlign" then top_y - (slack / 2)
|
|
414
|
-
when "BottomAlign" then top_y - slack
|
|
415
|
-
else top_y
|
|
416
|
-
end
|
|
417
|
-
end
|
|
418
|
-
private_class_method :vertical_justify_top
|
|
419
|
-
|
|
420
|
-
# FirstBaselineOffset: how far below the frame's top inset
|
|
421
|
-
# the first line's baseline sits. The layout's default is
|
|
422
|
-
# leading-based; AscentOffset shifts by (ascent − leading)
|
|
423
|
-
# and FixedHeight by (MinimumFirstBaselineOffset − leading).
|
|
424
|
-
# CapHeight uses the font's cap-height metric (leading
|
|
425
|
-
# fallback when the font reports none); XHeight approximates
|
|
426
|
-
# as 70% of cap height; EmboxHeight uses the em box (the
|
|
427
|
-
# first paragraph's point size). Returns the extra shift to
|
|
428
|
-
# subtract from the cursor.
|
|
429
|
-
FIRST_BASELINE_MODES = %w[
|
|
430
|
-
AscentOffset FixedHeight CapHeight XHeight EmboxHeight
|
|
431
|
-
].freeze
|
|
432
|
-
|
|
433
|
-
def self.first_baseline_offset(context, state, font)
|
|
434
|
-
pref = context.item&.text_frame_preference&.first
|
|
435
|
-
mode = pref&.first_baseline_offset
|
|
436
|
-
return 0.0 unless FIRST_BASELINE_MODES.include?(mode)
|
|
437
|
-
|
|
438
|
-
baseline_target(pref, mode, state, font) -
|
|
439
|
-
first_paragraph_leading(state)
|
|
440
|
-
end
|
|
441
|
-
private_class_method :first_baseline_offset
|
|
442
|
-
|
|
443
|
-
# Distance from the top inset to the first baseline under
|
|
444
|
-
# the given mode.
|
|
445
|
-
# pdfrb 0.7.1 never fills the cap_height metric, so
|
|
446
|
-
# CapHeight / XHeight fall back to standard font
|
|
447
|
-
# proportions (cap ≈ 0.72 em, x ≈ 0.52 em — Arial and
|
|
448
|
-
# Helvetica both sit within 1% of these).
|
|
449
|
-
CAP_HEIGHT_RATIO = 0.72
|
|
450
|
-
X_HEIGHT_RATIO = 0.52
|
|
451
|
-
|
|
452
|
-
def self.baseline_target(pref, mode, state, font)
|
|
453
|
-
leading = first_paragraph_leading(state)
|
|
454
|
-
case mode
|
|
455
|
-
when "AscentOffset"
|
|
456
|
-
ratio_scaled(font.ascent, font) * leading
|
|
457
|
-
when "CapHeight"
|
|
458
|
-
cap_ratio(font) * leading
|
|
459
|
-
when "XHeight"
|
|
460
|
-
X_HEIGHT_RATIO * leading
|
|
461
|
-
when "EmboxHeight"
|
|
462
|
-
first_paragraph_size(state)
|
|
463
|
-
else
|
|
464
|
-
pref.minimum_first_baseline_offset || leading
|
|
465
|
-
end
|
|
466
|
-
end
|
|
467
|
-
private_class_method :baseline_target
|
|
468
|
-
|
|
469
|
-
def self.cap_ratio(font)
|
|
470
|
-
cap = font.cap_height
|
|
471
|
-
return CAP_HEIGHT_RATIO unless cap
|
|
472
|
-
|
|
473
|
-
ratio_scaled(cap, font)
|
|
474
|
-
end
|
|
475
|
-
private_class_method :cap_ratio
|
|
476
|
-
|
|
477
|
-
def self.ratio_scaled(metric, font)
|
|
478
|
-
upem = font.units_per_em
|
|
479
|
-
return 1.0 if upem.zero?
|
|
480
|
-
|
|
481
|
-
metric / upem
|
|
482
|
-
end
|
|
483
|
-
private_class_method :ratio_scaled
|
|
484
|
-
|
|
485
|
-
def self.first_paragraph_size(state)
|
|
486
|
-
paragraph = state.current_paragraph || state.paragraphs.first
|
|
487
|
-
first_run = paragraph&.runs&.first
|
|
488
|
-
first_run&.point_size || DEFAULT_SIZE
|
|
489
|
-
end
|
|
490
|
-
private_class_method :first_paragraph_size
|
|
491
|
-
|
|
492
|
-
def self.first_paragraph_leading(state)
|
|
493
|
-
paragraph = state.current_paragraph || state.paragraphs.first
|
|
494
|
-
unless paragraph
|
|
495
|
-
return DEFAULT_SIZE *
|
|
496
|
-
TextEngine::VerticalLayout::DEFAULT_LEADING_FACTOR
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
size = paragraph.runs.first&.point_size || DEFAULT_SIZE
|
|
500
|
-
TextEngine::VerticalLayout.leading_for(paragraph.auto_leading,
|
|
501
|
-
size)
|
|
502
|
-
end
|
|
503
|
-
private_class_method :first_paragraph_leading
|
|
504
|
-
|
|
505
|
-
def self.text_frame_vertical_justification(context)
|
|
506
|
-
pref = context.item&.text_frame_preference&.first
|
|
507
|
-
pref&.vertical_justification
|
|
508
|
-
end
|
|
509
|
-
private_class_method :text_frame_vertical_justification
|
|
510
|
-
|
|
511
|
-
# Estimates total renderable content height for vertical
|
|
512
|
-
# justification. Walks paragraphs and runs, summing leading
|
|
513
|
-
# per run plus paragraph space_before/after. Approximation:
|
|
514
|
-
# treats each run as one line (wrap may produce more lines,
|
|
515
|
-
# which would over-estimate slack; acceptable for
|
|
516
|
-
# justification since we'd rather under-offset than overflow).
|
|
517
|
-
def self.estimate_content_height(state)
|
|
518
|
-
paragraphs = paragraphs_for_estimate(state)
|
|
519
|
-
return 0 if paragraphs.empty?
|
|
520
|
-
|
|
521
|
-
paragraphs.sum do |paragraph|
|
|
522
|
-
paragraph_height(paragraph)
|
|
523
|
-
end
|
|
524
|
-
end
|
|
525
|
-
private_class_method :estimate_content_height
|
|
526
|
-
|
|
527
|
-
def self.paragraphs_for_estimate(state)
|
|
528
|
-
result = []
|
|
529
|
-
result << state.current_paragraph if state.current_paragraph
|
|
530
|
-
result + state.paragraphs
|
|
531
|
-
end
|
|
532
|
-
private_class_method :paragraphs_for_estimate
|
|
533
|
-
|
|
534
|
-
def self.paragraph_height(paragraph)
|
|
535
|
-
size = paragraph.runs.first&.point_size || DEFAULT_SIZE
|
|
536
|
-
leading = leading_for(paragraph, size)
|
|
537
|
-
line_count = [paragraph.runs.length, 1].max
|
|
538
|
-
(leading * line_count) + space_before_after(paragraph)
|
|
539
|
-
end
|
|
540
|
-
private_class_method :paragraph_height
|
|
541
|
-
|
|
542
|
-
def self.space_before_after(paragraph)
|
|
543
|
-
(paragraph.space_before || 0) + (paragraph.space_after || 0)
|
|
544
|
-
end
|
|
545
|
-
private_class_method :space_before_after
|
|
546
396
|
|
|
547
397
|
def self.resume_paragraph(canvas, state, context, layout_frame, font,
|
|
548
398
|
cursor_y, bottom_limit, char_cursor,
|
|
@@ -571,9 +421,9 @@ module Idml
|
|
|
571
421
|
break if cursor_y < bottom_limit
|
|
572
422
|
|
|
573
423
|
next_paragraph = state.paragraphs[index + 1]
|
|
574
|
-
break if paragraph_deferred?(paragraph, next_paragraph,
|
|
575
|
-
|
|
576
|
-
|
|
424
|
+
break if KeepPolicy.paragraph_deferred?(paragraph, next_paragraph,
|
|
425
|
+
layout_frame, font, cursor_y,
|
|
426
|
+
bottom_limit, pending.positive?)
|
|
577
427
|
|
|
578
428
|
remaining_runs, cursor_y, char_cursor = render_runs_for_paragraph(
|
|
579
429
|
canvas, paragraph, paragraph.runs, context,
|
|
@@ -967,130 +817,6 @@ module Idml
|
|
|
967
817
|
end
|
|
968
818
|
private_class_method :mojikumi_aki_overrides
|
|
969
819
|
|
|
970
|
-
# True when the paragraph should move wholly to the next
|
|
971
|
-
# frame: a StartParagraph forced break, or KeepAllLines-
|
|
972
|
-
# Together with insufficient remaining space (never for the
|
|
973
|
-
# frame's first paragraph, so progress is always made).
|
|
974
|
-
def self.paragraph_deferred?(paragraph, next_paragraph,
|
|
975
|
-
layout_frame, font, cursor_y,
|
|
976
|
-
bottom_limit, placed_any)
|
|
977
|
-
return true if paragraph_break?(paragraph) && placed_any
|
|
978
|
-
return false unless placed_any
|
|
979
|
-
return true if keep_with_next_break?(paragraph, next_paragraph,
|
|
980
|
-
layout_frame, font,
|
|
981
|
-
cursor_y, bottom_limit)
|
|
982
|
-
|
|
983
|
-
keep_all_lines_break?(paragraph, layout_frame, font,
|
|
984
|
-
cursor_y, bottom_limit) ||
|
|
985
|
-
keep_windows_break?(paragraph, layout_frame, font,
|
|
986
|
-
cursor_y, bottom_limit)
|
|
987
|
-
end
|
|
988
|
-
private_class_method :paragraph_deferred?
|
|
989
|
-
|
|
990
|
-
# KeepAllLinesTogether: defer when the paragraph cannot
|
|
991
|
-
# fully fit the remaining space.
|
|
992
|
-
def self.keep_all_lines_break?(paragraph, layout_frame, font,
|
|
993
|
-
cursor_y, bottom_limit)
|
|
994
|
-
return false unless paragraph.keep_all_lines_together
|
|
995
|
-
|
|
996
|
-
wrap_width = TextEngine::VerticalLayout.wrap_width(
|
|
997
|
-
layout_frame, paragraph.right_indent || 0
|
|
998
|
-
)
|
|
999
|
-
height = TextEngine::Measurement.paragraph_height(
|
|
1000
|
-
paragraph, font, wrap_width
|
|
1001
|
-
)
|
|
1002
|
-
(cursor_y - height) < bottom_limit
|
|
1003
|
-
end
|
|
1004
|
-
private_class_method :keep_all_lines_break?
|
|
1005
|
-
|
|
1006
|
-
# Partial keep windows, approximated by measured line
|
|
1007
|
-
# counts: KeepFirstLines defers when fewer than N lines
|
|
1008
|
-
# fit; KeepLastLines defers when the overflow tail for the
|
|
1009
|
-
# next frame would strand fewer than N lines.
|
|
1010
|
-
def self.keep_windows_break?(paragraph, layout_frame, font,
|
|
1011
|
-
cursor_y, bottom_limit)
|
|
1012
|
-
return false unless paragraph.keep_first_lines ||
|
|
1013
|
-
paragraph.keep_last_lines
|
|
1014
|
-
|
|
1015
|
-
wrap_width = TextEngine::VerticalLayout.wrap_width(
|
|
1016
|
-
layout_frame, paragraph.right_indent || 0
|
|
1017
|
-
)
|
|
1018
|
-
height = TextEngine::Measurement.paragraph_height(
|
|
1019
|
-
paragraph, font, wrap_width
|
|
1020
|
-
)
|
|
1021
|
-
return false if (cursor_y - height) >= bottom_limit
|
|
1022
|
-
|
|
1023
|
-
leading = first_paragraph_leading_for(paragraph)
|
|
1024
|
-
lines_fit = [((cursor_y - bottom_limit) / leading).floor, 0].max
|
|
1025
|
-
first_window_break?(paragraph, lines_fit) ||
|
|
1026
|
-
last_window_break?(paragraph, lines_fit, height, leading)
|
|
1027
|
-
end
|
|
1028
|
-
private_class_method :keep_windows_break?
|
|
1029
|
-
|
|
1030
|
-
def self.first_window_break?(paragraph, lines_fit)
|
|
1031
|
-
paragraph.keep_first_lines &&
|
|
1032
|
-
lines_fit < paragraph.keep_first_lines
|
|
1033
|
-
end
|
|
1034
|
-
private_class_method :first_window_break?
|
|
1035
|
-
|
|
1036
|
-
def self.last_window_break?(paragraph, lines_fit, height, leading)
|
|
1037
|
-
return false unless paragraph.keep_last_lines
|
|
1038
|
-
|
|
1039
|
-
total_lines = [(height / leading).ceil, 1].max
|
|
1040
|
-
lines_fit.positive? &&
|
|
1041
|
-
(total_lines - lines_fit) < paragraph.keep_last_lines
|
|
1042
|
-
end
|
|
1043
|
-
private_class_method :last_window_break?
|
|
1044
|
-
|
|
1045
|
-
def self.first_paragraph_leading_for(paragraph)
|
|
1046
|
-
size = paragraph.runs.first&.point_size || DEFAULT_SIZE
|
|
1047
|
-
TextEngine::VerticalLayout.leading_for(paragraph.auto_leading,
|
|
1048
|
-
size)
|
|
1049
|
-
end
|
|
1050
|
-
private_class_method :first_paragraph_leading_for
|
|
1051
|
-
|
|
1052
|
-
# KeepWithNext: this paragraph defers when the next
|
|
1053
|
-
# paragraph is forced to the next frame, or when the next
|
|
1054
|
-
# paragraph's first line would not fit after this one.
|
|
1055
|
-
def self.keep_with_next_break?(paragraph, next_paragraph,
|
|
1056
|
-
layout_frame, font, cursor_y,
|
|
1057
|
-
bottom_limit)
|
|
1058
|
-
return false unless keepable_pair?(paragraph, next_paragraph)
|
|
1059
|
-
return true if paragraph_break?(next_paragraph)
|
|
1060
|
-
|
|
1061
|
-
wrap_width = TextEngine::VerticalLayout.wrap_width(
|
|
1062
|
-
layout_frame, paragraph.right_indent || 0
|
|
1063
|
-
)
|
|
1064
|
-
height = TextEngine::Measurement.paragraph_height(
|
|
1065
|
-
paragraph, font, wrap_width
|
|
1066
|
-
)
|
|
1067
|
-
(cursor_y - height - follower_leading(next_paragraph)) <
|
|
1068
|
-
bottom_limit
|
|
1069
|
-
end
|
|
1070
|
-
private_class_method :keep_with_next_break?
|
|
1071
|
-
|
|
1072
|
-
def self.keepable_pair?(paragraph, next_paragraph)
|
|
1073
|
-
!paragraph.keep_with_next.nil? && !next_paragraph.nil?
|
|
1074
|
-
end
|
|
1075
|
-
private_class_method :keepable_pair?
|
|
1076
|
-
|
|
1077
|
-
def self.follower_leading(next_paragraph)
|
|
1078
|
-
size = next_paragraph.runs.first&.point_size || DEFAULT_SIZE
|
|
1079
|
-
TextEngine::VerticalLayout.leading_for(
|
|
1080
|
-
next_paragraph.auto_leading, size
|
|
1081
|
-
)
|
|
1082
|
-
end
|
|
1083
|
-
private_class_method :follower_leading
|
|
1084
|
-
|
|
1085
|
-
# True when the paragraph requests a forced break to the
|
|
1086
|
-
# next frame/column (StartParagraph). All break flavors act
|
|
1087
|
-
# at frame/column granularity.
|
|
1088
|
-
def self.paragraph_break?(paragraph)
|
|
1089
|
-
%w[NextPage NextColumn NextFrame NextOddPage NextEvenPage]
|
|
1090
|
-
.include?(paragraph.start_paragraph)
|
|
1091
|
-
end
|
|
1092
|
-
private_class_method :paragraph_break?
|
|
1093
|
-
|
|
1094
820
|
# Justifies the run's lines; only the final line of the
|
|
1095
821
|
# paragraph's final run stays ragged under full
|
|
1096
822
|
# justification.
|
|
@@ -159,28 +159,6 @@ module Idml
|
|
|
159
159
|
end
|
|
160
160
|
private :interval_side_adjustment
|
|
161
161
|
|
|
162
|
-
# Returns the total horizontal overlap of all contours with a
|
|
163
|
-
# text line at the given y range within the given x range.
|
|
164
|
-
# Sum, not max — multiple overlapping contours compound.
|
|
165
|
-
# Returns 0.0 when there's no overlap. Inverse contours
|
|
166
|
-
# reduce by the frame width minus their own width (text may
|
|
167
|
-
# only flow inside).
|
|
168
|
-
def overlap_width(line_y, line_height, frame_x, frame_right)
|
|
169
|
-
@contours.sum do |contour|
|
|
170
|
-
width = if contour.is_a?(WrapContour::Shape)
|
|
171
|
-
WrapContour.overlap_width(contour, line_y,
|
|
172
|
-
line_height, frame_x,
|
|
173
|
-
frame_right)
|
|
174
|
-
else
|
|
175
|
-
line_overlap(contour, line_y, line_height,
|
|
176
|
-
frame_x, frame_right)[:width]
|
|
177
|
-
end
|
|
178
|
-
next width unless contour.inverse
|
|
179
|
-
|
|
180
|
-
(frame_right - frame_x) - width
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
|
|
184
162
|
# The wrap shape for an item: a bounding-box rectangle
|
|
185
163
|
# (BoundingBoxTextWrap — the legacy "BoundingBox" spelling is
|
|
186
164
|
# also accepted from early synthetic fixtures) or a flattened
|
|
@@ -250,13 +228,6 @@ module Idml
|
|
|
250
228
|
end
|
|
251
229
|
private_class_method :contour_mode?
|
|
252
230
|
|
|
253
|
-
def line_overlap(contour, line_y, line_height, frame_x, frame_right)
|
|
254
|
-
return { width: 0.0 } unless y_overlap?(contour, line_y, line_height)
|
|
255
|
-
return { width: 0.0 } unless x_overlap?(contour, frame_x, frame_right)
|
|
256
|
-
|
|
257
|
-
{ width: x_overlap_width(contour, frame_x, frame_right) }
|
|
258
|
-
end
|
|
259
|
-
|
|
260
231
|
def y_overlap?(contour, line_y, line_height)
|
|
261
232
|
line_top = line_y + line_height
|
|
262
233
|
line_bottom = line_y
|
data/lib/idml/render.rb
CHANGED
|
@@ -100,3 +100,11 @@ Idml::Render::Renderers.autoload(
|
|
|
100
100
|
:SimpleText,
|
|
101
101
|
"#{__dir__}/render/renderers/simple_text",
|
|
102
102
|
)
|
|
103
|
+
Idml::Render::Renderers.autoload(
|
|
104
|
+
:FrameMetrics,
|
|
105
|
+
"#{__dir__}/render/renderers/frame_metrics",
|
|
106
|
+
)
|
|
107
|
+
Idml::Render::Renderers.autoload(
|
|
108
|
+
:KeepPolicy,
|
|
109
|
+
"#{__dir__}/render/renderers/keep_policy",
|
|
110
|
+
)
|
data/lib/idml/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: idml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.10.
|
|
4
|
+
version: 0.10.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-30 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|
|
@@ -212,7 +212,9 @@ files:
|
|
|
212
212
|
- TODO.pdf/146-next-column-wrap.md
|
|
213
213
|
- TODO.pdf/147-side-aware-shapes.md
|
|
214
214
|
- TODO.pdf/148-architecture-improvements.md
|
|
215
|
+
- TODO.pdf/149-architecture-round-two.md
|
|
215
216
|
- TODO.pdf/15-wire-spread-children.md
|
|
217
|
+
- TODO.pdf/150-architecture-round-three.md
|
|
216
218
|
- TODO.pdf/16-typed-model-pipeline.md
|
|
217
219
|
- TODO.pdf/17-shape-rendering.md
|
|
218
220
|
- TODO.pdf/18-text-rendering.md
|
|
@@ -502,8 +504,10 @@ files:
|
|
|
502
504
|
- lib/idml/render/position_tracker.rb
|
|
503
505
|
- lib/idml/render/render_context.rb
|
|
504
506
|
- lib/idml/render/renderers.rb
|
|
507
|
+
- lib/idml/render/renderers/frame_metrics.rb
|
|
505
508
|
- lib/idml/render/renderers/graphic_line_renderer.rb
|
|
506
509
|
- lib/idml/render/renderers/group_renderer.rb
|
|
510
|
+
- lib/idml/render/renderers/keep_policy.rb
|
|
507
511
|
- lib/idml/render/renderers/oval_renderer.rb
|
|
508
512
|
- lib/idml/render/renderers/path_renderer.rb
|
|
509
513
|
- lib/idml/render/renderers/polygon_renderer.rb
|