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.
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Idml
4
- VERSION = "0.10.9"
4
+ VERSION = "0.10.11"
5
5
  end
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.9
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-28 00:00:00.000000000 Z
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