Module: Idml::Render::Renderers::FrameMetrics

Defined in:
lib/idml/render/renderers/frame_metrics.rb

Overview

Frame positioning policy for the text engine: where the first baseline sits under each FirstBaselineOffset mode, where the content block starts under vertical justification, and how tall the pending content is estimated to be. Pure functions of the TextFrame's preference, the chain state, and font metrics — no canvas, no emission (MECE: this module owns frame-positioning policy; TextFrameRenderer owns flow and emission; KeepPolicy owns paragraph deferral).

Constant Summary collapse

FIRST_BASELINE_MODES =
%w[
  AscentOffset FixedHeight CapHeight XHeight EmboxHeight
].freeze
CAP_HEIGHT_RATIO =

pdfrb 0.7.1 never fills the cap_height metric, so CapHeight / XHeight fall back to standard font proportions (cap ≈ 0.72 em, x ≈ 0.52 em — Arial and Helvetica both sit within 1% of these).

0.72
X_HEIGHT_RATIO =
0.52

Class Method Summary collapse

Class Method Details

.baseline_target(pref, mode, state, font) ⇒ Object

Distance from the top inset to the first baseline under the given mode.



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/idml/render/renderers/frame_metrics.rb', line 67

def baseline_target(pref, mode, state, font)
  leading = first_paragraph_leading(state)
  case mode
  when "AscentOffset"
    ratio_scaled(font.ascent, font) * leading
  when "CapHeight"
    cap_ratio(font) * leading
  when "XHeight"
    X_HEIGHT_RATIO * leading
  when "EmboxHeight"
    first_paragraph_size(state)
  else
    pref.minimum_first_baseline_offset || leading
  end
end

.cap_ratio(font) ⇒ Object



83
84
85
86
87
88
# File 'lib/idml/render/renderers/frame_metrics.rb', line 83

def cap_ratio(font)
  cap = font.cap_height
  return CAP_HEIGHT_RATIO unless cap

  ratio_scaled(cap, font)
end

.estimate_content_height(state) ⇒ Object

Estimates total renderable content height for vertical justification. Walks paragraphs and runs, summing leading per run plus paragraph space_before/after. Approximation: treats each run as one line (wrap may produce more lines, which would over-estimate slack; acceptable for justification since we'd rather under-offset than overflow).



134
135
136
# File 'lib/idml/render/renderers/frame_metrics.rb', line 134

def estimate_content_height(state)
  paragraphs_for_estimate(state).sum { |p| paragraph_height(p) }
end

.first_baseline_offset(context, state, font) ⇒ Object

FirstBaselineOffset: how far below the frame's top inset the first line's baseline sits, relative to the layout's leading-based default. AscentOffset shifts by (ascent − leading), FixedHeight by (MinimumFirstBaselineOffset − leading), CapHeight by the cap-height proportion, XHeight by the x-height proportion, EmboxHeight by the em box (the first paragraph's point size).



56
57
58
59
60
61
62
63
# File 'lib/idml/render/renderers/frame_metrics.rb', line 56

def first_baseline_offset(context, state, font)
  pref = context.item&.text_frame_preference&.first
  mode = pref&.first_baseline_offset
  return 0.0 unless FIRST_BASELINE_MODES.include?(mode)

  baseline_target(pref, mode, state, font) -
    first_paragraph_leading(state)
end

.first_paragraph_leading(state) ⇒ Object



112
113
114
115
116
117
118
119
120
# File 'lib/idml/render/renderers/frame_metrics.rb', line 112

def first_paragraph_leading(state)
  paragraph = state.current_paragraph || state.paragraphs.first
  unless paragraph
    return TextFrameRenderer::DEFAULT_SIZE *
        TextEngine::VerticalLayout::DEFAULT_LEADING_FACTOR
  end

  leading_for(paragraph)
end

.first_paragraph_size(state) ⇒ Object



97
98
99
100
101
# File 'lib/idml/render/renderers/frame_metrics.rb', line 97

def first_paragraph_size(state)
  paragraph = state.current_paragraph || state.paragraphs.first
  first_run = paragraph&.runs&.first
  first_run&.point_size || TextFrameRenderer::DEFAULT_SIZE
end

.leading_for(paragraph) ⇒ Object

Leading of a paragraph computed from its first run's point size (the default size when it has no runs yet).



105
106
107
108
109
110
# File 'lib/idml/render/renderers/frame_metrics.rb', line 105

def leading_for(paragraph)
  size = paragraph.runs.first&.point_size ||
    TextFrameRenderer::DEFAULT_SIZE
  TextEngine::VerticalLayout.leading_for(paragraph.auto_leading,
                                         size)
end

.paragraph_height(paragraph) ⇒ Object



144
145
146
147
148
149
150
151
152
# File 'lib/idml/render/renderers/frame_metrics.rb', line 144

def paragraph_height(paragraph)
  size = paragraph.runs.first&.point_size ||
    TextFrameRenderer::DEFAULT_SIZE
  leading = TextEngine::VerticalLayout.leading_for(
    paragraph.auto_leading, size
  )
  line_count = [paragraph.runs.length, 1].max
  (leading * line_count) + space_before_after(paragraph)
end

.paragraphs_for_estimate(state) ⇒ Object



138
139
140
141
142
# File 'lib/idml/render/renderers/frame_metrics.rb', line 138

def paragraphs_for_estimate(state)
  result = []
  result << state.current_paragraph if state.current_paragraph
  result + state.paragraphs
end

.ratio_scaled(metric, font) ⇒ Object



90
91
92
93
94
95
# File 'lib/idml/render/renderers/frame_metrics.rb', line 90

def ratio_scaled(metric, font)
  upem = font.units_per_em
  return 1.0 if upem.zero?

  metric / upem
end

.space_before_after(paragraph) ⇒ Object



154
155
156
# File 'lib/idml/render/renderers/frame_metrics.rb', line 154

def space_before_after(paragraph)
  (paragraph.space_before || 0) + (paragraph.space_after || 0)
end

.text_frame_vertical_justification(context) ⇒ Object



122
123
124
125
# File 'lib/idml/render/renderers/frame_metrics.rb', line 122

def text_frame_vertical_justification(context)
  pref = context.item&.text_frame_preference&.first
  pref&.vertical_justification
end

.vertical_justify_top(top_y, bottom_limit, state, context) ⇒ Object

Top y for the content block under the frame's vertical justification: TopAlign keeps the frame top; CenterAlign / BottomAlign offset by the slack between the estimated content height and the available space (JustifyAlign falls back to TopAlign behavior).



34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/idml/render/renderers/frame_metrics.rb', line 34

def vertical_justify_top(top_y, bottom_limit, state, context)
  justification = text_frame_vertical_justification(context)
  return top_y unless %w[CenterAlign BottomAlign].include?(justification)

  available = top_y - bottom_limit
  slack = [available - estimate_content_height(state), 0].max
  return top_y if slack.zero?

  case justification
  when "CenterAlign" then top_y - (slack / 2)
  when "BottomAlign" then top_y - slack
  else top_y
  end
end