idml 0.10.9 → 0.10.11
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/148-architecture-improvements.md +45 -0
- data/TODO.pdf/149-architecture-round-two.md +53 -0
- 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/simple_text.rb +82 -0
- data/lib/idml/render/renderers/text_frame_renderer.rb +17 -577
- data/lib/idml/render/renderers/vertical_emit.rb +228 -0
- data/lib/idml/render.rb +16 -0
- data/lib/idml/version.rb +1 -1
- metadata +8 -2
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Render
|
|
5
|
+
module Renderers
|
|
6
|
+
# Vertical writing path (StoryOrientation="Vertical", CJK):
|
|
7
|
+
# paragraphs flow as upright glyph columns advancing
|
|
8
|
+
# right-to-left; each run starts a fresh column (run-mixing
|
|
9
|
+
# within a column is not modeled). Runs whose columns would
|
|
10
|
+
# cross the frame's left inset overflow to the next frame in
|
|
11
|
+
# the chain. Paragraph spacing, rules, and decorations are
|
|
12
|
+
# not applied in vertical mode.
|
|
13
|
+
#
|
|
14
|
+
# A class-methods module extended into TextFrameRenderer —
|
|
15
|
+
# it shares the host's constants, font resolution, and
|
|
16
|
+
# layout_frame helper (MECE: this module owns everything
|
|
17
|
+
# vertical; the host owns the horizontal engine).
|
|
18
|
+
module VerticalEmit
|
|
19
|
+
def vertical_story?(story)
|
|
20
|
+
story.inner&.story_preference&.story_orientation == "Vertical"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def vertical_render(canvas, state, context, box, font)
|
|
24
|
+
layout_frame = layout_frame(context.item, box)
|
|
25
|
+
left_limit = layout_frame.x + (layout_frame.inset_left || 0)
|
|
26
|
+
resume_vertical_paragraph(state)
|
|
27
|
+
next_column = state.column_offset || 0
|
|
28
|
+
pending = 0
|
|
29
|
+
|
|
30
|
+
state.paragraphs.each do |paragraph|
|
|
31
|
+
remaining_runs, next_column = vertical_paragraph(
|
|
32
|
+
canvas, paragraph, context, layout_frame, font,
|
|
33
|
+
left_limit, next_column
|
|
34
|
+
)
|
|
35
|
+
if remaining_runs.empty?
|
|
36
|
+
pending += 1
|
|
37
|
+
else
|
|
38
|
+
state.current_paragraph = paragraph
|
|
39
|
+
state.runs_remaining = remaining_runs
|
|
40
|
+
state.column_offset = next_column
|
|
41
|
+
break
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
state.paragraphs = state.paragraphs[pending..]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Folds a chain-resumed paragraph (runs_remaining) back to
|
|
49
|
+
# the head of the paragraph list so the vertical path sees a
|
|
50
|
+
# uniform sequence. The consumed column offset persists in
|
|
51
|
+
# the state so the next frame continues right where this
|
|
52
|
+
# frame's columns ended.
|
|
53
|
+
def resume_vertical_paragraph(state)
|
|
54
|
+
return unless state.current_paragraph
|
|
55
|
+
|
|
56
|
+
state.current_paragraph.runs = state.runs_remaining
|
|
57
|
+
state.paragraphs.unshift(state.current_paragraph)
|
|
58
|
+
state.current_paragraph = nil
|
|
59
|
+
state.runs_remaining = []
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Renders one paragraph's runs as vertical columns starting
|
|
63
|
+
# at `start_column`. Returns [runs that did not fit (empty
|
|
64
|
+
# when all placed), the next unused column index].
|
|
65
|
+
def vertical_paragraph(canvas, paragraph, context,
|
|
66
|
+
layout_frame, font, left_limit,
|
|
67
|
+
start_column)
|
|
68
|
+
next_column = start_column
|
|
69
|
+
paragraph.runs.each_with_index do |run, index|
|
|
70
|
+
size = run.point_size || TextFrameRenderer::DEFAULT_SIZE
|
|
71
|
+
leading = TextEngine::VerticalLayout.leading_for(
|
|
72
|
+
paragraph.auto_leading, size
|
|
73
|
+
)
|
|
74
|
+
glyphs = TextEngine::Shaper.shape(text: run.text, font: font,
|
|
75
|
+
size: size)
|
|
76
|
+
grouped = tatechuyoko_group(run, glyphs, layout_frame,
|
|
77
|
+
leading, size, next_column)
|
|
78
|
+
positioned, next_column = grouped ||
|
|
79
|
+
TextEngine::VerticalTextLayout.layout(
|
|
80
|
+
glyphs: glyphs, frame: layout_frame, leading: leading,
|
|
81
|
+
size: size, start_column: next_column
|
|
82
|
+
)
|
|
83
|
+
unless vertical_fits?(layout_frame, leading, next_column,
|
|
84
|
+
left_limit)
|
|
85
|
+
return [paragraph.runs[index..], start_column]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
emit_vertical_run(canvas, positioned, grouped, run, context,
|
|
89
|
+
size)
|
|
90
|
+
emit_vertical_ruby(canvas, run, positioned, layout_frame,
|
|
91
|
+
size, context)
|
|
92
|
+
end
|
|
93
|
+
[[], next_column]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A tate-chu-yoko group for runs that declare it (and whose
|
|
97
|
+
# glyphs fit horizontally in a column slot); nil otherwise.
|
|
98
|
+
def tatechuyoko_group(run, glyphs, layout_frame, leading,
|
|
99
|
+
size, start_column)
|
|
100
|
+
return nil unless run.tatechuyoko
|
|
101
|
+
|
|
102
|
+
TextEngine::VerticalTextLayout.tatechuyoko_group(
|
|
103
|
+
glyphs: glyphs, frame: layout_frame, leading: leading,
|
|
104
|
+
size: size, start_column: start_column
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Emits a vertical run's glyphs: rotated/normal vertical
|
|
109
|
+
# handling for stacked columns, plain upright emission for
|
|
110
|
+
# tate-chu-yoko groups.
|
|
111
|
+
def emit_vertical_run(canvas, positioned, grouped, run,
|
|
112
|
+
context, size)
|
|
113
|
+
if grouped
|
|
114
|
+
positioned.each do |glyph|
|
|
115
|
+
emit_upright_glyph(canvas, glyph, run, context, size)
|
|
116
|
+
end
|
|
117
|
+
else
|
|
118
|
+
emit_vertical_stack(canvas, positioned, run, context, size)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Vertical-stack emission: CJK glyphs stand upright per
|
|
123
|
+
# glyph; consecutive non-CJK glyphs rotate as ONE group
|
|
124
|
+
# (the whole Latin word sideways, as InDesign does). The
|
|
125
|
+
# group's string renders as a single text op inside one
|
|
126
|
+
# rotated graphics state — the font's advances land each
|
|
127
|
+
# glyph exactly where per-glyph rotation placed it. A group
|
|
128
|
+
# never spans columns (x reset) and shares the run's
|
|
129
|
+
# styling, so one text op is lossless.
|
|
130
|
+
def emit_vertical_stack(canvas, positioned, run, context,
|
|
131
|
+
size)
|
|
132
|
+
segment = []
|
|
133
|
+
positioned.each do |glyph|
|
|
134
|
+
if TextEngine::CjkLayout.cjk?(glyph.codepoint)
|
|
135
|
+
flush_vertical_segment(canvas, segment, run, context, size)
|
|
136
|
+
emit_upright_glyph(canvas, glyph, run, context, size)
|
|
137
|
+
else
|
|
138
|
+
if !segment.empty? && segment.last.x != glyph.x
|
|
139
|
+
flush_vertical_segment(canvas, segment, run, context, size)
|
|
140
|
+
end
|
|
141
|
+
segment << glyph
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
flush_vertical_segment(canvas, segment, run, context, size)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def flush_vertical_segment(canvas, segment, run, context,
|
|
148
|
+
size)
|
|
149
|
+
return if segment.empty?
|
|
150
|
+
|
|
151
|
+
text = segment.map { |g| [g.codepoint].pack("U") }.join
|
|
152
|
+
first = segment.first
|
|
153
|
+
canvas.save_graphics_state do
|
|
154
|
+
# 90° clockwise: exact matrix (rotate would emit
|
|
155
|
+
# cos/sin float noise like 6.1e-17).
|
|
156
|
+
canvas.concat(0, -1, 1, 0, first.x, first.y)
|
|
157
|
+
text_kwargs = CharacterStyle.text_kwargs(
|
|
158
|
+
run,
|
|
159
|
+
{
|
|
160
|
+
at: [0, 0],
|
|
161
|
+
font: font_for_run(run, context),
|
|
162
|
+
size: size,
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
canvas.text(text, **text_kwargs)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def emit_upright_glyph(canvas, positioned, run, context,
|
|
170
|
+
size)
|
|
171
|
+
text = [positioned.codepoint].pack("U")
|
|
172
|
+
text_kwargs = CharacterStyle.text_kwargs(
|
|
173
|
+
run,
|
|
174
|
+
{
|
|
175
|
+
at: [positioned.x, positioned.y],
|
|
176
|
+
font: font_for_run(run, context),
|
|
177
|
+
size: size,
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
canvas.text(text, **text_kwargs)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def vertical_fits?(layout_frame, leading, columns, left_limit)
|
|
184
|
+
right = layout_frame.x + layout_frame.width -
|
|
185
|
+
(layout_frame.inset_right || 0)
|
|
186
|
+
(right - (columns * leading)) >= left_limit
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Emits the run's ruby alongside its vertical glyphs:
|
|
190
|
+
# stacked top-to-bottom beside the column — right side by
|
|
191
|
+
# default, left for Below* RubyPosition values (the
|
|
192
|
+
# horizontal above/below convention mirrored under
|
|
193
|
+
# rotation).
|
|
194
|
+
def emit_vertical_ruby(canvas, run, positioned, frame,
|
|
195
|
+
base_size, context)
|
|
196
|
+
ruby_text = run.ruby_string
|
|
197
|
+
return if ruby_text.nil? || ruby_text.empty?
|
|
198
|
+
|
|
199
|
+
ruby_size = run.ruby_font_size ||
|
|
200
|
+
(base_size * TextFrameRenderer::RUBY_SIZE_FACTOR)
|
|
201
|
+
x = ruby_vertical_x(positioned.first, run, base_size)
|
|
202
|
+
y = frame.y + frame.height - (frame.inset_top || 0)
|
|
203
|
+
ruby_text.each_codepoint do |codepoint|
|
|
204
|
+
text_kwargs = CharacterStyle.text_kwargs(
|
|
205
|
+
run,
|
|
206
|
+
{
|
|
207
|
+
at: [x, y - ruby_size],
|
|
208
|
+
font: font_for_run(run, context),
|
|
209
|
+
size: ruby_size,
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
canvas.text([codepoint].pack("U"), **text_kwargs)
|
|
213
|
+
y -= ruby_size
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def ruby_vertical_x(positioned, run, base_size)
|
|
218
|
+
offset = base_size * 0.9
|
|
219
|
+
if run.ruby_position.to_s.include?("Below")
|
|
220
|
+
positioned.x - offset
|
|
221
|
+
else
|
|
222
|
+
positioned.x + offset
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
data/lib/idml/render.rb
CHANGED
|
@@ -92,3 +92,19 @@ Idml::Render::Renderers.autoload(
|
|
|
92
92
|
:TableRenderer,
|
|
93
93
|
"#{__dir__}/render/renderers/table_renderer",
|
|
94
94
|
)
|
|
95
|
+
Idml::Render::Renderers.autoload(
|
|
96
|
+
:VerticalEmit,
|
|
97
|
+
"#{__dir__}/render/renderers/vertical_emit",
|
|
98
|
+
)
|
|
99
|
+
Idml::Render::Renderers.autoload(
|
|
100
|
+
:SimpleText,
|
|
101
|
+
"#{__dir__}/render/renderers/simple_text",
|
|
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.11
|
|
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-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|
|
@@ -211,6 +211,8 @@ files:
|
|
|
211
211
|
- TODO.pdf/145-apply-mojikumi-aki.md
|
|
212
212
|
- TODO.pdf/146-next-column-wrap.md
|
|
213
213
|
- TODO.pdf/147-side-aware-shapes.md
|
|
214
|
+
- TODO.pdf/148-architecture-improvements.md
|
|
215
|
+
- TODO.pdf/149-architecture-round-two.md
|
|
214
216
|
- TODO.pdf/15-wire-spread-children.md
|
|
215
217
|
- TODO.pdf/16-typed-model-pipeline.md
|
|
216
218
|
- TODO.pdf/17-shape-rendering.md
|
|
@@ -501,14 +503,18 @@ files:
|
|
|
501
503
|
- lib/idml/render/position_tracker.rb
|
|
502
504
|
- lib/idml/render/render_context.rb
|
|
503
505
|
- lib/idml/render/renderers.rb
|
|
506
|
+
- lib/idml/render/renderers/frame_metrics.rb
|
|
504
507
|
- lib/idml/render/renderers/graphic_line_renderer.rb
|
|
505
508
|
- lib/idml/render/renderers/group_renderer.rb
|
|
509
|
+
- lib/idml/render/renderers/keep_policy.rb
|
|
506
510
|
- lib/idml/render/renderers/oval_renderer.rb
|
|
507
511
|
- lib/idml/render/renderers/path_renderer.rb
|
|
508
512
|
- lib/idml/render/renderers/polygon_renderer.rb
|
|
509
513
|
- lib/idml/render/renderers/rectangle_renderer.rb
|
|
514
|
+
- lib/idml/render/renderers/simple_text.rb
|
|
510
515
|
- lib/idml/render/renderers/table_renderer.rb
|
|
511
516
|
- lib/idml/render/renderers/text_frame_renderer.rb
|
|
517
|
+
- lib/idml/render/renderers/vertical_emit.rb
|
|
512
518
|
- lib/idml/render/shape_paint.rb
|
|
513
519
|
- lib/idml/render/spread_renderer.rb
|
|
514
520
|
- lib/idml/render/story_chain_controller.rb
|