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
|
@@ -27,6 +27,13 @@ module Idml
|
|
|
27
27
|
DEFAULT_SIZE = 12.0
|
|
28
28
|
RUBY_SIZE_FACTOR = 0.5
|
|
29
29
|
|
|
30
|
+
# Vertical-writing wing (MECE split; shares this class's
|
|
31
|
+
# constants, layout_frame, and font resolution).
|
|
32
|
+
extend VerticalEmit
|
|
33
|
+
|
|
34
|
+
# No-metrics fallback path (MECE split; rough output).
|
|
35
|
+
extend SimpleText
|
|
36
|
+
|
|
30
37
|
def self.render(canvas, context)
|
|
31
38
|
frame = context.item
|
|
32
39
|
return unless frame.parent_story
|
|
@@ -221,231 +228,6 @@ module Idml
|
|
|
221
228
|
|
|
222
229
|
# True when the story's StoryPreference declares vertical
|
|
223
230
|
# writing (StoryOrientation="Vertical").
|
|
224
|
-
def self.vertical_story?(story)
|
|
225
|
-
story.inner&.story_preference&.story_orientation == "Vertical"
|
|
226
|
-
end
|
|
227
|
-
private_class_method :vertical_story?
|
|
228
|
-
|
|
229
|
-
# Vertical writing path (StoryOrientation="Vertical", CJK):
|
|
230
|
-
# paragraphs flow as upright glyph columns advancing
|
|
231
|
-
# right-to-left; each run starts a fresh column (run-mixing
|
|
232
|
-
# within a column is not modeled). Runs whose columns would
|
|
233
|
-
# cross the frame's left inset overflow to the next frame in
|
|
234
|
-
# the chain. Paragraph spacing, rules, and decorations are
|
|
235
|
-
# not applied in vertical mode.
|
|
236
|
-
def self.vertical_render(canvas, state, context, box, font)
|
|
237
|
-
layout_frame = layout_frame(context.item, box)
|
|
238
|
-
left_limit = layout_frame.x + (layout_frame.inset_left || 0)
|
|
239
|
-
resume_vertical_paragraph(state)
|
|
240
|
-
next_column = state.column_offset || 0
|
|
241
|
-
pending = 0
|
|
242
|
-
|
|
243
|
-
state.paragraphs.each do |paragraph|
|
|
244
|
-
remaining_runs, next_column = vertical_paragraph(
|
|
245
|
-
canvas, paragraph, context, layout_frame, font,
|
|
246
|
-
left_limit, next_column
|
|
247
|
-
)
|
|
248
|
-
if remaining_runs.empty?
|
|
249
|
-
pending += 1
|
|
250
|
-
else
|
|
251
|
-
state.current_paragraph = paragraph
|
|
252
|
-
state.runs_remaining = remaining_runs
|
|
253
|
-
state.column_offset = next_column
|
|
254
|
-
break
|
|
255
|
-
end
|
|
256
|
-
end
|
|
257
|
-
|
|
258
|
-
state.paragraphs = state.paragraphs[pending..]
|
|
259
|
-
end
|
|
260
|
-
private_class_method :vertical_render
|
|
261
|
-
|
|
262
|
-
# Folds a chain-resumed paragraph (runs_remaining) back to
|
|
263
|
-
# the head of the paragraph list so the vertical path sees a
|
|
264
|
-
# uniform sequence. The consumed column offset persists in
|
|
265
|
-
# the state so the next frame continues right where this
|
|
266
|
-
# frame's columns ended.
|
|
267
|
-
def self.resume_vertical_paragraph(state)
|
|
268
|
-
return unless state.current_paragraph
|
|
269
|
-
|
|
270
|
-
state.current_paragraph.runs = state.runs_remaining
|
|
271
|
-
state.paragraphs.unshift(state.current_paragraph)
|
|
272
|
-
state.current_paragraph = nil
|
|
273
|
-
state.runs_remaining = []
|
|
274
|
-
end
|
|
275
|
-
private_class_method :resume_vertical_paragraph
|
|
276
|
-
|
|
277
|
-
# Renders one paragraph's runs as vertical columns starting
|
|
278
|
-
# at `start_column`. Returns [runs that did not fit (empty
|
|
279
|
-
# when all placed), the next unused column index].
|
|
280
|
-
def self.vertical_paragraph(canvas, paragraph, context,
|
|
281
|
-
layout_frame, font, left_limit,
|
|
282
|
-
start_column)
|
|
283
|
-
next_column = start_column
|
|
284
|
-
paragraph.runs.each_with_index do |run, index|
|
|
285
|
-
size = run.point_size || DEFAULT_SIZE
|
|
286
|
-
leading = TextEngine::VerticalLayout.leading_for(
|
|
287
|
-
paragraph.auto_leading, size
|
|
288
|
-
)
|
|
289
|
-
glyphs = TextEngine::Shaper.shape(text: run.text, font: font,
|
|
290
|
-
size: size)
|
|
291
|
-
grouped = tatechuyoko_group(run, glyphs, layout_frame,
|
|
292
|
-
leading, size, next_column)
|
|
293
|
-
positioned, next_column = grouped ||
|
|
294
|
-
TextEngine::VerticalTextLayout.layout(
|
|
295
|
-
glyphs: glyphs, frame: layout_frame, leading: leading,
|
|
296
|
-
size: size, start_column: next_column
|
|
297
|
-
)
|
|
298
|
-
unless vertical_fits?(layout_frame, leading, next_column,
|
|
299
|
-
left_limit)
|
|
300
|
-
return [paragraph.runs[index..], start_column]
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
emit_vertical_run(canvas, positioned, grouped, run, context,
|
|
304
|
-
size)
|
|
305
|
-
emit_vertical_ruby(canvas, run, positioned, layout_frame,
|
|
306
|
-
size, context)
|
|
307
|
-
end
|
|
308
|
-
[[], next_column]
|
|
309
|
-
end
|
|
310
|
-
private_class_method :vertical_paragraph
|
|
311
|
-
|
|
312
|
-
# A tate-chu-yoko group for runs that declare it (and whose
|
|
313
|
-
# glyphs fit horizontally in a column slot); nil otherwise.
|
|
314
|
-
def self.tatechuyoko_group(run, glyphs, layout_frame, leading,
|
|
315
|
-
size, start_column)
|
|
316
|
-
return nil unless run.tatechuyoko
|
|
317
|
-
|
|
318
|
-
TextEngine::VerticalTextLayout.tatechuyoko_group(
|
|
319
|
-
glyphs: glyphs, frame: layout_frame, leading: leading,
|
|
320
|
-
size: size, start_column: start_column
|
|
321
|
-
)
|
|
322
|
-
end
|
|
323
|
-
private_class_method :tatechuyoko_group
|
|
324
|
-
|
|
325
|
-
# Emits a vertical run's glyphs: rotated/normal vertical
|
|
326
|
-
# handling for stacked columns, plain upright emission for
|
|
327
|
-
# tate-chu-yoko groups.
|
|
328
|
-
def self.emit_vertical_run(canvas, positioned, grouped, run,
|
|
329
|
-
context, size)
|
|
330
|
-
if grouped
|
|
331
|
-
positioned.each do |glyph|
|
|
332
|
-
emit_upright_glyph(canvas, glyph, run, context, size)
|
|
333
|
-
end
|
|
334
|
-
else
|
|
335
|
-
emit_vertical_stack(canvas, positioned, run, context, size)
|
|
336
|
-
end
|
|
337
|
-
end
|
|
338
|
-
private_class_method :emit_vertical_run
|
|
339
|
-
|
|
340
|
-
# Vertical-stack emission: CJK glyphs stand upright per
|
|
341
|
-
# glyph; consecutive non-CJK glyphs rotate as ONE group
|
|
342
|
-
# (the whole Latin word sideways, as InDesign does). The
|
|
343
|
-
# group's string renders as a single text op inside one
|
|
344
|
-
# rotated graphics state — the font's advances land each
|
|
345
|
-
# glyph exactly where per-glyph rotation placed it. A group
|
|
346
|
-
# never spans columns (x reset) and shares the run's
|
|
347
|
-
# styling, so one text op is lossless.
|
|
348
|
-
def self.emit_vertical_stack(canvas, positioned, run, context,
|
|
349
|
-
size)
|
|
350
|
-
segment = []
|
|
351
|
-
positioned.each do |glyph|
|
|
352
|
-
if TextEngine::CjkLayout.cjk?(glyph.codepoint)
|
|
353
|
-
flush_vertical_segment(canvas, segment, run, context, size)
|
|
354
|
-
emit_upright_glyph(canvas, glyph, run, context, size)
|
|
355
|
-
else
|
|
356
|
-
if !segment.empty? && segment.last.x != glyph.x
|
|
357
|
-
flush_vertical_segment(canvas, segment, run, context, size)
|
|
358
|
-
end
|
|
359
|
-
segment << glyph
|
|
360
|
-
end
|
|
361
|
-
end
|
|
362
|
-
flush_vertical_segment(canvas, segment, run, context, size)
|
|
363
|
-
end
|
|
364
|
-
private_class_method :emit_vertical_stack
|
|
365
|
-
|
|
366
|
-
def self.flush_vertical_segment(canvas, segment, run, context,
|
|
367
|
-
size)
|
|
368
|
-
return if segment.empty?
|
|
369
|
-
|
|
370
|
-
text = segment.map { |g| [g.codepoint].pack("U") }.join
|
|
371
|
-
first = segment.first
|
|
372
|
-
canvas.save_graphics_state do
|
|
373
|
-
# 90° clockwise: exact matrix (rotate would emit
|
|
374
|
-
# cos/sin float noise like 6.1e-17).
|
|
375
|
-
canvas.concat(0, -1, 1, 0, first.x, first.y)
|
|
376
|
-
text_kwargs = CharacterStyle.text_kwargs(
|
|
377
|
-
run,
|
|
378
|
-
{
|
|
379
|
-
at: [0, 0],
|
|
380
|
-
font: font_for_run(run, context),
|
|
381
|
-
size: size,
|
|
382
|
-
},
|
|
383
|
-
)
|
|
384
|
-
canvas.text(text, **text_kwargs)
|
|
385
|
-
end
|
|
386
|
-
end
|
|
387
|
-
private_class_method :flush_vertical_segment
|
|
388
|
-
|
|
389
|
-
def self.emit_upright_glyph(canvas, positioned, run, context,
|
|
390
|
-
size)
|
|
391
|
-
text = [positioned.codepoint].pack("U")
|
|
392
|
-
text_kwargs = CharacterStyle.text_kwargs(
|
|
393
|
-
run,
|
|
394
|
-
{
|
|
395
|
-
at: [positioned.x, positioned.y],
|
|
396
|
-
font: font_for_run(run, context),
|
|
397
|
-
size: size,
|
|
398
|
-
},
|
|
399
|
-
)
|
|
400
|
-
canvas.text(text, **text_kwargs)
|
|
401
|
-
end
|
|
402
|
-
private_class_method :emit_upright_glyph
|
|
403
|
-
|
|
404
|
-
def self.vertical_fits?(layout_frame, leading, columns, left_limit)
|
|
405
|
-
right = layout_frame.x + layout_frame.width -
|
|
406
|
-
(layout_frame.inset_right || 0)
|
|
407
|
-
(right - (columns * leading)) >= left_limit
|
|
408
|
-
end
|
|
409
|
-
private_class_method :vertical_fits?
|
|
410
|
-
|
|
411
|
-
# Emits the run's ruby alongside its vertical glyphs:
|
|
412
|
-
# stacked top-to-bottom beside the column — right side by
|
|
413
|
-
# default, left for Below* RubyPosition values (the
|
|
414
|
-
# horizontal above/below convention mirrored under
|
|
415
|
-
# rotation).
|
|
416
|
-
def self.emit_vertical_ruby(canvas, run, positioned, frame,
|
|
417
|
-
base_size, context)
|
|
418
|
-
ruby_text = run.ruby_string
|
|
419
|
-
return if ruby_text.nil? || ruby_text.empty?
|
|
420
|
-
|
|
421
|
-
ruby_size = run.ruby_font_size ||
|
|
422
|
-
(base_size * RUBY_SIZE_FACTOR)
|
|
423
|
-
x = ruby_vertical_x(positioned.first, run, base_size)
|
|
424
|
-
y = frame.y + frame.height - (frame.inset_top || 0)
|
|
425
|
-
ruby_text.each_codepoint do |codepoint|
|
|
426
|
-
text_kwargs = CharacterStyle.text_kwargs(
|
|
427
|
-
run,
|
|
428
|
-
{
|
|
429
|
-
at: [x, y - ruby_size],
|
|
430
|
-
font: font_for_run(run, context),
|
|
431
|
-
size: ruby_size,
|
|
432
|
-
},
|
|
433
|
-
)
|
|
434
|
-
canvas.text([codepoint].pack("U"), **text_kwargs)
|
|
435
|
-
y -= ruby_size
|
|
436
|
-
end
|
|
437
|
-
end
|
|
438
|
-
private_class_method :emit_vertical_ruby
|
|
439
|
-
|
|
440
|
-
def self.ruby_vertical_x(positioned, run, base_size)
|
|
441
|
-
offset = base_size * 0.9
|
|
442
|
-
if run.ruby_position.to_s.include?("Below")
|
|
443
|
-
positioned.x - offset
|
|
444
|
-
else
|
|
445
|
-
positioned.x + offset
|
|
446
|
-
end
|
|
447
|
-
end
|
|
448
|
-
private_class_method :ruby_vertical_x
|
|
449
231
|
|
|
450
232
|
def self.chain_head?(frame)
|
|
451
233
|
frame.previous_text_frame.nil? || frame.previous_text_frame == "n"
|
|
@@ -502,8 +284,10 @@ module Idml
|
|
|
502
284
|
(layout_frame.inset_top || 0)
|
|
503
285
|
bottom_limit = TextEngine::VerticalLayout.bottom_limit(layout_frame)
|
|
504
286
|
footnotes = []
|
|
505
|
-
cursor_y = vertical_justify_top(top_y, bottom_limit,
|
|
506
|
-
|
|
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)
|
|
507
291
|
char_cursor = state.char_cursor
|
|
508
292
|
|
|
509
293
|
if state.current_paragraph
|
|
@@ -544,7 +328,9 @@ module Idml
|
|
|
544
328
|
top_y = col_frame.y + col_frame.height - (col_frame.inset_top || 0)
|
|
545
329
|
bottom_limit = TextEngine::VerticalLayout.bottom_limit(col_frame)
|
|
546
330
|
footnotes = []
|
|
547
|
-
cursor_y = top_y - first_baseline_offset(
|
|
331
|
+
cursor_y = top_y - FrameMetrics.first_baseline_offset(
|
|
332
|
+
context, state, font
|
|
333
|
+
)
|
|
548
334
|
char_cursor = state.char_cursor
|
|
549
335
|
|
|
550
336
|
if state.current_paragraph
|
|
@@ -607,160 +393,6 @@ module Idml
|
|
|
607
393
|
private_class_method :build_column_frames
|
|
608
394
|
|
|
609
395
|
# Computes the starting cursor_y for vertical justification.
|
|
610
|
-
# IDML's TextFramePreference.VerticalJustification can be:
|
|
611
|
-
# - TopAlign (default): start at frame top, no offset
|
|
612
|
-
# - CenterAlign: center the content block vertically
|
|
613
|
-
# - BottomAlign: align content to the frame bottom
|
|
614
|
-
# - JustifyAlign: distribute extra space between paragraphs
|
|
615
|
-
# (deferred — falls back to TopAlign behavior)
|
|
616
|
-
#
|
|
617
|
-
# Content height is estimated by walking paragraphs and
|
|
618
|
-
# summing leading × lines + space_before/after. Without
|
|
619
|
-
# shaping/wrapping we can't know exact line count, so we
|
|
620
|
-
# approximate by treating each run as one line.
|
|
621
|
-
def self.vertical_justify_top(top_y, bottom_limit, state, context)
|
|
622
|
-
justification = text_frame_vertical_justification(context)
|
|
623
|
-
return top_y unless %w[CenterAlign BottomAlign].include?(justification)
|
|
624
|
-
|
|
625
|
-
content_height = estimate_content_height(state)
|
|
626
|
-
available = top_y - bottom_limit
|
|
627
|
-
slack = [available - content_height, 0].max
|
|
628
|
-
return top_y if slack.zero?
|
|
629
|
-
|
|
630
|
-
case justification
|
|
631
|
-
when "CenterAlign" then top_y - (slack / 2)
|
|
632
|
-
when "BottomAlign" then top_y - slack
|
|
633
|
-
else top_y
|
|
634
|
-
end
|
|
635
|
-
end
|
|
636
|
-
private_class_method :vertical_justify_top
|
|
637
|
-
|
|
638
|
-
# FirstBaselineOffset: how far below the frame's top inset
|
|
639
|
-
# the first line's baseline sits. The layout's default is
|
|
640
|
-
# leading-based; AscentOffset shifts by (ascent − leading)
|
|
641
|
-
# and FixedHeight by (MinimumFirstBaselineOffset − leading).
|
|
642
|
-
# CapHeight uses the font's cap-height metric (leading
|
|
643
|
-
# fallback when the font reports none); XHeight approximates
|
|
644
|
-
# as 70% of cap height; EmboxHeight uses the em box (the
|
|
645
|
-
# first paragraph's point size). Returns the extra shift to
|
|
646
|
-
# subtract from the cursor.
|
|
647
|
-
FIRST_BASELINE_MODES = %w[
|
|
648
|
-
AscentOffset FixedHeight CapHeight XHeight EmboxHeight
|
|
649
|
-
].freeze
|
|
650
|
-
|
|
651
|
-
def self.first_baseline_offset(context, state, font)
|
|
652
|
-
pref = context.item&.text_frame_preference&.first
|
|
653
|
-
mode = pref&.first_baseline_offset
|
|
654
|
-
return 0.0 unless FIRST_BASELINE_MODES.include?(mode)
|
|
655
|
-
|
|
656
|
-
baseline_target(pref, mode, state, font) -
|
|
657
|
-
first_paragraph_leading(state)
|
|
658
|
-
end
|
|
659
|
-
private_class_method :first_baseline_offset
|
|
660
|
-
|
|
661
|
-
# Distance from the top inset to the first baseline under
|
|
662
|
-
# the given mode.
|
|
663
|
-
# pdfrb 0.7.1 never fills the cap_height metric, so
|
|
664
|
-
# CapHeight / XHeight fall back to standard font
|
|
665
|
-
# proportions (cap ≈ 0.72 em, x ≈ 0.52 em — Arial and
|
|
666
|
-
# Helvetica both sit within 1% of these).
|
|
667
|
-
CAP_HEIGHT_RATIO = 0.72
|
|
668
|
-
X_HEIGHT_RATIO = 0.52
|
|
669
|
-
|
|
670
|
-
def self.baseline_target(pref, mode, state, font)
|
|
671
|
-
leading = first_paragraph_leading(state)
|
|
672
|
-
case mode
|
|
673
|
-
when "AscentOffset"
|
|
674
|
-
ratio_scaled(font.ascent, font) * leading
|
|
675
|
-
when "CapHeight"
|
|
676
|
-
cap_ratio(font) * leading
|
|
677
|
-
when "XHeight"
|
|
678
|
-
X_HEIGHT_RATIO * leading
|
|
679
|
-
when "EmboxHeight"
|
|
680
|
-
first_paragraph_size(state)
|
|
681
|
-
else
|
|
682
|
-
pref.minimum_first_baseline_offset || leading
|
|
683
|
-
end
|
|
684
|
-
end
|
|
685
|
-
private_class_method :baseline_target
|
|
686
|
-
|
|
687
|
-
def self.cap_ratio(font)
|
|
688
|
-
cap = font.cap_height
|
|
689
|
-
return CAP_HEIGHT_RATIO unless cap
|
|
690
|
-
|
|
691
|
-
ratio_scaled(cap, font)
|
|
692
|
-
end
|
|
693
|
-
private_class_method :cap_ratio
|
|
694
|
-
|
|
695
|
-
def self.ratio_scaled(metric, font)
|
|
696
|
-
upem = font.units_per_em
|
|
697
|
-
return 1.0 if upem.zero?
|
|
698
|
-
|
|
699
|
-
metric / upem
|
|
700
|
-
end
|
|
701
|
-
private_class_method :ratio_scaled
|
|
702
|
-
|
|
703
|
-
def self.first_paragraph_size(state)
|
|
704
|
-
paragraph = state.current_paragraph || state.paragraphs.first
|
|
705
|
-
first_run = paragraph&.runs&.first
|
|
706
|
-
first_run&.point_size || DEFAULT_SIZE
|
|
707
|
-
end
|
|
708
|
-
private_class_method :first_paragraph_size
|
|
709
|
-
|
|
710
|
-
def self.first_paragraph_leading(state)
|
|
711
|
-
paragraph = state.current_paragraph || state.paragraphs.first
|
|
712
|
-
unless paragraph
|
|
713
|
-
return DEFAULT_SIZE *
|
|
714
|
-
TextEngine::VerticalLayout::DEFAULT_LEADING_FACTOR
|
|
715
|
-
end
|
|
716
|
-
|
|
717
|
-
size = paragraph.runs.first&.point_size || DEFAULT_SIZE
|
|
718
|
-
TextEngine::VerticalLayout.leading_for(paragraph.auto_leading,
|
|
719
|
-
size)
|
|
720
|
-
end
|
|
721
|
-
private_class_method :first_paragraph_leading
|
|
722
|
-
|
|
723
|
-
def self.text_frame_vertical_justification(context)
|
|
724
|
-
pref = context.item&.text_frame_preference&.first
|
|
725
|
-
pref&.vertical_justification
|
|
726
|
-
end
|
|
727
|
-
private_class_method :text_frame_vertical_justification
|
|
728
|
-
|
|
729
|
-
# Estimates total renderable content height for vertical
|
|
730
|
-
# justification. Walks paragraphs and runs, summing leading
|
|
731
|
-
# per run plus paragraph space_before/after. Approximation:
|
|
732
|
-
# treats each run as one line (wrap may produce more lines,
|
|
733
|
-
# which would over-estimate slack; acceptable for
|
|
734
|
-
# justification since we'd rather under-offset than overflow).
|
|
735
|
-
def self.estimate_content_height(state)
|
|
736
|
-
paragraphs = paragraphs_for_estimate(state)
|
|
737
|
-
return 0 if paragraphs.empty?
|
|
738
|
-
|
|
739
|
-
paragraphs.sum do |paragraph|
|
|
740
|
-
paragraph_height(paragraph)
|
|
741
|
-
end
|
|
742
|
-
end
|
|
743
|
-
private_class_method :estimate_content_height
|
|
744
|
-
|
|
745
|
-
def self.paragraphs_for_estimate(state)
|
|
746
|
-
result = []
|
|
747
|
-
result << state.current_paragraph if state.current_paragraph
|
|
748
|
-
result + state.paragraphs
|
|
749
|
-
end
|
|
750
|
-
private_class_method :paragraphs_for_estimate
|
|
751
|
-
|
|
752
|
-
def self.paragraph_height(paragraph)
|
|
753
|
-
size = paragraph.runs.first&.point_size || DEFAULT_SIZE
|
|
754
|
-
leading = leading_for(paragraph, size)
|
|
755
|
-
line_count = [paragraph.runs.length, 1].max
|
|
756
|
-
(leading * line_count) + space_before_after(paragraph)
|
|
757
|
-
end
|
|
758
|
-
private_class_method :paragraph_height
|
|
759
|
-
|
|
760
|
-
def self.space_before_after(paragraph)
|
|
761
|
-
(paragraph.space_before || 0) + (paragraph.space_after || 0)
|
|
762
|
-
end
|
|
763
|
-
private_class_method :space_before_after
|
|
764
396
|
|
|
765
397
|
def self.resume_paragraph(canvas, state, context, layout_frame, font,
|
|
766
398
|
cursor_y, bottom_limit, char_cursor,
|
|
@@ -789,9 +421,9 @@ module Idml
|
|
|
789
421
|
break if cursor_y < bottom_limit
|
|
790
422
|
|
|
791
423
|
next_paragraph = state.paragraphs[index + 1]
|
|
792
|
-
break if paragraph_deferred?(paragraph, next_paragraph,
|
|
793
|
-
|
|
794
|
-
|
|
424
|
+
break if KeepPolicy.paragraph_deferred?(paragraph, next_paragraph,
|
|
425
|
+
layout_frame, font, cursor_y,
|
|
426
|
+
bottom_limit, pending.positive?)
|
|
795
427
|
|
|
796
428
|
remaining_runs, cursor_y, char_cursor = render_runs_for_paragraph(
|
|
797
429
|
canvas, paragraph, paragraph.runs, context,
|
|
@@ -1185,130 +817,6 @@ module Idml
|
|
|
1185
817
|
end
|
|
1186
818
|
private_class_method :mojikumi_aki_overrides
|
|
1187
819
|
|
|
1188
|
-
# True when the paragraph should move wholly to the next
|
|
1189
|
-
# frame: a StartParagraph forced break, or KeepAllLines-
|
|
1190
|
-
# Together with insufficient remaining space (never for the
|
|
1191
|
-
# frame's first paragraph, so progress is always made).
|
|
1192
|
-
def self.paragraph_deferred?(paragraph, next_paragraph,
|
|
1193
|
-
layout_frame, font, cursor_y,
|
|
1194
|
-
bottom_limit, placed_any)
|
|
1195
|
-
return true if paragraph_break?(paragraph) && placed_any
|
|
1196
|
-
return false unless placed_any
|
|
1197
|
-
return true if keep_with_next_break?(paragraph, next_paragraph,
|
|
1198
|
-
layout_frame, font,
|
|
1199
|
-
cursor_y, bottom_limit)
|
|
1200
|
-
|
|
1201
|
-
keep_all_lines_break?(paragraph, layout_frame, font,
|
|
1202
|
-
cursor_y, bottom_limit) ||
|
|
1203
|
-
keep_windows_break?(paragraph, layout_frame, font,
|
|
1204
|
-
cursor_y, bottom_limit)
|
|
1205
|
-
end
|
|
1206
|
-
private_class_method :paragraph_deferred?
|
|
1207
|
-
|
|
1208
|
-
# KeepAllLinesTogether: defer when the paragraph cannot
|
|
1209
|
-
# fully fit the remaining space.
|
|
1210
|
-
def self.keep_all_lines_break?(paragraph, layout_frame, font,
|
|
1211
|
-
cursor_y, bottom_limit)
|
|
1212
|
-
return false unless paragraph.keep_all_lines_together
|
|
1213
|
-
|
|
1214
|
-
wrap_width = TextEngine::VerticalLayout.wrap_width(
|
|
1215
|
-
layout_frame, paragraph.right_indent || 0
|
|
1216
|
-
)
|
|
1217
|
-
height = TextEngine::Measurement.paragraph_height(
|
|
1218
|
-
paragraph, font, wrap_width
|
|
1219
|
-
)
|
|
1220
|
-
(cursor_y - height) < bottom_limit
|
|
1221
|
-
end
|
|
1222
|
-
private_class_method :keep_all_lines_break?
|
|
1223
|
-
|
|
1224
|
-
# Partial keep windows, approximated by measured line
|
|
1225
|
-
# counts: KeepFirstLines defers when fewer than N lines
|
|
1226
|
-
# fit; KeepLastLines defers when the overflow tail for the
|
|
1227
|
-
# next frame would strand fewer than N lines.
|
|
1228
|
-
def self.keep_windows_break?(paragraph, layout_frame, font,
|
|
1229
|
-
cursor_y, bottom_limit)
|
|
1230
|
-
return false unless paragraph.keep_first_lines ||
|
|
1231
|
-
paragraph.keep_last_lines
|
|
1232
|
-
|
|
1233
|
-
wrap_width = TextEngine::VerticalLayout.wrap_width(
|
|
1234
|
-
layout_frame, paragraph.right_indent || 0
|
|
1235
|
-
)
|
|
1236
|
-
height = TextEngine::Measurement.paragraph_height(
|
|
1237
|
-
paragraph, font, wrap_width
|
|
1238
|
-
)
|
|
1239
|
-
return false if (cursor_y - height) >= bottom_limit
|
|
1240
|
-
|
|
1241
|
-
leading = first_paragraph_leading_for(paragraph)
|
|
1242
|
-
lines_fit = [((cursor_y - bottom_limit) / leading).floor, 0].max
|
|
1243
|
-
first_window_break?(paragraph, lines_fit) ||
|
|
1244
|
-
last_window_break?(paragraph, lines_fit, height, leading)
|
|
1245
|
-
end
|
|
1246
|
-
private_class_method :keep_windows_break?
|
|
1247
|
-
|
|
1248
|
-
def self.first_window_break?(paragraph, lines_fit)
|
|
1249
|
-
paragraph.keep_first_lines &&
|
|
1250
|
-
lines_fit < paragraph.keep_first_lines
|
|
1251
|
-
end
|
|
1252
|
-
private_class_method :first_window_break?
|
|
1253
|
-
|
|
1254
|
-
def self.last_window_break?(paragraph, lines_fit, height, leading)
|
|
1255
|
-
return false unless paragraph.keep_last_lines
|
|
1256
|
-
|
|
1257
|
-
total_lines = [(height / leading).ceil, 1].max
|
|
1258
|
-
lines_fit.positive? &&
|
|
1259
|
-
(total_lines - lines_fit) < paragraph.keep_last_lines
|
|
1260
|
-
end
|
|
1261
|
-
private_class_method :last_window_break?
|
|
1262
|
-
|
|
1263
|
-
def self.first_paragraph_leading_for(paragraph)
|
|
1264
|
-
size = paragraph.runs.first&.point_size || DEFAULT_SIZE
|
|
1265
|
-
TextEngine::VerticalLayout.leading_for(paragraph.auto_leading,
|
|
1266
|
-
size)
|
|
1267
|
-
end
|
|
1268
|
-
private_class_method :first_paragraph_leading_for
|
|
1269
|
-
|
|
1270
|
-
# KeepWithNext: this paragraph defers when the next
|
|
1271
|
-
# paragraph is forced to the next frame, or when the next
|
|
1272
|
-
# paragraph's first line would not fit after this one.
|
|
1273
|
-
def self.keep_with_next_break?(paragraph, next_paragraph,
|
|
1274
|
-
layout_frame, font, cursor_y,
|
|
1275
|
-
bottom_limit)
|
|
1276
|
-
return false unless keepable_pair?(paragraph, next_paragraph)
|
|
1277
|
-
return true if paragraph_break?(next_paragraph)
|
|
1278
|
-
|
|
1279
|
-
wrap_width = TextEngine::VerticalLayout.wrap_width(
|
|
1280
|
-
layout_frame, paragraph.right_indent || 0
|
|
1281
|
-
)
|
|
1282
|
-
height = TextEngine::Measurement.paragraph_height(
|
|
1283
|
-
paragraph, font, wrap_width
|
|
1284
|
-
)
|
|
1285
|
-
(cursor_y - height - follower_leading(next_paragraph)) <
|
|
1286
|
-
bottom_limit
|
|
1287
|
-
end
|
|
1288
|
-
private_class_method :keep_with_next_break?
|
|
1289
|
-
|
|
1290
|
-
def self.keepable_pair?(paragraph, next_paragraph)
|
|
1291
|
-
!paragraph.keep_with_next.nil? && !next_paragraph.nil?
|
|
1292
|
-
end
|
|
1293
|
-
private_class_method :keepable_pair?
|
|
1294
|
-
|
|
1295
|
-
def self.follower_leading(next_paragraph)
|
|
1296
|
-
size = next_paragraph.runs.first&.point_size || DEFAULT_SIZE
|
|
1297
|
-
TextEngine::VerticalLayout.leading_for(
|
|
1298
|
-
next_paragraph.auto_leading, size
|
|
1299
|
-
)
|
|
1300
|
-
end
|
|
1301
|
-
private_class_method :follower_leading
|
|
1302
|
-
|
|
1303
|
-
# True when the paragraph requests a forced break to the
|
|
1304
|
-
# next frame/column (StartParagraph). All break flavors act
|
|
1305
|
-
# at frame/column granularity.
|
|
1306
|
-
def self.paragraph_break?(paragraph)
|
|
1307
|
-
%w[NextPage NextColumn NextFrame NextOddPage NextEvenPage]
|
|
1308
|
-
.include?(paragraph.start_paragraph)
|
|
1309
|
-
end
|
|
1310
|
-
private_class_method :paragraph_break?
|
|
1311
|
-
|
|
1312
820
|
# Justifies the run's lines; only the final line of the
|
|
1313
821
|
# paragraph's final run stays ragged under full
|
|
1314
822
|
# justification.
|
|
@@ -1446,74 +954,6 @@ module Idml
|
|
|
1446
954
|
line.glyphs.map { |g| [g.codepoint].pack("U") }.join
|
|
1447
955
|
end
|
|
1448
956
|
private_class_method :line_text
|
|
1449
|
-
|
|
1450
|
-
# Fallback when no metrics are available: emit all runs as
|
|
1451
|
-
# one `text_rich` block, letting pdfrb measure advance widths.
|
|
1452
|
-
# Chain threading is best-effort here — the simple path
|
|
1453
|
-
# doesn't wrap, so the first frame's render typically emits
|
|
1454
|
-
# all content and clears the chain state.
|
|
1455
|
-
def self.simple_render(canvas, state, context, box)
|
|
1456
|
-
runs = collect_runs(state)
|
|
1457
|
-
return if runs.empty?
|
|
1458
|
-
|
|
1459
|
-
runs_for = build_rich_runs(runs, context)
|
|
1460
|
-
first_size = runs.first.point_size || DEFAULT_SIZE
|
|
1461
|
-
canvas.text_rich(
|
|
1462
|
-
runs_for,
|
|
1463
|
-
at: [box[:x], box[:y] + box[:height] - first_size],
|
|
1464
|
-
)
|
|
1465
|
-
emit_simple_footnotes(canvas, runs, context, box)
|
|
1466
|
-
state.paragraphs = []
|
|
1467
|
-
state.current_paragraph = nil
|
|
1468
|
-
state.runs_remaining = []
|
|
1469
|
-
end
|
|
1470
|
-
private_class_method :simple_render
|
|
1471
|
-
|
|
1472
|
-
# Footnote text for the rough (no-metrics) path: one line
|
|
1473
|
-
# per footnote paragraph, stacked upward from the frame's
|
|
1474
|
-
# bottom edge below a hairline separator. Paragraphs are
|
|
1475
|
-
# already marker-prefixed at extraction.
|
|
1476
|
-
def self.emit_simple_footnotes(canvas, runs, context, box)
|
|
1477
|
-
paragraphs = runs.flat_map(&:footnote_paragraphs).compact
|
|
1478
|
-
return if paragraphs.empty?
|
|
1479
|
-
|
|
1480
|
-
size = paragraphs.first.runs.first&.point_size || DEFAULT_SIZE
|
|
1481
|
-
block_top = box[:y] + (paragraphs.length * size)
|
|
1482
|
-
canvas.line_width = 0.5
|
|
1483
|
-
canvas.stroke_color([:gray, 0.0])
|
|
1484
|
-
canvas.move_to(box[:x], block_top + (size * 0.3))
|
|
1485
|
-
canvas.line_to(box[:x] + (box[:width] * 0.25), block_top + (size * 0.3))
|
|
1486
|
-
canvas.stroke
|
|
1487
|
-
|
|
1488
|
-
paragraphs.reverse_each.with_index do |paragraph, index|
|
|
1489
|
-
canvas.text_rich(
|
|
1490
|
-
build_rich_runs(paragraph.runs, context),
|
|
1491
|
-
at: [box[:x], box[:y] + ((index + 1) * size)],
|
|
1492
|
-
)
|
|
1493
|
-
end
|
|
1494
|
-
end
|
|
1495
|
-
private_class_method :emit_simple_footnotes
|
|
1496
|
-
|
|
1497
|
-
def self.collect_runs(state)
|
|
1498
|
-
runs = state.runs_remaining || []
|
|
1499
|
-
if state.current_paragraph
|
|
1500
|
-
runs + state.current_paragraph.runs
|
|
1501
|
-
else
|
|
1502
|
-
runs + state.paragraphs.flat_map(&:runs)
|
|
1503
|
-
end
|
|
1504
|
-
end
|
|
1505
|
-
private_class_method :collect_runs
|
|
1506
|
-
|
|
1507
|
-
def self.build_rich_runs(runs, context)
|
|
1508
|
-
runs.map do |run|
|
|
1509
|
-
{
|
|
1510
|
-
text: run.text,
|
|
1511
|
-
font: font_for_run(run, context),
|
|
1512
|
-
size: run.point_size || DEFAULT_SIZE,
|
|
1513
|
-
}
|
|
1514
|
-
end
|
|
1515
|
-
end
|
|
1516
|
-
private_class_method :build_rich_runs
|
|
1517
957
|
end
|
|
1518
958
|
end
|
|
1519
959
|
end
|