idml 0.7.1 → 0.7.2
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/108-multi-frame-story-flow.md +5 -1
- data/TODO.pdf/110-conditional-text.md +7 -1
- data/lib/idml/elements/condition.rb +31 -0
- data/lib/idml/elements.rb +1 -0
- data/lib/idml/parts/designmap.rb +2 -0
- data/lib/idml/render/condition_filter.rb +57 -0
- data/lib/idml/render/pipeline.rb +8 -4
- data/lib/idml/render/render_context.rb +2 -0
- data/lib/idml/render/renderers/text_frame_renderer.rb +186 -57
- data/lib/idml/render/spread_renderer.rb +5 -1
- data/lib/idml/render/story_chain_controller.rb +66 -0
- data/lib/idml/render/style_resolver.rb +17 -3
- data/lib/idml/render.rb +2 -0
- data/lib/idml/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c6cc7058e67830e2dc6ea523ea6ca054f702ce29b51989a19deb023e5a840bee
|
|
4
|
+
data.tar.gz: 434b5af5ea49b9f2b48728b100b612ff0cd784079abfd6d419dbeb16554b3ebb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a59cabfafa44138cb40ed8b6e493396c110ffe43728c0d0ea94d8ef4d8039efad635ba23660852b1040b58ce7141b6e301bf27ecb96599efd6d72069e6d81a95
|
|
7
|
+
data.tar.gz: d142ebcfc9eb6591827c72d3cd33303ee3a4c70637031b03622ea1355d453cac4aac1654f60ee7a3ee54a050755e88fb84c32c01df11df802c434442c0f9a4e2
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# TODO PDF 108: Multi-frame story flow (use StoryThreader)
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: DONE — `Render::StoryChainController` threads paragraph/run
|
|
4
|
+
state across linked TextFrames. TextFrameRenderer no longer treats
|
|
5
|
+
chain non-heads as standalone; it picks up leftover state from the
|
|
6
|
+
controller, renders what fits, and stores the new leftover for the
|
|
7
|
+
next frame. Overflow text is no longer silently dropped.
|
|
4
8
|
|
|
5
9
|
## Problem
|
|
6
10
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# TODO PDF 110: Conditional text (AppliedConditions)
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: DONE — `Elements::Condition` models the `<Condition>`
|
|
4
|
+
element from designmap.rnc. `Render::ConditionFilter` indexes
|
|
5
|
+
conditions by Self and answers `visible?(applied_conditions_string)`
|
|
6
|
+
per run. `StyleResolver.extract_paragraphs` takes optional
|
|
7
|
+
`condition_filter:` kwarg and drops PSRs/CSRs whose AppliedConditions
|
|
8
|
+
reference a hidden Condition. Pipeline constructs the filter from
|
|
9
|
+
`designmap.condition` and threads it through RenderContext.
|
|
4
10
|
|
|
5
11
|
## Problem
|
|
6
12
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Elements
|
|
5
|
+
# `<Condition>` — a single condition declared in `designmap.xml`.
|
|
6
|
+
# Authors tag text runs with one or more conditions (the CSR's
|
|
7
|
+
# `AppliedConditions` attribute is a list of condition Self IDs).
|
|
8
|
+
# The condition's `Visible` flag controls whether tagged text
|
|
9
|
+
# appears in the rendered output.
|
|
10
|
+
#
|
|
11
|
+
# Schema: `Condition_Object` in
|
|
12
|
+
# `reference-docs/schemas/package/designmap.rnc`.
|
|
13
|
+
class Condition < Lutaml::Model::Serializable
|
|
14
|
+
attribute :self_attr, :string
|
|
15
|
+
attribute :name, :string
|
|
16
|
+
attribute :indicator_method, :string
|
|
17
|
+
attribute :underline_indicator_appearance, :string
|
|
18
|
+
attribute :visible, :boolean, default: true
|
|
19
|
+
|
|
20
|
+
xml do
|
|
21
|
+
root "Condition"
|
|
22
|
+
map_attribute "Self", to: :self_attr
|
|
23
|
+
map_attribute "Name", to: :name
|
|
24
|
+
map_attribute "IndicatorMethod", to: :indicator_method
|
|
25
|
+
map_attribute "UnderlineIndicatorAppearance",
|
|
26
|
+
to: :underline_indicator_appearance
|
|
27
|
+
map_attribute "Visible", to: :visible
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/idml/elements.rb
CHANGED
|
@@ -17,6 +17,7 @@ module Idml
|
|
|
17
17
|
autoload :ButtonPreference, "idml/elements/pref_button_preference"
|
|
18
18
|
autoload :Bookmark, "idml/elements/bookmark"
|
|
19
19
|
autoload :Cell, "idml/elements/cell"
|
|
20
|
+
autoload :Condition, "idml/elements/condition"
|
|
20
21
|
autoload :CellStyle, "idml/elements/cell_style"
|
|
21
22
|
autoload :CellStyleGroup, "idml/elements/cell_style_group"
|
|
22
23
|
autoload :ChapterNumberPreference,
|
data/lib/idml/parts/designmap.rb
CHANGED
|
@@ -48,6 +48,7 @@ module Idml
|
|
|
48
48
|
|
|
49
49
|
attribute :layer, Idml::Elements::Layer, collection: true
|
|
50
50
|
attribute :bookmark, Idml::Elements::Bookmark, collection: true
|
|
51
|
+
attribute :condition, Idml::Elements::Condition, collection: true
|
|
51
52
|
attribute :hyperlink, Idml::Elements::Hyperlink, collection: true
|
|
52
53
|
attribute :hyperlink_page_destination,
|
|
53
54
|
Idml::Elements::HyperlinkPageDestination, collection: true
|
|
@@ -92,6 +93,7 @@ module Idml
|
|
|
92
93
|
map_attribute "ActiveProcess", to: :active_process
|
|
93
94
|
map_element "Layer", to: :layer
|
|
94
95
|
map_element "Bookmark", to: :bookmark
|
|
96
|
+
map_element "Condition", to: :condition
|
|
95
97
|
map_element "Hyperlink", to: :hyperlink
|
|
96
98
|
map_element "HyperlinkPageDestination",
|
|
97
99
|
to: :hyperlink_page_destination
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Render
|
|
5
|
+
# Resolves whether a styled run should render based on its
|
|
6
|
+
# AppliedConditions and the visibility flags declared on the
|
|
7
|
+
# corresponding `<Condition>` elements in `designmap.xml`.
|
|
8
|
+
#
|
|
9
|
+
# A run is visible when either:
|
|
10
|
+
# - It has no AppliedConditions (the common case), OR
|
|
11
|
+
# - Every condition it references is itself Visible.
|
|
12
|
+
#
|
|
13
|
+
# A run is hidden when ANY referenced condition is hidden. This
|
|
14
|
+
# matches InDesign's "hide any text the user has toggled off"
|
|
15
|
+
# semantics.
|
|
16
|
+
#
|
|
17
|
+
# Construction: `ConditionFilter.from_designmap(designmap)`
|
|
18
|
+
# reads the condition collection and indexes by Self. Use
|
|
19
|
+
# `filter.visible?(applied_conditions_string)` per run; pass nil
|
|
20
|
+
# or empty string for untagged runs.
|
|
21
|
+
class ConditionFilter
|
|
22
|
+
def self.from_designmap(designmap)
|
|
23
|
+
conditions = designmap ? designmap.condition : []
|
|
24
|
+
new(conditions)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(conditions)
|
|
28
|
+
@visibility_by_self = conditions.to_h do |cond|
|
|
29
|
+
[cond.self_attr, condition_visible?(cond)]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# `applied_conditions` is the raw PSR/CSR string. IDML encodes
|
|
34
|
+
# it as a space-separated list of condition Self IDs. Returns
|
|
35
|
+
# true when the run should render.
|
|
36
|
+
def visible?(applied_conditions)
|
|
37
|
+
return true if applied_conditions.nil?
|
|
38
|
+
return true if applied_conditions.strip.empty?
|
|
39
|
+
|
|
40
|
+
applied_conditions.split.uniq.all? do |cond_self|
|
|
41
|
+
condition_visible?(cond_self)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# A condition is visible when explicitly Visible="true" OR when
|
|
48
|
+
# the Visibility flag is unset (default true). Only an explicit
|
|
49
|
+
# Visible="false" hides it.
|
|
50
|
+
def condition_visible?(cond_or_self)
|
|
51
|
+
return @visibility_by_self.fetch(cond_or_self, true) if cond_or_self.is_a?(String)
|
|
52
|
+
|
|
53
|
+
cond_or_self.visible.nil? || cond_or_self.visible
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/idml/render/pipeline.rb
CHANGED
|
@@ -38,6 +38,7 @@ module Idml
|
|
|
38
38
|
structure = StructureTracker.new(enabled: @tagged)
|
|
39
39
|
position_tracker = PositionTracker.new
|
|
40
40
|
layer_filter = LayerFilter.from_designmap(@package.designmap)
|
|
41
|
+
condition_filter = ConditionFilter.from_designmap(@package.designmap)
|
|
41
42
|
font_ref_resolver = FontReferenceResolver.build(@package)
|
|
42
43
|
font_setup = FontSetup.new(package: @package,
|
|
43
44
|
font_search_paths: @font_search_paths)
|
|
@@ -50,7 +51,8 @@ module Idml
|
|
|
50
51
|
page_index = render_spread(writer, spread, layer_filter,
|
|
51
52
|
font_ref_resolver, font_resource,
|
|
52
53
|
font_metrics, font_map, structure,
|
|
53
|
-
position_tracker,
|
|
54
|
+
position_tracker, condition_filter,
|
|
55
|
+
page_index)
|
|
54
56
|
end
|
|
55
57
|
|
|
56
58
|
structure.flush(writer)
|
|
@@ -90,14 +92,14 @@ module Idml
|
|
|
90
92
|
|
|
91
93
|
def render_spread(writer, spread, layer_filter, font_ref_resolver,
|
|
92
94
|
font_resource, font_metrics, font_map, structure,
|
|
93
|
-
position_tracker, page_offset)
|
|
95
|
+
position_tracker, condition_filter, page_offset)
|
|
94
96
|
pages = spread.spread.flat_map(&:page)
|
|
95
97
|
image_refs = ImageCollector.new(writer: writer,
|
|
96
98
|
base_dir: File.dirname(@package.path),
|
|
97
99
|
page_height: DEFAULT_HEIGHT).collect(spread)
|
|
98
100
|
renderer = build_renderer(layer_filter, font_ref_resolver,
|
|
99
101
|
font_resource, font_metrics, font_map,
|
|
100
|
-
structure, position_tracker)
|
|
102
|
+
structure, position_tracker, condition_filter)
|
|
101
103
|
current = page_offset
|
|
102
104
|
|
|
103
105
|
pages.each do |page|
|
|
@@ -126,7 +128,8 @@ module Idml
|
|
|
126
128
|
end
|
|
127
129
|
|
|
128
130
|
def build_renderer(layer_filter, font_ref_resolver, font_resource,
|
|
129
|
-
font_metrics, font_map, structure, position_tracker
|
|
131
|
+
font_metrics, font_map, structure, position_tracker,
|
|
132
|
+
condition_filter)
|
|
130
133
|
SpreadRenderer.new(
|
|
131
134
|
font_metrics: font_metrics,
|
|
132
135
|
font_ps_name: font_resource,
|
|
@@ -136,6 +139,7 @@ module Idml
|
|
|
136
139
|
font_ref_resolver: font_ref_resolver,
|
|
137
140
|
structure: structure,
|
|
138
141
|
position_tracker: position_tracker,
|
|
142
|
+
condition_filter: condition_filter,
|
|
139
143
|
)
|
|
140
144
|
end
|
|
141
145
|
|
|
@@ -30,7 +30,6 @@ module Idml
|
|
|
30
30
|
def self.render(canvas, context)
|
|
31
31
|
frame = context.item
|
|
32
32
|
return unless frame.parent_story
|
|
33
|
-
return unless chain_head?(frame)
|
|
34
33
|
|
|
35
34
|
story = context.package&.story_by_id(frame.parent_story)
|
|
36
35
|
return unless story
|
|
@@ -38,12 +37,53 @@ module Idml
|
|
|
38
37
|
box = frame_box(frame, context.page_height)
|
|
39
38
|
render_inline_tables(canvas, story, box, context)
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
return
|
|
40
|
+
state = initial_chain_state(frame, story, context)
|
|
41
|
+
return unless state
|
|
43
42
|
|
|
44
|
-
render_text(canvas,
|
|
43
|
+
render_text(canvas, state, context, box)
|
|
44
|
+
store_chain_state(frame, state, context)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# Returns the StoryChainController::State to render this frame
|
|
48
|
+
# with. For chain heads (no predecessor), this is a fresh
|
|
49
|
+
# state built from extract_paragraphs. For chain non-heads,
|
|
50
|
+
# this is the leftover state from the previous frame in the
|
|
51
|
+
# chain (nil when there's nothing left to render).
|
|
52
|
+
def self.initial_chain_state(frame, story, context)
|
|
53
|
+
controller = context.chain_controller
|
|
54
|
+
return fresh_state(story, context) if chain_head?(frame)
|
|
55
|
+
return nil unless controller
|
|
56
|
+
|
|
57
|
+
controller.state_for(frame.parent_story)
|
|
58
|
+
end
|
|
59
|
+
private_class_method :initial_chain_state
|
|
60
|
+
|
|
61
|
+
# Records the post-render state for the chain so the next
|
|
62
|
+
# frame in the chain picks up where this one left off. Only
|
|
63
|
+
# meaningful when a chain_controller is wired.
|
|
64
|
+
def self.store_chain_state(frame, state, context)
|
|
65
|
+
controller = context.chain_controller
|
|
66
|
+
return unless controller
|
|
67
|
+
|
|
68
|
+
controller.store_state(frame.parent_story, state)
|
|
69
|
+
end
|
|
70
|
+
private_class_method :store_chain_state
|
|
71
|
+
|
|
72
|
+
def self.fresh_state(story, context)
|
|
73
|
+
paragraphs = StyleResolver.extract_paragraphs(
|
|
74
|
+
story, condition_filter: context.condition_filter
|
|
75
|
+
)
|
|
76
|
+
return nil if paragraphs.empty?
|
|
77
|
+
|
|
78
|
+
StoryChainController::State.new(
|
|
79
|
+
paragraphs: paragraphs,
|
|
80
|
+
current_paragraph: nil,
|
|
81
|
+
runs_remaining: [],
|
|
82
|
+
char_cursor: 0,
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
private_class_method :fresh_state
|
|
86
|
+
|
|
47
87
|
# Discovers Tables inlined in the story (Story > PSR > CSR >
|
|
48
88
|
# Table — the real IDML structure) and renders each within
|
|
49
89
|
# the frame's bounds. Real IDML Tables have no own geometry,
|
|
@@ -70,12 +110,12 @@ module Idml
|
|
|
70
110
|
end
|
|
71
111
|
private_class_method :tables_in_story
|
|
72
112
|
|
|
73
|
-
def self.render_text(canvas,
|
|
113
|
+
def self.render_text(canvas, state, context, box)
|
|
74
114
|
font = context.font_metrics
|
|
75
115
|
if font
|
|
76
|
-
engine_render(canvas,
|
|
116
|
+
engine_render(canvas, state, context, box, font)
|
|
77
117
|
else
|
|
78
|
-
simple_render(canvas,
|
|
118
|
+
simple_render(canvas, state, context, box)
|
|
79
119
|
end
|
|
80
120
|
end
|
|
81
121
|
private_class_method :render_text
|
|
@@ -109,56 +149,150 @@ module Idml
|
|
|
109
149
|
end
|
|
110
150
|
private_class_method :layout_frame
|
|
111
151
|
|
|
112
|
-
# Walks paragraphs and emits one
|
|
113
|
-
# the Shaper → LineBreaker →
|
|
114
|
-
# pipeline. Cursor y descends
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
|
|
152
|
+
# Walks the chain state's paragraphs/runs and emits one
|
|
153
|
+
# `canvas.text` per line via the Shaper → LineBreaker →
|
|
154
|
+
# Justifier → VerticalLayout pipeline. Cursor y descends
|
|
155
|
+
# monotonically across paragraphs. When a paragraph partially
|
|
156
|
+
# fits, its remaining runs go back into the state for the
|
|
157
|
+
# next frame in the chain.
|
|
158
|
+
def self.engine_render(canvas, state, context, box, font)
|
|
118
159
|
layout_frame = layout_frame(context.item, box)
|
|
119
160
|
cursor_y = box[:y] + box[:height] - (layout_frame.inset_top || 0)
|
|
120
161
|
bottom_limit = TextEngine::VerticalLayout.bottom_limit(layout_frame)
|
|
121
|
-
char_cursor =
|
|
122
|
-
|
|
123
|
-
paragraphs.each do |paragraph|
|
|
124
|
-
break if cursor_y < bottom_limit
|
|
162
|
+
char_cursor = state.char_cursor
|
|
125
163
|
|
|
126
|
-
|
|
127
|
-
|
|
164
|
+
if state.current_paragraph
|
|
165
|
+
cursor_y, char_cursor = resume_paragraph(
|
|
166
|
+
canvas, state, context, layout_frame, font,
|
|
128
167
|
cursor_y, bottom_limit, char_cursor
|
|
129
168
|
)
|
|
169
|
+
return if state.current_paragraph
|
|
130
170
|
end
|
|
171
|
+
|
|
172
|
+
consume_paragraphs(canvas, state, context, layout_frame, font,
|
|
173
|
+
cursor_y, bottom_limit, char_cursor)
|
|
131
174
|
end
|
|
132
175
|
private_class_method :engine_render
|
|
133
176
|
|
|
134
|
-
def self.
|
|
135
|
-
|
|
177
|
+
def self.resume_paragraph(canvas, state, context, layout_frame, font,
|
|
178
|
+
cursor_y, bottom_limit, char_cursor)
|
|
179
|
+
paragraph = state.current_paragraph
|
|
180
|
+
remaining_runs, cursor_y, char_cursor = render_runs_for_paragraph(
|
|
181
|
+
canvas, paragraph, state.runs_remaining, context,
|
|
182
|
+
layout_frame, font, cursor_y, bottom_limit, char_cursor,
|
|
183
|
+
first_paragraph: true
|
|
184
|
+
)
|
|
185
|
+
if remaining_runs.any?
|
|
186
|
+
state.runs_remaining = remaining_runs
|
|
187
|
+
else
|
|
188
|
+
state.current_paragraph = nil
|
|
189
|
+
state.runs_remaining = []
|
|
190
|
+
end
|
|
191
|
+
[cursor_y, char_cursor]
|
|
192
|
+
end
|
|
193
|
+
private_class_method :resume_paragraph
|
|
194
|
+
|
|
195
|
+
def self.consume_paragraphs(canvas, state, context, layout_frame, font,
|
|
196
|
+
cursor_y, bottom_limit, char_cursor)
|
|
197
|
+
pending = 0
|
|
198
|
+
state.paragraphs.each do |paragraph|
|
|
199
|
+
break if cursor_y < bottom_limit
|
|
200
|
+
|
|
201
|
+
remaining_runs, cursor_y, char_cursor = render_runs_for_paragraph(
|
|
202
|
+
canvas, paragraph, paragraph.runs, context,
|
|
203
|
+
layout_frame, font, cursor_y, bottom_limit, char_cursor,
|
|
204
|
+
first_paragraph: pending.zero?
|
|
205
|
+
)
|
|
206
|
+
if remaining_runs.any?
|
|
207
|
+
state.current_paragraph = paragraph
|
|
208
|
+
state.runs_remaining = remaining_runs
|
|
209
|
+
break
|
|
210
|
+
end
|
|
211
|
+
pending += 1
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
state.paragraphs = state.paragraphs[pending..]
|
|
215
|
+
state.char_cursor = char_cursor
|
|
216
|
+
end
|
|
217
|
+
private_class_method :consume_paragraphs
|
|
218
|
+
|
|
219
|
+
# Renders a paragraph's runs (a subset when resuming mid-para).
|
|
220
|
+
# Returns [remaining_runs, cursor_y, char_cursor] where
|
|
221
|
+
# remaining_runs is the runs that didn't fit (empty when all
|
|
222
|
+
# done). Emits RuleAbove before the first run and RuleBelow
|
|
223
|
+
# after the last successfully-rendered run.
|
|
224
|
+
def self.render_runs_for_paragraph(canvas, paragraph, runs, context,
|
|
225
|
+
layout_frame, font, cursor_y,
|
|
226
|
+
bottom_limit, char_cursor,
|
|
227
|
+
first_paragraph:)
|
|
136
228
|
wrap_width = TextEngine::VerticalLayout.wrap_width(
|
|
137
229
|
layout_frame, paragraph.right_indent || 0
|
|
138
230
|
)
|
|
139
231
|
para_attrs = paragraph_attrs(paragraph)
|
|
140
232
|
frame_left, frame_right = paragraph_frame_extents(layout_frame)
|
|
141
233
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
234
|
+
emit_paragraph_top_rule(canvas, paragraph, context, cursor_y,
|
|
235
|
+
frame_left, frame_right, first_paragraph)
|
|
236
|
+
|
|
237
|
+
rendered_count = 0
|
|
238
|
+
runs.each do |run|
|
|
239
|
+
break if cursor_y < bottom_limit
|
|
240
|
+
|
|
241
|
+
space_before = paragraph_space_before(para_attrs, first_paragraph,
|
|
242
|
+
rendered_count)
|
|
243
|
+
positioned, next_y = layout_run(
|
|
244
|
+
canvas, run, paragraph, context, layout_frame, font,
|
|
245
|
+
wrap_width, cursor_y, space_before,
|
|
246
|
+
para_attrs[:first_line_indent], para_attrs[:left_indent],
|
|
247
|
+
char_cursor
|
|
248
|
+
)
|
|
249
|
+
char_cursor += positioned.length
|
|
250
|
+
cursor_y = next_y
|
|
251
|
+
rendered_count += 1
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
remaining_runs = runs[rendered_count..]
|
|
255
|
+
cursor_y = emit_paragraph_bottom_rule(canvas, paragraph, context,
|
|
256
|
+
cursor_y, frame_left,
|
|
257
|
+
frame_right, para_attrs,
|
|
258
|
+
remaining_runs)
|
|
259
|
+
[remaining_runs, cursor_y, char_cursor]
|
|
260
|
+
end
|
|
261
|
+
private_class_method :render_runs_for_paragraph
|
|
262
|
+
|
|
263
|
+
def self.emit_paragraph_top_rule(canvas, paragraph, context, cursor_y,
|
|
264
|
+
frame_left, frame_right,
|
|
265
|
+
first_paragraph)
|
|
266
|
+
return unless first_paragraph
|
|
267
|
+
|
|
145
268
|
ParagraphRules.emit_rule_above(canvas, paragraph, context,
|
|
146
269
|
cursor_y, frame_left, frame_right)
|
|
270
|
+
end
|
|
271
|
+
private_class_method :emit_paragraph_top_rule
|
|
147
272
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
273
|
+
def self.paragraph_space_before(para_attrs, first_paragraph,
|
|
274
|
+
rendered_count)
|
|
275
|
+
return 0 unless first_paragraph && rendered_count.zero?
|
|
276
|
+
|
|
277
|
+
para_attrs[:space_before]
|
|
278
|
+
end
|
|
279
|
+
private_class_method :paragraph_space_before
|
|
153
280
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
281
|
+
# Emits RuleBelow after a paragraph's last run. Returns the
|
|
282
|
+
# updated cursor_y (subtracting space_after when the paragraph
|
|
283
|
+
# completed).
|
|
284
|
+
def self.emit_paragraph_bottom_rule(canvas, paragraph, context,
|
|
285
|
+
cursor_y, frame_left,
|
|
286
|
+
frame_right, para_attrs,
|
|
287
|
+
remaining_runs)
|
|
288
|
+
return cursor_y unless remaining_runs.empty?
|
|
289
|
+
|
|
290
|
+
after_y = cursor_y - para_attrs[:space_after]
|
|
157
291
|
ParagraphRules.emit_rule_below(canvas, paragraph, context,
|
|
158
292
|
after_y, frame_left, frame_right)
|
|
159
|
-
|
|
293
|
+
after_y
|
|
160
294
|
end
|
|
161
|
-
private_class_method :
|
|
295
|
+
private_class_method :emit_paragraph_bottom_rule
|
|
162
296
|
|
|
163
297
|
def self.paragraph_frame_extents(layout_frame)
|
|
164
298
|
inset_left = layout_frame.inset_left || 0
|
|
@@ -178,27 +312,6 @@ module Idml
|
|
|
178
312
|
end
|
|
179
313
|
private_class_method :paragraph_attrs
|
|
180
314
|
|
|
181
|
-
def self.render_runs(canvas, paragraph, context, layout_frame, font,
|
|
182
|
-
wrap_width, cursor_y, bottom_limit, char_cursor,
|
|
183
|
-
para_attrs)
|
|
184
|
-
space_before = para_attrs[:space_before]
|
|
185
|
-
paragraph.runs.each do |run|
|
|
186
|
-
break if cursor_y < bottom_limit
|
|
187
|
-
|
|
188
|
-
positioned, next_y = layout_run(
|
|
189
|
-
canvas, run, paragraph, context, layout_frame, font,
|
|
190
|
-
wrap_width, cursor_y, space_before,
|
|
191
|
-
para_attrs[:first_line_indent], para_attrs[:left_indent],
|
|
192
|
-
char_cursor
|
|
193
|
-
)
|
|
194
|
-
char_cursor += positioned.length
|
|
195
|
-
cursor_y = next_y
|
|
196
|
-
space_before = 0
|
|
197
|
-
end
|
|
198
|
-
[cursor_y, char_cursor]
|
|
199
|
-
end
|
|
200
|
-
private_class_method :render_runs
|
|
201
|
-
|
|
202
315
|
# Shapes one run's text, wraps to the inset/indent-adjusted
|
|
203
316
|
# width, justifies per paragraph alignment, and asks
|
|
204
317
|
# VerticalLayout to position the lines. Emits each line via
|
|
@@ -321,8 +434,11 @@ module Idml
|
|
|
321
434
|
|
|
322
435
|
# Fallback when no metrics are available: emit all runs as
|
|
323
436
|
# one `text_rich` block, letting pdfrb measure advance widths.
|
|
324
|
-
|
|
325
|
-
|
|
437
|
+
# Chain threading is best-effort here — the simple path
|
|
438
|
+
# doesn't wrap, so the first frame's render typically emits
|
|
439
|
+
# all content and clears the chain state.
|
|
440
|
+
def self.simple_render(canvas, state, context, box)
|
|
441
|
+
runs = collect_runs(state)
|
|
326
442
|
return if runs.empty?
|
|
327
443
|
|
|
328
444
|
runs_for = build_rich_runs(runs, context)
|
|
@@ -331,9 +447,22 @@ module Idml
|
|
|
331
447
|
runs_for,
|
|
332
448
|
at: [box[:x], box[:y] + box[:height] - first_size],
|
|
333
449
|
)
|
|
450
|
+
state.paragraphs = []
|
|
451
|
+
state.current_paragraph = nil
|
|
452
|
+
state.runs_remaining = []
|
|
334
453
|
end
|
|
335
454
|
private_class_method :simple_render
|
|
336
455
|
|
|
456
|
+
def self.collect_runs(state)
|
|
457
|
+
runs = state.runs_remaining || []
|
|
458
|
+
if state.current_paragraph
|
|
459
|
+
runs + state.current_paragraph.runs
|
|
460
|
+
else
|
|
461
|
+
runs + state.paragraphs.flat_map(&:runs)
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
private_class_method :collect_runs
|
|
465
|
+
|
|
337
466
|
def self.build_rich_runs(runs, context)
|
|
338
467
|
runs.map do |run|
|
|
339
468
|
{
|
|
@@ -7,7 +7,8 @@ module Idml
|
|
|
7
7
|
def initialize(font_metrics: nil, font_ps_name: Render::DEFAULT_FONT,
|
|
8
8
|
package: nil, layer_filter: LayerFilter::EXCLUDE_NONE,
|
|
9
9
|
font_ref_resolver: nil, structure: nil,
|
|
10
|
-
position_tracker: nil, font_map: {}
|
|
10
|
+
position_tracker: nil, font_map: {},
|
|
11
|
+
condition_filter: nil)
|
|
11
12
|
@font_metrics = font_metrics
|
|
12
13
|
@font_ps_name = font_ps_name
|
|
13
14
|
@package = package
|
|
@@ -16,6 +17,7 @@ module Idml
|
|
|
16
17
|
@structure = structure
|
|
17
18
|
@position_tracker = position_tracker
|
|
18
19
|
@font_map = font_map
|
|
20
|
+
@condition_filter = condition_filter
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def render(canvas, spread, page_width:, page_height:, image_refs: [],
|
|
@@ -33,6 +35,8 @@ module Idml
|
|
|
33
35
|
structure: @structure,
|
|
34
36
|
page_index: page_index,
|
|
35
37
|
position_tracker: @position_tracker,
|
|
38
|
+
chain_controller: StoryChainController.new,
|
|
39
|
+
condition_filter: @condition_filter,
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
canvas.save_graphics_state do
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Render
|
|
5
|
+
# Threads story-rendering state across linked TextFrames in a
|
|
6
|
+
# chain. A story overflows from frame 1 → frame 2 → frame 3 etc.
|
|
7
|
+
# via PreviousTextFrame/NextTextFrame links; without chain
|
|
8
|
+
# awareness, the renderer would silently truncate overflow text.
|
|
9
|
+
#
|
|
10
|
+
# State is per story_id (the ParentStory attribute). State is
|
|
11
|
+
# populated when a chain head renders and consumed when chain
|
|
12
|
+
# non-head frames render.
|
|
13
|
+
#
|
|
14
|
+
# Threading granularity: paragraphs and runs. If a run partially
|
|
15
|
+
# fits in a frame, the next frame picks up where the previous
|
|
16
|
+
# left off (the partially-rendered lines stay in the previous
|
|
17
|
+
# frame; remaining lines come from re-wrapping the run).
|
|
18
|
+
class StoryChainController
|
|
19
|
+
# `paragraphs` : Array<Paragraph> not yet started
|
|
20
|
+
# `current_paragraph` : Paragraph in progress (nil if none)
|
|
21
|
+
# `runs_remaining` : Array<StyledRun> in current_paragraph not yet rendered
|
|
22
|
+
# `char_cursor` : global character cursor (for hyperlink tracking)
|
|
23
|
+
State = Struct.new(
|
|
24
|
+
:paragraphs,
|
|
25
|
+
:current_paragraph,
|
|
26
|
+
:runs_remaining,
|
|
27
|
+
:char_cursor,
|
|
28
|
+
keyword_init: true,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def initialize
|
|
32
|
+
@states = {}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns the chain state for a story, or nil if the story has
|
|
36
|
+
# no leftover content. Chain heads (no predecessor) get no
|
|
37
|
+
# state — they render from scratch.
|
|
38
|
+
def state_for(story_id)
|
|
39
|
+
@states[story_id]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Records the chain state for a story after a frame's render.
|
|
43
|
+
# Removes the state entirely when no content remains.
|
|
44
|
+
def store_state(story_id, state)
|
|
45
|
+
if state.nil? || empty_state?(state)
|
|
46
|
+
@states.delete(story_id)
|
|
47
|
+
else
|
|
48
|
+
@states[story_id] = state
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# True when a story has pending content from a previous frame.
|
|
53
|
+
def has_pending?(story_id)
|
|
54
|
+
@states.key?(story_id)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def empty_state?(state)
|
|
60
|
+
state.paragraphs.empty? &&
|
|
61
|
+
state.current_paragraph.nil? &&
|
|
62
|
+
state.runs_remaining&.empty?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -104,11 +104,13 @@ module Idml
|
|
|
104
104
|
# its runs and carries the paragraph-level attributes from the
|
|
105
105
|
# owning PSR. Empty paragraphs (no runs after filtering) are
|
|
106
106
|
# skipped.
|
|
107
|
-
def self.extract_paragraphs(story)
|
|
107
|
+
def self.extract_paragraphs(story, condition_filter: nil)
|
|
108
108
|
return [] unless story&.inner
|
|
109
109
|
|
|
110
110
|
story.inner.paragraph_style_range.filter_map do |psr|
|
|
111
|
-
|
|
111
|
+
next unless condition_visible?(psr, condition_filter)
|
|
112
|
+
|
|
113
|
+
runs = csr_runs(psr, condition_filter: condition_filter)
|
|
112
114
|
next if runs.empty?
|
|
113
115
|
|
|
114
116
|
Paragraph.new(
|
|
@@ -140,8 +142,10 @@ module Idml
|
|
|
140
142
|
end
|
|
141
143
|
end
|
|
142
144
|
|
|
143
|
-
def self.csr_runs(psr)
|
|
145
|
+
def self.csr_runs(psr, condition_filter: nil)
|
|
144
146
|
psr.character_style_range.filter_map do |csr|
|
|
147
|
+
next unless condition_visible?(csr, condition_filter)
|
|
148
|
+
|
|
145
149
|
text = csr.text_content
|
|
146
150
|
next if text.nil? || text.empty?
|
|
147
151
|
|
|
@@ -170,6 +174,16 @@ module Idml
|
|
|
170
174
|
end
|
|
171
175
|
private_class_method :csr_runs
|
|
172
176
|
|
|
177
|
+
# Drops the PSR/CSR when any of its AppliedConditions
|
|
178
|
+
# references a hidden Condition. Returns true when no filter
|
|
179
|
+
# is supplied (caller doesn't care about conditions).
|
|
180
|
+
def self.condition_visible?(psr_or_csr, condition_filter)
|
|
181
|
+
return true unless condition_filter
|
|
182
|
+
|
|
183
|
+
condition_filter.visible?(psr_or_csr.applied_conditions)
|
|
184
|
+
end
|
|
185
|
+
private_class_method :condition_visible?
|
|
186
|
+
|
|
173
187
|
def self.alignment_for(csr)
|
|
174
188
|
ALIGNMENT_MAP[csr.justification] || :left
|
|
175
189
|
end
|
data/lib/idml/render.rb
CHANGED
|
@@ -15,7 +15,9 @@ module Idml
|
|
|
15
15
|
autoload :StrokeStyle, "#{__dir__}/render/stroke_style"
|
|
16
16
|
autoload :CharacterStyle, "#{__dir__}/render/character_style"
|
|
17
17
|
autoload :ParagraphRules, "#{__dir__}/render/paragraph_rules"
|
|
18
|
+
autoload :StoryChainController, "#{__dir__}/render/story_chain_controller"
|
|
18
19
|
autoload :LayerFilter, "#{__dir__}/render/layer_filter"
|
|
20
|
+
autoload :ConditionFilter, "#{__dir__}/render/condition_filter"
|
|
19
21
|
autoload :Placement, "#{__dir__}/render/placement"
|
|
20
22
|
autoload :ImageCollector, "#{__dir__}/render/image_collector"
|
|
21
23
|
autoload :StructureTracker, "#{__dir__}/render/structure_tracker"
|
data/lib/idml/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: idml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -276,6 +276,7 @@ files:
|
|
|
276
276
|
- lib/idml/elements/character_style_group.rb
|
|
277
277
|
- lib/idml/elements/character_style_range.rb
|
|
278
278
|
- lib/idml/elements/color.rb
|
|
279
|
+
- lib/idml/elements/condition.rb
|
|
279
280
|
- lib/idml/elements/content.rb
|
|
280
281
|
- lib/idml/elements/content_transparency_setting.rb
|
|
281
282
|
- lib/idml/elements/contour_option.rb
|
|
@@ -428,6 +429,7 @@ files:
|
|
|
428
429
|
- lib/idml/render/character_style.rb
|
|
429
430
|
- lib/idml/render/color_helper.rb
|
|
430
431
|
- lib/idml/render/color_resolver.rb
|
|
432
|
+
- lib/idml/render/condition_filter.rb
|
|
431
433
|
- lib/idml/render/font_reference_resolver.rb
|
|
432
434
|
- lib/idml/render/font_setup.rb
|
|
433
435
|
- lib/idml/render/gradient_resolver.rb
|
|
@@ -454,6 +456,7 @@ files:
|
|
|
454
456
|
- lib/idml/render/renderers/table_renderer.rb
|
|
455
457
|
- lib/idml/render/renderers/text_frame_renderer.rb
|
|
456
458
|
- lib/idml/render/spread_renderer.rb
|
|
459
|
+
- lib/idml/render/story_chain_controller.rb
|
|
457
460
|
- lib/idml/render/story_threader.rb
|
|
458
461
|
- lib/idml/render/stroke_style.rb
|
|
459
462
|
- lib/idml/render/structure_mapper.rb
|