Module: Idml::Render::Renderers::VerticalEmit
- Included in:
- TextFrameRenderer
- Defined in:
- lib/idml/render/renderers/vertical_emit.rb
Overview
Vertical writing path (StoryOrientation="Vertical", CJK): paragraphs flow as upright glyph columns advancing right-to-left; each run starts a fresh column (run-mixing within a column is not modeled). Runs whose columns would cross the frame's left inset overflow to the next frame in the chain. Paragraph spacing, rules, and decorations are not applied in vertical mode.
A class-methods module extended into TextFrameRenderer — it shares the host's constants, font resolution, and layout_frame helper (MECE: this module owns everything vertical; the host owns the horizontal engine).
Instance Method Summary collapse
- #emit_upright_glyph(canvas, positioned, run, context, size) ⇒ Object
-
#emit_vertical_ruby(canvas, run, positioned, frame, base_size, context) ⇒ Object
Emits the run's ruby alongside its vertical glyphs: stacked top-to-bottom beside the column — right side by default, left for Below* RubyPosition values (the horizontal above/below convention mirrored under rotation).
-
#emit_vertical_run(canvas, positioned, grouped, run, context, size) ⇒ Object
Emits a vertical run's glyphs: rotated/normal vertical handling for stacked columns, plain upright emission for tate-chu-yoko groups.
-
#emit_vertical_stack(canvas, positioned, run, context, size) ⇒ Object
Vertical-stack emission: CJK glyphs stand upright per glyph; consecutive non-CJK glyphs rotate as ONE group (the whole Latin word sideways, as InDesign does).
- #flush_vertical_segment(canvas, segment, run, context, size) ⇒ Object
- #keep_all_deferred?(paragraph, needed, room) ⇒ Boolean
- #keep_flagged?(paragraph) ⇒ Boolean
-
#resume_vertical_paragraph(state) ⇒ Object
Folds a chain-resumed paragraph (runs_remaining) back to the head of the paragraph list so the vertical path sees a uniform sequence.
- #ruby_vertical_x(positioned, run, base_size) ⇒ Object
-
#tatechuyoko_group(run, glyphs, layout_frame, leading, size, start_column) ⇒ Object
A tate-chu-yoko group for runs that declare it (and whose glyphs fit horizontally in a column slot); nil otherwise.
- #vertical_available_columns(layout_frame, leading, left_limit, next_column) ⇒ Object
- #vertical_fits?(layout_frame, leading, columns, left_limit) ⇒ Boolean
-
#vertical_keep_deferred?(paragraph, next_paragraph, layout_frame, _font, left_limit, next_column, pending, entry_offset) ⇒ Boolean
Keep options in vertical mode (TODO 153, column-count approximation — one column per run under-estimates runs that span several columns, so deferral errs toward placing): KeepAllLinesTogether defers the paragraph wholly to the next frame; KeepWithNext also defers when the follower (or its forced break) would strand.
-
#vertical_paragraph(canvas, paragraph, context, layout_frame, font, left_limit, start_column) ⇒ Object
Renders one paragraph's runs as vertical columns starting at
start_column. - #vertical_render(canvas, state, context, box, font) ⇒ Object
- #vertical_room(layout_frame, paragraph, left_limit, next_column) ⇒ Object
- #vertical_story?(story) ⇒ Boolean
-
#window_deferred?(paragraph, needed, room) ⇒ Boolean
Keep windows on the same column-count approximation: defer a splitting paragraph when fewer than KeepFirstLines columns fit, or when the stranded tail would hold fewer than KeepLastLines columns.
- #with_next_deferred?(paragraph, next_paragraph, layout_frame, left_limit, next_column, needed) ⇒ Boolean
Instance Method Details
#emit_upright_glyph(canvas, positioned, run, context, size) ⇒ Object
260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 260 def emit_upright_glyph(canvas, positioned, run, context, size) text = [positioned.codepoint].pack("U") text_kwargs = CharacterStyle.text_kwargs( run, { at: [positioned.x, positioned.y], font: font_for_run(run, context), size: size, }, ) canvas.text(text, **text_kwargs) end |
#emit_vertical_ruby(canvas, run, positioned, frame, base_size, context) ⇒ Object
Emits the run's ruby alongside its vertical glyphs: stacked top-to-bottom beside the column — right side by default, left for Below* RubyPosition values (the horizontal above/below convention mirrored under rotation).
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 285 def emit_vertical_ruby(canvas, run, positioned, frame, base_size, context) ruby_text = run.ruby_string return if ruby_text.nil? || ruby_text.empty? ruby_size = run.ruby_font_size || (base_size * TextFrameRenderer::RUBY_SIZE_FACTOR) x = ruby_vertical_x(positioned.first, run, base_size) y = frame.y + frame.height - (frame.inset_top || 0) ruby_text.each_codepoint do |codepoint| text_kwargs = CharacterStyle.text_kwargs( run, { at: [x, y - ruby_size], font: font_for_run(run, context), size: ruby_size, }, ) canvas.text([codepoint].pack("U"), **text_kwargs) y -= ruby_size end end |
#emit_vertical_run(canvas, positioned, grouped, run, context, size) ⇒ Object
Emits a vertical run's glyphs: rotated/normal vertical handling for stacked columns, plain upright emission for tate-chu-yoko groups.
202 203 204 205 206 207 208 209 210 211 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 202 def emit_vertical_run(canvas, positioned, grouped, run, context, size) if grouped positioned.each do |glyph| emit_upright_glyph(canvas, glyph, run, context, size) end else emit_vertical_stack(canvas, positioned, run, context, size) end end |
#emit_vertical_stack(canvas, positioned, run, context, size) ⇒ Object
Vertical-stack emission: CJK glyphs stand upright per glyph; consecutive non-CJK glyphs rotate as ONE group (the whole Latin word sideways, as InDesign does). The group's string renders as a single text op inside one rotated graphics state — the font's advances land each glyph exactly where per-glyph rotation placed it. A group never spans columns (x reset) and shares the run's styling, so one text op is lossless.
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 221 def emit_vertical_stack(canvas, positioned, run, context, size) segment = [] positioned.each do |glyph| if TextEngine::CjkLayout.cjk?(glyph.codepoint) flush_vertical_segment(canvas, segment, run, context, size) emit_upright_glyph(canvas, glyph, run, context, size) else if !segment.empty? && segment.last.x != glyph.x flush_vertical_segment(canvas, segment, run, context, size) end segment << glyph end end flush_vertical_segment(canvas, segment, run, context, size) end |
#flush_vertical_segment(canvas, segment, run, context, size) ⇒ Object
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 238 def flush_vertical_segment(canvas, segment, run, context, size) return if segment.empty? text = segment.map { |g| [g.codepoint].pack("U") }.join first = segment.first canvas.save_graphics_state do # 90° clockwise: exact matrix (rotate would emit # cos/sin float noise like 6.1e-17). canvas.concat(0, -1, 1, 0, first.x, first.y) text_kwargs = CharacterStyle.text_kwargs( run, { at: [0, 0], font: font_for_run(run, context), size: size, }, ) canvas.text(text, **text_kwargs) end end |
#keep_all_deferred?(paragraph, needed, room) ⇒ Boolean
90 91 92 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 90 def keep_all_deferred?(paragraph, needed, room) paragraph.keep_all_lines_together && needed > room end |
#keep_flagged?(paragraph) ⇒ Boolean
83 84 85 86 87 88 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 83 def keep_flagged?(paragraph) paragraph.keep_all_lines_together || !paragraph.keep_with_next.nil? || paragraph.keep_first_lines || paragraph.keep_last_lines end |
#resume_vertical_paragraph(state) ⇒ Object
Folds a chain-resumed paragraph (runs_remaining) back to the head of the paragraph list so the vertical path sees a uniform sequence. The consumed column offset persists in the state so the next frame continues right where this frame's columns ended.
144 145 146 147 148 149 150 151 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 144 def resume_vertical_paragraph(state) return unless state.current_paragraph state.current_paragraph.runs = state.runs_remaining state.paragraphs.unshift(state.current_paragraph) state.current_paragraph = nil state.runs_remaining = [] end |
#ruby_vertical_x(positioned, run, base_size) ⇒ Object
308 309 310 311 312 313 314 315 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 308 def ruby_vertical_x(positioned, run, base_size) offset = base_size * 0.9 if run.ruby_position.to_s.include?("Below") positioned.x - offset else positioned.x + offset end end |
#tatechuyoko_group(run, glyphs, layout_frame, leading, size, start_column) ⇒ Object
A tate-chu-yoko group for runs that declare it (and whose glyphs fit horizontally in a column slot); nil otherwise.
189 190 191 192 193 194 195 196 197 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 189 def tatechuyoko_group(run, glyphs, layout_frame, leading, size, start_column) return nil unless run.tatechuyoko TextEngine::VerticalTextLayout.tatechuyoko_group( glyphs: glyphs, frame: layout_frame, leading: leading, size: size, start_column: start_column ) end |
#vertical_available_columns(layout_frame, leading, left_limit, next_column) ⇒ Object
132 133 134 135 136 137 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 132 def vertical_available_columns(layout_frame, leading, left_limit, next_column) right = layout_frame.x + layout_frame.width - (layout_frame.inset_right || 0) (((right - left_limit) / leading).floor - next_column).to_i end |
#vertical_fits?(layout_frame, leading, columns, left_limit) ⇒ Boolean
274 275 276 277 278 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 274 def vertical_fits?(layout_frame, leading, columns, left_limit) right = layout_frame.x + layout_frame.width - (layout_frame.inset_right || 0) (right - (columns * leading)) >= left_limit end |
#vertical_keep_deferred?(paragraph, next_paragraph, layout_frame, _font, left_limit, next_column, pending, entry_offset) ⇒ Boolean
Keep options in vertical mode (TODO 153, column-count approximation — one column per run under-estimates runs that span several columns, so deferral errs toward placing): KeepAllLinesTogether defers the paragraph wholly to the next frame; KeepWithNext also defers when the follower (or its forced break) would strand. Never defers the frame's first paragraph — the progress guarantee, shared with KeepPolicy.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 66 def vertical_keep_deferred?(paragraph, next_paragraph, layout_frame, _font, left_limit, next_column, pending, entry_offset) placed = pending.positive? || entry_offset.positive? return false unless placed return false unless keep_flagged?(paragraph) needed = [paragraph.runs.length, 1].max room = vertical_room(layout_frame, paragraph, left_limit, next_column) return true if keep_all_deferred?(paragraph, needed, room) return true if window_deferred?(paragraph, needed, room) with_next_deferred?(paragraph, next_paragraph, layout_frame, left_limit, next_column, needed) end |
#vertical_paragraph(canvas, paragraph, context, layout_frame, font, left_limit, start_column) ⇒ Object
Renders one paragraph's runs as vertical columns starting
at start_column. Returns [runs that did not fit (empty
when all placed), the next unused column index].
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 156 def vertical_paragraph(canvas, paragraph, context, layout_frame, font, left_limit, start_column) next_column = start_column paragraph.runs.each_with_index do |run, index| size = run.point_size || TextFrameRenderer::DEFAULT_SIZE leading = TextEngine::VerticalLayout.leading_for( paragraph.auto_leading, size ) glyphs = TextEngine::Shaper.shape(text: run.text, font: font, size: size) grouped = tatechuyoko_group(run, glyphs, layout_frame, leading, size, next_column) positioned, next_column = grouped || TextEngine::VerticalTextLayout.layout( glyphs: glyphs, frame: layout_frame, leading: leading, size: size, start_column: next_column ) unless vertical_fits?(layout_frame, leading, next_column, left_limit) return [paragraph.runs[index..], start_column] end emit_vertical_run(canvas, positioned, grouped, run, context, size) emit_vertical_ruby(canvas, run, positioned, layout_frame, size, context) end [[], next_column] end |
#vertical_render(canvas, state, context, box, font) ⇒ Object
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 23 def vertical_render(canvas, state, context, box, font) layout_frame = layout_frame(context.item, box) left_limit = layout_frame.x + (layout_frame.inset_left || 0) resume_vertical_paragraph(state) entry_offset = state.column_offset || 0 next_column = entry_offset pending = 0 state.paragraphs.each_with_index do |paragraph, index| if vertical_keep_deferred?(paragraph, state.paragraphs[index + 1], layout_frame, font, left_limit, next_column, pending, entry_offset) state.current_paragraph = nil state.runs_remaining = [] state.column_offset = 0 break end remaining_runs, next_column = vertical_paragraph( canvas, paragraph, context, layout_frame, font, left_limit, next_column ) if remaining_runs.empty? pending += 1 else state.current_paragraph = paragraph state.runs_remaining = remaining_runs state.column_offset = next_column break end end state.paragraphs = state.paragraphs[pending..] end |
#vertical_room(layout_frame, paragraph, left_limit, next_column) ⇒ Object
121 122 123 124 125 126 127 128 129 130 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 121 def vertical_room(layout_frame, paragraph, left_limit, next_column) leading = TextEngine::VerticalLayout.leading_for( paragraph.auto_leading, paragraph.runs.first&.point_size || TextFrameRenderer::DEFAULT_SIZE, ) vertical_available_columns(layout_frame, leading, left_limit, next_column) end |
#vertical_story?(story) ⇒ Boolean
19 20 21 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 19 def vertical_story?(story) story.inner&.story_preference&.story_orientation == "Vertical" end |
#window_deferred?(paragraph, needed, room) ⇒ Boolean
Keep windows on the same column-count approximation: defer a splitting paragraph when fewer than KeepFirstLines columns fit, or when the stranded tail would hold fewer than KeepLastLines columns.
98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 98 def window_deferred?(paragraph, needed, room) return false unless needed > room if paragraph.keep_first_lines && room < paragraph.keep_first_lines return true end paragraph.keep_last_lines && (needed - room) < paragraph.keep_last_lines end |
#with_next_deferred?(paragraph, next_paragraph, layout_frame, left_limit, next_column, needed) ⇒ Boolean
110 111 112 113 114 115 116 117 118 119 |
# File 'lib/idml/render/renderers/vertical_emit.rb', line 110 def with_next_deferred?(paragraph, next_paragraph, layout_frame, left_limit, next_column, needed) return false if paragraph.keep_with_next.nil? follower_needed = [next_paragraph&.runs&.length || 1, 1].max follower_break = KeepPolicy.paragraph_break?(next_paragraph) room = vertical_room(layout_frame, paragraph, left_limit, next_column) follower_break || needed + follower_needed > room end |