idml 0.8.9 → 0.9.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d58f00746f6afb4233e161dfa9cdeb116d714eed0c238ec543a0f039b4167f0e
4
- data.tar.gz: 1a000e34996525fe34cdc441625c9eaa303f121c836ae3f3b07b73c12712abb1
3
+ metadata.gz: ff487a296f9cbd5b926b963c7acb7d4864d43060ffed5b1ec7438a75bac1446a
4
+ data.tar.gz: f4926fe3add1833a954628aace986839f101722deafde891783c54afdd509a58
5
5
  SHA512:
6
- metadata.gz: 15001cbf1427dc278c701cd4980b837ff4b015f6c5657eeb5c88c0c8774933934e606439c40fa3c900368ef2959fc1170cd12841a9caadc0c025a199dd18cd68
7
- data.tar.gz: 41f45b1b2db245e1dd2d5ef40aa60990fc3c957930b3d16a633974d1f267149e8b2c5cf638b7b3549b4318bf8ea62f378fbb5d0f98ad32d4d50fbce0e3cb9505
6
+ metadata.gz: 043ed38d8ebdcf1af8506e83d8548b6590d923f0b4b1ef01687dca5c7528bc5d9a16c79c67e06dc626abe70334afd72882aefac2f15f8253c71d56bc40224274
7
+ data.tar.gz: 0fefb80560e396290585e3677ca395abd1096511b78321c7ff05ff3add4b5bf0242a4c21ea3321244b7da5290651e369b376696400dc19429ecc547b1c534842
@@ -1,47 +1,44 @@
1
1
  # TODO PDF 101: Paragraph borders and rules (RuleAbove, RuleBelow, ParagraphBorder)
2
2
 
3
- ## Status: PARTIALRuleAbove and RuleBelow implemented via new
4
- `Render::ParagraphRules` helper; TextFrameRenderer emits them at
5
- paragraph top/bottom. Rule color comes from PSR#stroke_color (IDML's
6
- PSR doesn't have a dedicated RuleAboveColor attribute). RuleAboveWidth
7
- "Text" vs "Column" honored. Tint scaling + line weight + offset honored.
8
-
9
- ParagraphBorder and ParagraphShading remain open — they need the
10
- paragraph's bounding rect, which the renderer doesn't currently
11
- expose to ParagraphRules. Will add when a real document fixture
12
- exercises them.
13
-
14
- ## Problem
15
-
16
- `ParagraphStyleRange` carries:
17
-
18
- - `RuleAbove`, `RuleAboveLineWeight`, `RuleAboveTint`, `RuleAboveOffset`,
19
- `RuleAboveLeftIndent`, `RuleAboveRightIndent`, `RuleAboveWidth`
20
- - `RuleBelow`, `RuleBelowLineWeight`, `RuleBelowTint`, `RuleBelowOffset`,
21
- `RuleBelowLeftIndent`, `RuleBelowRightIndent`, `RuleBelowWidth`
22
- - `ParagraphBorderOn`, `ParagraphBorderTopLineWeight`,
23
- `ParagraphBorderLeftLineWeight`, etc.
24
- - `ParagraphShadingOn`, `ParagraphShadingTint`, `ParagraphShadingColor`
25
-
26
- None of these are rendered today. Real-world documents use RuleAbove
27
- heavily for headings ("section divider line"), and ParagraphShading
28
- for callouts.
29
-
30
- ## What needs to happen
31
-
32
- 1. After laying out a paragraph, emit RuleAbove as a horizontal line
33
- above the first line, with the rule's weight/tint/color/offset/indent.
34
- 2. Emit RuleBelow similarly below the last line.
35
- 3. Optionally emit ParagraphBorder as a rectangle around the paragraph.
36
- 4. Optionally emit ParagraphShading as a fill behind the paragraph.
3
+ ## Status: COMPLETE — implemented 2026-08-17
4
+
5
+ ## What was built
6
+
7
+ - RuleAbove / RuleBelow (earlier milestone): horizontal rules at
8
+ paragraph top/bottom with weight / tint / offset / indent /
9
+ TextWidth-ColumnWidth modes, via `Render::ParagraphRules`.
10
+ - Rule colors now resolve from `Properties > RuleAboveColor` /
11
+ `RuleBelowColor` (typed value elements) with StrokeColor fallback
12
+ modeled via `Elements::TypedValue` + Properties mapping, PSR/CSR
13
+ now carry `properties` collections.
14
+ - ParagraphShading: fill rect behind the paragraph block. Color from
15
+ `Properties > ParagraphShadingColor`, tint scaling, ParagraphShading
16
+ width mode (ColumnWidth default / TextWidth), four outward offsets.
17
+ `TextEngine::Measurement` pre-measures the paragraph's height so
18
+ the rect is known before the lines are laid out (clamped to the
19
+ frame's bottom limit when the paragraph splits).
20
+ - ParagraphBorder: per-side strokes around the same block rect, each
21
+ side with its own line weight and the border offsets/tint/color
22
+ (Properties > ParagraphBorderColor). Shading and border share one
23
+ extent so they always agree.
24
+
25
+ ## Known limitations
26
+
27
+ - Border corner radii and gap colors are parsed on the model but not
28
+ rendered; MergeConsecutiveParaBorders (merged rects across like-
29
+ bordered paragraphs) is not modeled.
30
+ - DisplayIfSplits: when a paragraph splits across frames, both
31
+ shading and border are drawn around the rendered portion only.
37
32
 
38
33
  ## Acceptance criteria
39
34
 
40
- - [ ] PSR with RuleAbove=true and RuleAboveLineWeight=2 draws a 2pt
41
- horizontal line above the paragraph.
42
- - [ ] PSR with RuleAboveOffset=4 leaves 4pt gap between rule and text.
43
- - [ ] PSR with ParagraphShadingOn=true and tint fills the paragraph rect.
35
+ - [x] PSR with RuleAbove=true and RuleAboveLineWeight=2 draws the
36
+ rule above the paragraph.
37
+ - [x] PSR with RuleAboveOffset=4 leaves a 4pt gap.
38
+ - [x] PSR with ParagraphShadingOn=true and tint fills the paragraph
39
+ rect.
40
+ - [x] ParagraphBorderOn draws per-side strokes.
44
41
 
45
42
  ## Dependencies
46
43
 
47
- - TODO 94 (VerticalLayout knows the paragraph's rect).
44
+ - TODO 94 (VerticalLayout knows the paragraph's rect) — satisfied.
@@ -298,6 +298,7 @@ module Idml
298
298
  attribute :content, Idml::Elements::Content, collection: true
299
299
  attribute :hyperlink_text_source, Idml::Elements::HyperlinkTextSource,
300
300
  collection: true
301
+ attribute :properties, Idml::Elements::Properties, collection: true
301
302
  attribute :table, Idml::Elements::Table, collection: true
302
303
  attribute :xml_element, Idml::Elements::XmlElement, collection: true
303
304
  attribute :footnote, Idml::Elements::Footnote, collection: true
@@ -664,6 +665,7 @@ module Idml
664
665
  map_element "CharacterStyleRange", to: :character_style_range
665
666
  map_element "Content", to: :content
666
667
  map_element "HyperlinkTextSource", to: :hyperlink_text_source
668
+ map_element "Properties", to: :properties
667
669
  map_element "Table", to: :table
668
670
  map_element "XMLElement", to: :xml_element
669
671
  map_element "Footnote", to: :footnote
@@ -298,6 +298,7 @@ module Idml
298
298
  collection: true
299
299
  attribute :content, Idml::Elements::Content, collection: true
300
300
  attribute :xml_element, Idml::Elements::XmlElement, collection: true
301
+ attribute :properties, Idml::Elements::Properties, collection: true
301
302
 
302
303
  xml do
303
304
  root "ParagraphStyleRange"
@@ -651,6 +652,7 @@ module Idml
651
652
  map_element "ParagraphStyleRange", to: :paragraph_style_range
652
653
  map_element "Content", to: :content
653
654
  map_element "XMLElement", to: :xml_element
655
+ map_element "Properties", to: :properties
654
656
  end
655
657
 
656
658
  def text_content
@@ -2,18 +2,31 @@
2
2
 
3
3
  module Idml
4
4
  module Elements
5
- # `<Properties>` — a common child element on page items. Carries
6
- # optional PathBoundingBox, PathGeometry, and Label. Only the
7
- # modeled children are populated from XML; others are ignored.
5
+ # `<Properties>` — a common child element on page items and
6
+ # story style ranges. Carries optional PathBoundingBox,
7
+ # PathGeometry, Label, and the paragraph-decoration color value
8
+ # elements (RuleAboveColor, ParagraphShadingColor, …) used on
9
+ # PSR/CSR Properties. Only the modeled children are populated
10
+ # from XML; others are ignored.
8
11
  class Properties < Lutaml::Model::Serializable
9
12
  attribute :path_bounding_box, :string
10
13
  attribute :path_geometry, Idml::Elements::PathGeometry,
11
14
  collection: true
15
+ attribute :rule_above_color, Idml::Elements::TypedValue
16
+ attribute :rule_below_color, Idml::Elements::TypedValue
17
+ attribute :paragraph_shading_color, Idml::Elements::TypedValue
18
+ attribute :paragraph_border_color, Idml::Elements::TypedValue
19
+ attribute :paragraph_border_gap_color, Idml::Elements::TypedValue
12
20
 
13
21
  xml do
14
22
  root "Properties"
15
23
  map_element "PathBoundingBox", to: :path_bounding_box
16
24
  map_element "PathGeometry", to: :path_geometry
25
+ map_element "RuleAboveColor", to: :rule_above_color
26
+ map_element "RuleBelowColor", to: :rule_below_color
27
+ map_element "ParagraphShadingColor", to: :paragraph_shading_color
28
+ map_element "ParagraphBorderColor", to: :paragraph_border_color
29
+ map_element "ParagraphBorderGapColor", to: :paragraph_border_gap_color
17
30
  end
18
31
 
19
32
  def first_geometry
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Elements
5
+ # Typed value child element — IDML's `(object_type, xsd:string) |
6
+ # (string_type, xsd:string)` wrapper used inside Properties for
7
+ # values like RuleAboveColor and ParagraphShadingColor: a `type`
8
+ # attribute plus the value as text content.
9
+ class TypedValue < Lutaml::Model::Serializable
10
+ attribute :type, :string
11
+ attribute :value, :string
12
+
13
+ xml do
14
+ root "TypedValue"
15
+ map_attribute "type", to: :type
16
+ map_content to: :value
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/idml/elements.rb CHANGED
@@ -147,6 +147,7 @@ module Idml
147
147
  autoload :TOCStyle, "idml/elements/toc_style"
148
148
  autoload :TOCStyleEntry, "idml/elements/toc_style_entry"
149
149
  autoload :Table, "idml/elements/table"
150
+ autoload :TypedValue, "idml/elements/typed_value"
150
151
  autoload :TableCell, "idml/elements/table_cell"
151
152
  autoload :TableRow, "idml/elements/table_row"
152
153
  autoload :TableStyle, "idml/elements/table_style"
@@ -82,6 +82,131 @@ module Idml
82
82
  end
83
83
  private_class_method :emit_rule
84
84
 
85
+ # Emits the paragraph's shading: a fill rect behind the
86
+ # paragraph block (top_y..bottom_y within the column). The
87
+ # ParagraphShading* offsets expand the rect outward; tint
88
+ # scales the color. Color comes from Properties >
89
+ # ParagraphShadingColor (black when absent, per InDesign's
90
+ # default shading color).
91
+ def self.emit_shading(canvas, paragraph, context, top_y, bottom_y,
92
+ frame_left, frame_right)
93
+ return unless paragraph.paragraph_shading_on
94
+ return unless top_y > bottom_y
95
+
96
+ tint_value = decoration_tint(paragraph.paragraph_shading_tint)
97
+ return unless tint_value
98
+
99
+ color = resolve_color(paragraph.paragraph_shading_color,
100
+ tint_value, context)
101
+ return unless color
102
+
103
+ left, right, top, bottom = decoration_rect(
104
+ paragraph, top_y, bottom_y, frame_left, frame_right,
105
+ left_offset: paragraph.paragraph_shading_left_offset,
106
+ right_offset: paragraph.paragraph_shading_right_offset,
107
+ top_offset: paragraph.paragraph_shading_top_offset,
108
+ bottom_offset: paragraph.paragraph_shading_bottom_offset
109
+ )
110
+ canvas.fill_color(color)
111
+ canvas.rectangle(left, bottom, right - left, top - bottom)
112
+ canvas.fill
113
+ end
114
+
115
+ # Emits the paragraph's border: per-side strokes around the
116
+ # paragraph block, each side with its own line weight, drawn
117
+ # at the block rect expanded by the border offsets.
118
+ def self.emit_border(canvas, paragraph, context, top_y, bottom_y,
119
+ frame_left, frame_right)
120
+ return unless paragraph.paragraph_border_on
121
+ return unless top_y > bottom_y
122
+
123
+ tint_value = decoration_tint(paragraph.paragraph_border_tint)
124
+ return unless tint_value
125
+
126
+ color = resolve_color(paragraph.paragraph_border_color,
127
+ tint_value, context)
128
+ return unless color
129
+
130
+ left, right, top, bottom = decoration_rect(
131
+ paragraph, top_y, bottom_y, frame_left, frame_right,
132
+ left_offset: paragraph.paragraph_border_left_offset,
133
+ right_offset: paragraph.paragraph_border_right_offset,
134
+ top_offset: paragraph.paragraph_border_top_offset,
135
+ bottom_offset: paragraph.paragraph_border_bottom_offset
136
+ )
137
+ apply_rule_color(canvas, paragraph.paragraph_border_color,
138
+ tint_value, context)
139
+ stroke_border_sides(canvas, paragraph, left, right, top, bottom)
140
+ end
141
+
142
+ # Tint value for a decoration, or nil when zero/absent.
143
+ def self.decoration_tint(tint)
144
+ value = (tint || DEFAULT_TINT).to_f
145
+ value.positive? ? value : nil
146
+ end
147
+ private_class_method :decoration_tint
148
+
149
+ # The decoration rect [left, right, top, bottom]: the column
150
+ # (or text) extents expanded outward by the given offsets.
151
+ def self.decoration_rect(paragraph, top_y, bottom_y, frame_left,
152
+ frame_right, left_offset:, right_offset:,
153
+ top_offset:, bottom_offset:)
154
+ left, right = decoration_extents(paragraph, frame_left, frame_right)
155
+ [left - (left_offset || 0).to_f,
156
+ right + (right_offset || 0).to_f,
157
+ top_y + (top_offset || 0).to_f,
158
+ bottom_y - (bottom_offset || 0).to_f]
159
+ end
160
+ private_class_method :decoration_rect
161
+
162
+ def self.stroke_border_sides(canvas, paragraph, left, right,
163
+ top, bottom)
164
+ stroke_horizontal(canvas, left, right, top,
165
+ paragraph.paragraph_border_top_line_weight)
166
+ stroke_horizontal(canvas, left, right, bottom,
167
+ paragraph.paragraph_border_bottom_line_weight)
168
+ stroke_vertical(canvas, bottom, top, left,
169
+ paragraph.paragraph_border_left_line_weight)
170
+ stroke_vertical(canvas, bottom, top, right,
171
+ paragraph.paragraph_border_right_line_weight)
172
+ end
173
+ private_class_method :stroke_border_sides
174
+
175
+ def self.stroke_horizontal(canvas, x1, x2, y, weight)
176
+ thickness = weight.to_f
177
+ return unless thickness.positive?
178
+
179
+ canvas.line_width = thickness
180
+ canvas.move_to(x1, y)
181
+ canvas.line_to(x2, y)
182
+ canvas.stroke
183
+ end
184
+ private_class_method :stroke_horizontal
185
+
186
+ def self.stroke_vertical(canvas, y1, y2, x, weight)
187
+ thickness = weight.to_f
188
+ return unless thickness.positive?
189
+
190
+ canvas.line_width = thickness
191
+ canvas.move_to(x, y1)
192
+ canvas.line_to(x, y2)
193
+ canvas.stroke
194
+ end
195
+ private_class_method :stroke_vertical
196
+
197
+ # Horizontal extents for shading/border: the column width, or
198
+ # the text width (indented by the paragraph's indents) when
199
+ # the shading width mode is "TextWidth".
200
+ def self.decoration_extents(paragraph, frame_left, frame_right)
201
+ if paragraph.paragraph_shading_width == "TextWidth"
202
+ [frame_left + (paragraph.left_indent || 0).to_f,
203
+ frame_right - (paragraph.right_indent || 0).to_f]
204
+ else
205
+ [frame_left, frame_right]
206
+ end
207
+ end
208
+ private_class_method :decoration_extents
209
+
85
210
  # Returns `[x1, x2]` for the rule. Width mode "Text" indents
86
211
  # the rule by the paragraph's left/right indent (matches the
87
212
  # text bounds); "Column" (default) spans the full column.
@@ -445,6 +445,12 @@ module Idml
445
445
 
446
446
  emit_paragraph_top_rule(canvas, paragraph, context, cursor_y,
447
447
  frame_left, frame_right, first_paragraph)
448
+ block_top, block_bottom = paragraph_block_extent(
449
+ paragraph, font, wrap_width, cursor_y, bottom_limit
450
+ )
451
+ ParagraphRules.emit_shading(canvas, paragraph, context,
452
+ block_top, block_bottom,
453
+ frame_left, frame_right)
448
454
  drop_cap = emit_drop_cap(canvas, paragraph, context, frame_left,
449
455
  cursor_y, first_paragraph)
450
456
 
@@ -463,10 +469,28 @@ module Idml
463
469
  cursor_y, frame_left,
464
470
  frame_right, para_attrs,
465
471
  remaining_runs)
472
+ ParagraphRules.emit_border(canvas, paragraph, context,
473
+ block_top, block_bottom,
474
+ frame_left, frame_right)
466
475
  [remaining_runs, cursor_y, char_cursor]
467
476
  end
468
477
  private_class_method :render_runs_for_paragraph
469
478
 
479
+ # The paragraph's block extent: top at the current cursor,
480
+ # bottom at the pre-measured height clamped to the frame's
481
+ # bottom limit. Shading and border both use this one rect so
482
+ # they agree even when the paragraph splits across frames
483
+ # (the split case shades only the rendered part).
484
+ def self.paragraph_block_extent(paragraph, font, wrap_width,
485
+ cursor_y, bottom_limit)
486
+ height = TextEngine::Measurement.paragraph_height(
487
+ paragraph, font, wrap_width
488
+ )
489
+ block_bottom = [cursor_y - height, bottom_limit].max
490
+ [cursor_y, block_bottom]
491
+ end
492
+ private_class_method :paragraph_block_extent
493
+
470
494
  def self.iterate_paragraph_runs(canvas, paragraph, runs, context,
471
495
  layout_frame, font, wrap_width,
472
496
  cursor_y, bottom_limit, char_cursor,
@@ -82,6 +82,28 @@ module Idml
82
82
  :rule_below_left_indent,
83
83
  :rule_below_right_indent,
84
84
  :rule_below_width,
85
+ # ParagraphShading — fill rect behind the paragraph block.
86
+ # Color comes from Properties > ParagraphShadingColor.
87
+ :paragraph_shading_on,
88
+ :paragraph_shading_color,
89
+ :paragraph_shading_tint,
90
+ :paragraph_shading_width,
91
+ :paragraph_shading_left_offset,
92
+ :paragraph_shading_right_offset,
93
+ :paragraph_shading_top_offset,
94
+ :paragraph_shading_bottom_offset,
95
+ # ParagraphBorder — per-side strokes around the paragraph.
96
+ :paragraph_border_on,
97
+ :paragraph_border_color,
98
+ :paragraph_border_tint,
99
+ :paragraph_border_top_line_weight,
100
+ :paragraph_border_left_line_weight,
101
+ :paragraph_border_right_line_weight,
102
+ :paragraph_border_bottom_line_weight,
103
+ :paragraph_border_top_offset,
104
+ :paragraph_border_left_offset,
105
+ :paragraph_border_right_offset,
106
+ :paragraph_border_bottom_offset,
85
107
  keyword_init: true,
86
108
  )
87
109
 
@@ -171,7 +193,8 @@ module Idml
171
193
  rule_above: resolve_attr(style_lookup, psr, :rule_above),
172
194
  rule_above_line_weight: resolve_attr(style_lookup, psr,
173
195
  :rule_above_line_weight),
174
- rule_above_color: resolve_attr(style_lookup, psr, :stroke_color),
196
+ rule_above_color: properties_color(psr, :rule_above_color) ||
197
+ resolve_attr(style_lookup, psr, :stroke_color),
175
198
  rule_above_tint: resolve_attr(style_lookup, psr,
176
199
  :rule_above_tint),
177
200
  rule_above_offset: resolve_attr(style_lookup, psr,
@@ -185,7 +208,8 @@ module Idml
185
208
  rule_below: resolve_attr(style_lookup, psr, :rule_below),
186
209
  rule_below_line_weight: resolve_attr(style_lookup, psr,
187
210
  :rule_below_line_weight),
188
- rule_below_color: resolve_attr(style_lookup, psr, :stroke_color),
211
+ rule_below_color: properties_color(psr, :rule_below_color) ||
212
+ resolve_attr(style_lookup, psr, :stroke_color),
189
213
  rule_below_tint: resolve_attr(style_lookup, psr,
190
214
  :rule_below_tint),
191
215
  rule_below_offset: resolve_attr(style_lookup, psr,
@@ -196,12 +220,70 @@ module Idml
196
220
  :rule_below_right_indent),
197
221
  rule_below_width: resolve_attr(style_lookup, psr,
198
222
  :rule_below_width),
223
+ paragraph_shading_on: resolve_attr(style_lookup, psr,
224
+ :paragraph_shading_on),
225
+ paragraph_shading_color: properties_color(psr,
226
+ :paragraph_shading_color),
227
+ paragraph_shading_tint: resolve_attr(style_lookup, psr,
228
+ :paragraph_shading_tint),
229
+ paragraph_shading_width: resolve_attr(style_lookup, psr,
230
+ :paragraph_shading_width),
231
+ paragraph_shading_left_offset: resolve_attr(
232
+ style_lookup, psr, :paragraph_shading_left_offset
233
+ ),
234
+ paragraph_shading_right_offset: resolve_attr(
235
+ style_lookup, psr, :paragraph_shading_right_offset
236
+ ),
237
+ paragraph_shading_top_offset: resolve_attr(
238
+ style_lookup, psr, :paragraph_shading_top_offset
239
+ ),
240
+ paragraph_shading_bottom_offset: resolve_attr(
241
+ style_lookup, psr, :paragraph_shading_bottom_offset
242
+ ),
243
+ paragraph_border_on: resolve_attr(style_lookup, psr,
244
+ :paragraph_border_on),
245
+ paragraph_border_color: properties_color(psr,
246
+ :paragraph_border_color),
247
+ paragraph_border_tint: resolve_attr(style_lookup, psr,
248
+ :paragraph_border_tint),
249
+ paragraph_border_top_line_weight: resolve_attr(
250
+ style_lookup, psr, :paragraph_border_top_line_weight
251
+ ),
252
+ paragraph_border_left_line_weight: resolve_attr(
253
+ style_lookup, psr, :paragraph_border_left_line_weight
254
+ ),
255
+ paragraph_border_right_line_weight: resolve_attr(
256
+ style_lookup, psr, :paragraph_border_right_line_weight
257
+ ),
258
+ paragraph_border_bottom_line_weight: resolve_attr(
259
+ style_lookup, psr, :paragraph_border_bottom_line_weight
260
+ ),
261
+ paragraph_border_top_offset: resolve_attr(
262
+ style_lookup, psr, :paragraph_border_top_offset
263
+ ),
264
+ paragraph_border_left_offset: resolve_attr(
265
+ style_lookup, psr, :paragraph_border_left_offset
266
+ ),
267
+ paragraph_border_right_offset: resolve_attr(
268
+ style_lookup, psr, :paragraph_border_right_offset
269
+ ),
270
+ paragraph_border_bottom_offset: resolve_attr(
271
+ style_lookup, psr, :paragraph_border_bottom_offset
272
+ ),
199
273
  )
200
274
  prepend_list_marker(paragraph)
201
275
  paragraph
202
276
  end
203
277
  end
204
278
 
279
+ # Reads a color value element from the PSR's Properties (e.g.
280
+ # Properties > ParagraphShadingColor). Returns the color name
281
+ # string, or nil when the element is absent.
282
+ def self.properties_color(psr, element_attr)
283
+ psr.properties.first&.public_send(element_attr)&.value
284
+ end
285
+ private_class_method :properties_color
286
+
205
287
  def self.csr_runs(psr, condition_filter: nil, style_lookup: nil,
206
288
  footnote_counter: nil, footnote_option: nil)
207
289
  counter = footnote_counter || Footnote.counter_for(footnote_option)
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module TextEngine
5
+ # Height measurement for paragraphs — shape + wrap each run and
6
+ # sum the line count × leading. Used where a paragraph's full
7
+ # extent must be known before its lines are laid out (paragraph
8
+ # shading/border rects).
9
+ module Measurement
10
+ DEFAULT_POINT_SIZE = 12.0
11
+
12
+ # Returns the paragraph's rendered height at `wrap_width`:
13
+ # every run's wrapped line count × leading, plus paragraph
14
+ # SpaceBefore / SpaceAfter.
15
+ def self.paragraph_height(paragraph, font, wrap_width)
16
+ lines_height = paragraph.runs.sum do |run|
17
+ size = run.point_size || DEFAULT_POINT_SIZE
18
+ lines = wrapped_lines(run, font, wrap_width, size)
19
+ lines.length *
20
+ TextEngine::VerticalLayout.leading_for(paragraph.auto_leading,
21
+ size)
22
+ end
23
+ lines_height + space_before_after(paragraph)
24
+ end
25
+
26
+ def self.wrapped_lines(run, font, wrap_width, size)
27
+ glyphs = TextEngine::Shaper.shape(text: run.text, font: font,
28
+ size: size)
29
+ TextEngine::LineBreaker.break(glyphs: glyphs,
30
+ frame_width: wrap_width)
31
+ end
32
+ private_class_method :wrapped_lines
33
+
34
+ def self.space_before_after(paragraph)
35
+ (paragraph.space_before || 0) + (paragraph.space_after || 0)
36
+ end
37
+ private_class_method :space_before_after
38
+ end
39
+ end
40
+ end
@@ -6,7 +6,8 @@ module Idml
6
6
  # .ttf/.otf files. IDML-agnostic — works on any styled text.
7
7
  module TextEngine
8
8
  autoload :PdfrbFontMetrics, "#{__dir__}/text_engine/pdfrb_font_metrics"
9
- autoload :Shaper, "#{__dir__}/text_engine/shaper"
9
+ autoload :Shaper, "#{__dir__}/text_engine/shaper"
10
+ autoload :Measurement, "#{__dir__}/text_engine/measurement"
10
11
  autoload :ShapedGlyph, "#{__dir__}/text_engine/shaper"
11
12
  autoload :LineBreaker, "#{__dir__}/text_engine/line_breaker"
12
13
  autoload :Line, "#{__dir__}/text_engine/line_breaker"
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.8.9"
4
+ VERSION = "0.9.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.9
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -407,6 +407,7 @@ files:
407
407
  - lib/idml/elements/transform_attribute_option.rb
408
408
  - lib/idml/elements/transparency_setting.rb
409
409
  - lib/idml/elements/trap_preset.rb
410
+ - lib/idml/elements/typed_value.rb
410
411
  - lib/idml/elements/xml_element.rb
411
412
  - lib/idml/elements/xml_export_map.rb
412
413
  - lib/idml/elements/xml_import_map.rb
@@ -484,6 +485,7 @@ files:
484
485
  - lib/idml/text_engine/cjk_layout.rb
485
486
  - lib/idml/text_engine/justifier.rb
486
487
  - lib/idml/text_engine/line_breaker.rb
488
+ - lib/idml/text_engine/measurement.rb
487
489
  - lib/idml/text_engine/pdfrb_font_metrics.rb
488
490
  - lib/idml/text_engine/shaper.rb
489
491
  - lib/idml/text_engine/vertical_layout.rb