idml 0.7.0 → 0.7.1

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: 0f40610210789018b1fbe0625c1541cfe3611169d0004543891ebb9efd6c74ea
4
- data.tar.gz: 6998d5c66bd56eed147dfb88d60a602be32fee2adb9b9fa9f08413af4cb2103f
3
+ metadata.gz: f43caa091c83d9d8288f22f3913d21760bf5a5dac986f76b72521bb19f5110e0
4
+ data.tar.gz: 4e33ef7a0c119936d16c0eecb687d153e04d9dc29d789511633fb6fff7f1a152
5
5
  SHA512:
6
- metadata.gz: 02c03bf2c81b2ebb81b04ae4b413b25d484adf806b1d8c7c0768e557323bca7c30de46d5174cff27c90e2275f6f89070ac5f981df567cb20867e5c17382b0c42
7
- data.tar.gz: 642103e8e48d2b5b52a4b669d7e09329f5706e4f165f4da8de6df51ef38125686ca75db28de194a974f94cdc9273a193c76a653bf8727b3a16f6b5ac2a9c5f3c
6
+ metadata.gz: 16b0f93a7c8993c5f98ad84c6e186ed61ebcaaabcdda63b26c9d1e4bfadd1c3986bef933d2a832d2b3463ab5b23324778a37e92e62cbeb8ce0876dbc26bebcbe
7
+ data.tar.gz: 9950c7afceda42f377e0d2bef226c4caa346c9d79ac4b3b61dcba99612a321c646eecd6b341be88fd7f8a46c695ecc2449d1bf4d900e4338ed1a4c9f76ea4d45
@@ -1,6 +1,15 @@
1
1
  # TODO PDF 101: Paragraph borders and rules (RuleAbove, RuleBelow, ParagraphBorder)
2
2
 
3
- ## Status: OPENgap identified in 2026-08-08 audit
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.
4
13
 
5
14
  ## Problem
6
15
 
@@ -0,0 +1,50 @@
1
+ # TODO PDF 108: Multi-frame story flow (use StoryThreader)
2
+
3
+ ## Status: OPEN — gap identified in 2026-08-09 audit
4
+
5
+ ## Problem
6
+
7
+ `Render::StoryThreader` exists with specs (`spec/idml/render_layer_threader_spec.rb`).
8
+ It builds text-frame chains from PreviousTextFrame/NextTextFrame
9
+ links. The intent: a story flows across linked frames, with each
10
+ frame getting the lines that fit and overflow going to the next.
11
+
12
+ But `TextFrameRenderer` doesn't use it. The renderer treats each
13
+ frame independently:
14
+ - `chain_head?` filter skips non-head frames (correct — they'd
15
+ double-render)
16
+ - Each chain-head frame renders its entire story from the top
17
+ - Overflow that doesn't fit is silently dropped
18
+
19
+ For real documents with multi-page stories (books, magazines,
20
+ newspapers), this loses text. The PDF shows only what fits in the
21
+ first frame.
22
+
23
+ ## What needs to happen
24
+
25
+ 1. TextFrameRenderer accepts the chain (list of frames) instead
26
+ of one frame at a time.
27
+ 2. For each chain:
28
+ - Lay out story text starting at frame 1's top
29
+ - When text overflows frame 1's bottom, continue at frame 2's top
30
+ - Repeat until either text ends or chain ends
31
+ 3. The PageItemRenderer dispatch needs to know about chains, not
32
+ just per-item rendering.
33
+
34
+ Architecture: the chain context needs to live in RenderContext
35
+ (or a new RenderChainContext). Each frame's render call passes the
36
+ current y cursor + remaining paragraphs/runs/lines, and the next
37
+ frame's render call picks up where the previous left off.
38
+
39
+ ## Acceptance criteria
40
+
41
+ - [ ] Story that overflows frame 1 continues in frame 2.
42
+ - [ ] Hyperlink rects resolve correctly across frames.
43
+ - [ ] Spec: 2 linked TextFrames, story of 50 lines, each frame fits
44
+ 20 lines → frame 1 shows lines 1-20, frame 2 shows 21-40,
45
+ frame 3 (if present) shows 41-50.
46
+
47
+ ## Dependencies
48
+
49
+ - None — independent feature work, but changes the renderer's
50
+ per-frame abstraction significantly.
@@ -0,0 +1,42 @@
1
+ # TODO PDF 109: DRY color tint application (ColorHelper vs CharacterStyle vs ParagraphRules)
2
+
3
+ ## Status: DONE — `ColorHelper.apply_tint(color, tint)` is the single
4
+ canonical tint helper. CharacterStyle and ParagraphRules both delegate
5
+ to it. 7 new specs cover nil tint, tint >= 1.0, RGB/CMYK scaling,
6
+ tint 0.0, unknown models, nil input.
7
+
8
+ ## Problem
9
+
10
+ Color tint (scaling a color's components toward zero) is implemented
11
+ in three places with slightly different shapes:
12
+
13
+ 1. `Render::CharacterStyle.apply_tint(color, tint)` — used for
14
+ text fill color.
15
+ 2. `Render::ParagraphRules.scale_color(color, tint_value)` — used
16
+ for paragraph rule colors.
17
+ 3. (Implicit) `Render::ColorResolver` resolves tints on its own
18
+ when reading ColorValue, before returning the color hash.
19
+
20
+ The first two are essentially the same logic with different method
21
+ names. The third means a `ColorResolver.resolve` result might or
22
+ might not have tint already applied — callers can't tell.
23
+
24
+ ## What needs to happen
25
+
26
+ 1. Pick one canonical tint helper. Either:
27
+ - Move to `ColorHelper.apply_tint(color, tint)`, or
28
+ - Create `Render::ColorTint.apply(color, tint)`.
29
+ 2. CharacterStyle and ParagraphRules call the canonical helper.
30
+ 3. ColorResolver documents whether it applies tint or returns raw
31
+ colors (today it appears to apply tint internally).
32
+
33
+ ## Acceptance criteria
34
+
35
+ - [ ] Single tint helper in one place.
36
+ - [ ] CharacterStyle and ParagraphRules both delegate to it.
37
+ - [ ] ColorResolver's tint behavior is documented.
38
+ - [ ] Spec coverage for tint 0.0, 0.5, 1.0 on RGB and CMYK colors.
39
+
40
+ ## Dependencies
41
+
42
+ - None — pure refactor.
@@ -0,0 +1,37 @@
1
+ # TODO PDF 110: Conditional text (AppliedConditions)
2
+
3
+ ## Status: OPEN — gap identified in 2026-08-09 audit
4
+
5
+ ## Problem
6
+
7
+ PSR and CSR both carry an `AppliedConditions` attribute (a list of
8
+ condition Self IDs). In InDesign, conditions let authors mark text
9
+ as conditional (e.g., "show only in instructor edition", "draft
10
+ review comments"). Each `<Condition>` in Resources/Styles.xml
11
+ declares whether it's visible, its highlight color, etc.
12
+
13
+ Today the renderer ignores `AppliedConditions` — all conditional
14
+ text renders regardless of condition visibility. For documents that
15
+ use conditions heavily (textbooks, legal, regulatory), this means
16
+ the PDF always shows every variant instead of the chosen subset.
17
+
18
+ ## What needs to happen
19
+
20
+ 1. Verify `Elements::Condition` exists in the model (Resources/Styles.rnc).
21
+ 2. Add `Render::ConditionFilter` that reads visible conditions from
22
+ Resources/Styles.xml.
23
+ 3. StyleResolver filters PSRs/CSRs whose AppliedConditions reference
24
+ a hidden condition.
25
+ 4. Spec: PSR with condition A (hidden) doesn't render; condition B
26
+ (visible) renders.
27
+
28
+ ## Acceptance criteria
29
+
30
+ - [ ] Condition model from RNC.
31
+ - [ ] ConditionFilter.from_styles(graphic_or_styles).
32
+ - [ ] StyleResolver.extract_paragraphs takes optional ConditionFilter.
33
+ - [ ] Hidden-condition runs filtered out.
34
+
35
+ ## Dependencies
36
+
37
+ - None — pure feature work on existing models.
@@ -0,0 +1,40 @@
1
+ # TODO PDF 111: Footnote support
2
+
3
+ ## Status: OPEN — gap identified in 2026-08-09 audit
4
+
5
+ ## Problem
6
+
7
+ IDML has full footnote support:
8
+ - `<Footnote>` elements in Stories (inline footnote markers + text)
9
+ - `<FootnoteOption>` on PSR (numbering rules, layout)
10
+ - `TextFramePreference.FootnotesEnableOverrides`,
11
+ `FootnotesSpanAcrossColumns`, `FootnotesMinimumSpacing`,
12
+ `FootnotesSpaceBetween` — frame-level footnote layout
13
+ - `<TextFrameFootnoteOptionsObject>` — per-frame footnote config
14
+
15
+ The renderer doesn't model footnote content or lay it out.
16
+ Documents with footnotes (academic, legal, technical) currently
17
+ lose footnote text in the PDF.
18
+
19
+ ## What needs to happen
20
+
21
+ 1. Model `<Footnote>` element (story-embedded footnote text +
22
+ reference marker).
23
+ 2. Story parsing extracts footnotes alongside main text.
24
+ 3. Footnote text renders at the bottom of the page that contains
25
+ the reference marker.
26
+ 4. Footnote numbering rules from FootnoteOption honored.
27
+ 5. Continuous footnotes flow across pages.
28
+
29
+ This is a large feature — defer until needed by a real fixture.
30
+
31
+ ## Acceptance criteria
32
+
33
+ - [ ] Footnote text rendered at bottom of containing page.
34
+ - [ ] Footnote marker in body text links to footnote text.
35
+ - [ ] Numbering restarts per document section per FootnoteOption.
36
+
37
+ ## Dependencies
38
+
39
+ - TODO 108 (multi-frame story flow) — footnotes need to know which
40
+ page a reference appears on.
@@ -20,7 +20,10 @@ module Idml
20
20
  # Yields with the run's fill color applied, then draws underline
21
21
  # and/or strike-through rules above/below the text baseline.
22
22
  # `x`, `y`, `width`, `size` describe the line's geometry so the
23
- # rules can be positioned correctly.
23
+ # rules can be positioned correctly. The block's emitted text
24
+ # gets the run's character-level adjustments baked in via
25
+ # `text_kwargs` (caller passes the canvas.text kwargs in, and
26
+ # CharacterStyle adds tracking/scale on top).
24
27
  def self.apply(canvas, run, context, x:, y:, width:, size:)
25
28
  apply_fill_color(canvas, run, context)
26
29
  yield
@@ -28,6 +31,42 @@ module Idml
28
31
  draw_strike_through(canvas, run, x, y, width, size)
29
32
  end
30
33
 
34
+ # Returns the canvas.text kwargs augmented with the run's
35
+ # Tracking (PDF char_spacing). IDML Tracking is in points;
36
+ # pdfrb's char_spacing is also in text-space units, so the
37
+ # mapping is 1:1.
38
+ def self.text_kwargs(run, base_kwargs)
39
+ return base_kwargs unless run.tracking
40
+
41
+ base_kwargs.merge(char_spacing: run.tracking.to_f)
42
+ end
43
+
44
+ # Yields within a transformed coordinate space when the run
45
+ # declares glyph scaling. IDML HorizontalScale/VerticalScale
46
+ # are percentages (100 = no scaling). When both are 100 / nil,
47
+ # yields without transformation (no graphics-state push).
48
+ def self.with_glyph_scaling(canvas, run, &)
49
+ sx = scale_factor(run.horizontal_scale)
50
+ sy = scale_factor(run.vertical_scale)
51
+ return yield if scaling_identity?(sx, sy)
52
+
53
+ canvas.scale(sx, sy, &)
54
+ end
55
+
56
+ def self.scaling_identity?(sx, sy)
57
+ (sx - 1.0).abs < FLOAT_EPSILON && (sy - 1.0).abs < FLOAT_EPSILON
58
+ end
59
+
60
+ FLOAT_EPSILON = 1e-9
61
+ private_constant :FLOAT_EPSILON
62
+
63
+ # Returns the run's baseline shift (in PDF units). Positive
64
+ # values shift up; negative shift down. Used to offset the
65
+ # text's `at: y` for CSR's BaselineShift.
66
+ def self.baseline_offset(run)
67
+ run.baseline_shift&.nonzero? ? run.baseline_shift.to_f : 0.0
68
+ end
69
+
31
70
  # Transforms the text per the CSR's Capitalization attribute.
32
71
  # AllCaps/SmallCaps → uppercase (true small-caps would require
33
72
  # an OpenType feature; deferred). Other values pass through.
@@ -55,10 +94,19 @@ module Idml
55
94
  color = context.color_resolver&.resolve(run.fill_color)
56
95
  return unless color
57
96
 
58
- canvas.fill_color(ColorHelper.to_canvas(color))
97
+ tinted = ColorHelper.apply_tint(color, run.fill_tint)
98
+ canvas.fill_color(ColorHelper.to_canvas(tinted))
59
99
  end
60
100
  private_class_method :apply_fill_color
61
101
 
102
+ def self.scale_factor(declared)
103
+ return 1.0 unless declared
104
+ return 1.0 if declared <= 0
105
+
106
+ declared.to_f / 100.0
107
+ end
108
+ private_class_method :scale_factor
109
+
62
110
  def self.draw_underline(canvas, run, x, y, width, size)
63
111
  return unless run.underline
64
112
 
@@ -1,9 +1,10 @@
1
- # frozen_string_literal: true
1
+ # frozen_string: true
2
2
 
3
3
  module Idml
4
4
  module Render
5
- # Converts IDML color hashes to pdfrb Canvas color arrays.
6
- # Centralizes the mapping so all renderers use a consistent format.
5
+ # Converts IDML color hashes to pdfrb Canvas color arrays and
6
+ # applies tint scaling. Centralizes the mapping so all renderers
7
+ # use a consistent format.
7
8
  module ColorHelper
8
9
  module_function
9
10
 
@@ -18,6 +19,29 @@ module Idml
18
19
  when :cmyk then [:cmyk, color[:c], color[:m], color[:y], color[:k]]
19
20
  end
20
21
  end
22
+
23
+ # Scales a color's components by `tint` (1.0 = full strength,
24
+ # 0.5 = halved). Tint is the IDML mechanism for lightening a
25
+ # color without defining a new one. Returns the original color
26
+ # unchanged when tint is nil or >= 1.0. Single canonical
27
+ # implementation — CharacterStyle, ParagraphRules, and any
28
+ # future caller should all delegate here.
29
+ def apply_tint(color, tint)
30
+ return color unless color
31
+ return color unless tint
32
+ return color if tint >= 1.0
33
+
34
+ case color[:model]
35
+ when :rgb
36
+ { model: :rgb, r: color[:r] * tint, g: color[:g] * tint,
37
+ b: color[:b] * tint }
38
+ when :cmyk
39
+ { model: :cmyk, c: color[:c] * tint, m: color[:m] * tint,
40
+ y: color[:y] * tint, k: color[:k] * tint }
41
+ else
42
+ color
43
+ end
44
+ end
21
45
  end
22
46
  end
23
47
  end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Emits paragraph-level rules (RuleAbove / RuleBelow) on a pdfrb
6
+ # canvas. Centralizes the IDML → PDF mapping for the paragraph
7
+ # rule attributes carried by `StyleResolver::Paragraph`.
8
+ #
9
+ # Each rule is a horizontal stroke above (RuleAbove) or below
10
+ # (RuleBelow) the paragraph block. The rule's weight, color,
11
+ # tint, offset, and indent come from the PSR's RuleAbove* /
12
+ # RuleBelow* attributes; defaults approximate InDesign behavior
13
+ # when an attribute is missing.
14
+ #
15
+ # Used by `TextFrameRenderer` so paragraph rules live in one
16
+ # place (DRY).
17
+ module ParagraphRules
18
+ DEFAULT_LINE_WEIGHT = 0.5
19
+ DEFAULT_TINT = 1.0
20
+ DEFAULT_OFFSET = 0.0
21
+
22
+ # Emits RuleAbove if `paragraph.rule_above` is true. The rule
23
+ # sits above the paragraph's first line. `top_y` is the y of
24
+ # the paragraph's top edge (before SpaceBefore is applied);
25
+ # `frame_left` / `frame_right` are the column's text bounds.
26
+ def self.emit_rule_above(canvas, paragraph, context, top_y,
27
+ frame_left, frame_right)
28
+ return unless paragraph.rule_above
29
+
30
+ emit_rule(canvas, paragraph, context, top_y, frame_left,
31
+ frame_right,
32
+ weight: paragraph.rule_above_line_weight,
33
+ color: paragraph.rule_above_color,
34
+ tint: paragraph.rule_above_tint,
35
+ offset: paragraph.rule_above_offset,
36
+ left_indent: paragraph.rule_above_left_indent,
37
+ right_indent: paragraph.rule_above_right_indent,
38
+ width_mode: paragraph.rule_above_width,
39
+ position: :above)
40
+ end
41
+
42
+ # Emits RuleBelow if `paragraph.rule_below` is true. The rule
43
+ # sits below the paragraph's last line. `bottom_y` is the y of
44
+ # the paragraph's bottom edge (after SpaceAfter is applied).
45
+ def self.emit_rule_below(canvas, paragraph, context, bottom_y,
46
+ frame_left, frame_right)
47
+ return unless paragraph.rule_below
48
+
49
+ emit_rule(canvas, paragraph, context, bottom_y, frame_left,
50
+ frame_right,
51
+ weight: paragraph.rule_below_line_weight,
52
+ color: paragraph.rule_below_color,
53
+ tint: paragraph.rule_below_tint,
54
+ offset: paragraph.rule_below_offset,
55
+ left_indent: paragraph.rule_below_left_indent,
56
+ right_indent: paragraph.rule_below_right_indent,
57
+ width_mode: paragraph.rule_below_width,
58
+ position: :below)
59
+ end
60
+
61
+ def self.emit_rule(canvas, paragraph, context, anchor_y,
62
+ frame_left, frame_right,
63
+ weight:, color:, tint:, offset:,
64
+ left_indent:, right_indent:, width_mode:,
65
+ position:)
66
+ thickness = (weight || DEFAULT_LINE_WEIGHT).to_f
67
+ return unless thickness.positive?
68
+
69
+ tint_value = (tint || DEFAULT_TINT).to_f
70
+ return unless tint_value.positive?
71
+
72
+ offset_value = (offset || DEFAULT_OFFSET).to_f
73
+ x1, x2 = rule_extents(frame_left, frame_right, left_indent,
74
+ right_indent, width_mode, paragraph)
75
+ y = rule_y(anchor_y, offset_value, position)
76
+
77
+ apply_rule_color(canvas, color, tint_value, context)
78
+ canvas.line_width = thickness
79
+ canvas.move_to(x1, y)
80
+ canvas.line_to(x2, y)
81
+ canvas.stroke
82
+ end
83
+ private_class_method :emit_rule
84
+
85
+ # Returns `[x1, x2]` for the rule. Width mode "Text" indents
86
+ # the rule by the paragraph's left/right indent (matches the
87
+ # text bounds); "Column" (default) spans the full column.
88
+ def self.rule_extents(frame_left, frame_right, rule_left_indent,
89
+ rule_right_indent, width_mode, paragraph)
90
+ left = frame_left
91
+ right = frame_right
92
+ if width_mode == "Text"
93
+ left += (paragraph.left_indent || 0).to_f
94
+ right -= (paragraph.right_indent || 0).to_f
95
+ end
96
+ left += (rule_left_indent || 0).to_f
97
+ right -= (rule_right_indent || 0).to_f
98
+ [left, right]
99
+ end
100
+ private_class_method :rule_extents
101
+
102
+ def self.rule_y(anchor_y, offset, position)
103
+ position == :above ? anchor_y + offset : anchor_y - offset
104
+ end
105
+ private_class_method :rule_y
106
+
107
+ def self.apply_rule_color(canvas, color_name, tint_value, context)
108
+ color = resolve_color(color_name, tint_value, context)
109
+ canvas.stroke_color(color)
110
+ end
111
+ private_class_method :apply_rule_color
112
+
113
+ def self.resolve_color(color_name, tint_value, context)
114
+ return [:gray, 1.0 * tint_value] unless color_name
115
+ return [:gray, 1.0 * tint_value] if color_name == "Color/None"
116
+
117
+ color = context&.color_resolver&.resolve(color_name)
118
+ return [:gray, 1.0 * tint_value] unless color
119
+
120
+ ColorHelper.apply_tint(color, tint_value)
121
+ end
122
+ private_class_method :resolve_color
123
+ end
124
+ end
125
+ end
@@ -137,14 +137,37 @@ module Idml
137
137
  layout_frame, paragraph.right_indent || 0
138
138
  )
139
139
  para_attrs = paragraph_attrs(paragraph)
140
+ frame_left, frame_right = paragraph_frame_extents(layout_frame)
141
+
142
+ # RuleAbove sits at the paragraph's top edge (before
143
+ # space_before is subtracted), so emit it first using the
144
+ # incoming cursor_y as the anchor.
145
+ ParagraphRules.emit_rule_above(canvas, paragraph, context,
146
+ cursor_y, frame_left, frame_right)
147
+
140
148
  cursor_y, char_cursor = render_runs(
141
149
  canvas, paragraph, context, layout_frame, font,
142
150
  wrap_width, cursor_y, bottom_limit, char_cursor, para_attrs
143
151
  )
144
- [cursor_y - para_attrs[:space_after], char_cursor]
152
+ after_y = cursor_y - para_attrs[:space_after]
153
+
154
+ # RuleBelow sits at the paragraph's bottom edge (after the
155
+ # last line + space_after), so emit it last using after_y
156
+ # as the anchor.
157
+ ParagraphRules.emit_rule_below(canvas, paragraph, context,
158
+ after_y, frame_left, frame_right)
159
+ [after_y, char_cursor]
145
160
  end
146
161
  private_class_method :render_paragraph
147
162
 
163
+ def self.paragraph_frame_extents(layout_frame)
164
+ inset_left = layout_frame.inset_left || 0
165
+ inset_right = layout_frame.inset_right || 0
166
+ [layout_frame.x + inset_left,
167
+ layout_frame.x + layout_frame.width - inset_right]
168
+ end
169
+ private_class_method :paragraph_frame_extents
170
+
148
171
  def self.paragraph_attrs(paragraph)
149
172
  {
150
173
  space_before: paragraph.space_before || 0,
@@ -223,23 +246,32 @@ module Idml
223
246
  private_class_method :layout_run
224
247
 
225
248
  # Emits one positioned line: applies character-level styling
226
- # (fill color, capitalization, position) via CharacterStyle,
227
- # then draws underline/strike-through rules after the text.
249
+ # (fill color + tint, capitalization, position, tracking,
250
+ # glyph scaling, baseline shift) via CharacterStyle, then
251
+ # draws underline/strike-through rules after the text.
228
252
  def self.emit_line(canvas, line, run, context, size)
229
253
  text = CharacterStyle.transform_text(line_text(line),
230
254
  run.capitalization)
231
- scaled_size, baseline_offset = CharacterStyle.position_scale(
255
+ scaled_size, position_offset = CharacterStyle.position_scale(
232
256
  run.position, size
233
257
  )
258
+ baseline_offset = CharacterStyle.baseline_offset(run) +
259
+ position_offset
260
+ text_at = [line.x, line.y + baseline_offset]
261
+ text_kwargs = CharacterStyle.text_kwargs(
262
+ run,
263
+ {
264
+ at: text_at,
265
+ font: font_for_run(run, context),
266
+ size: scaled_size,
267
+ },
268
+ )
234
269
  CharacterStyle.apply(canvas, run, context,
235
270
  x: line.x, y: line.y + baseline_offset,
236
271
  width: line.width, size: scaled_size) do
237
- canvas.text(
238
- text,
239
- at: [line.x, line.y + baseline_offset],
240
- font: font_for_run(run, context),
241
- size: scaled_size,
242
- )
272
+ CharacterStyle.with_glyph_scaling(canvas, run) do
273
+ canvas.text(text, **text_kwargs)
274
+ end
243
275
  end
244
276
  end
245
277
  private_class_method :emit_line
@@ -54,6 +54,24 @@ module Idml
54
54
  :left_indent,
55
55
  :right_indent,
56
56
  :auto_leading,
57
+ # RuleAbove / RuleBelow — paragraph rules (horizontal lines
58
+ # above/below the paragraph block). Driven by PSR attributes.
59
+ :rule_above,
60
+ :rule_above_line_weight,
61
+ :rule_above_color,
62
+ :rule_above_tint,
63
+ :rule_above_offset,
64
+ :rule_above_left_indent,
65
+ :rule_above_right_indent,
66
+ :rule_above_width,
67
+ :rule_below,
68
+ :rule_below_line_weight,
69
+ :rule_below_color,
70
+ :rule_below_tint,
71
+ :rule_below_offset,
72
+ :rule_below_left_indent,
73
+ :rule_below_right_indent,
74
+ :rule_below_width,
57
75
  keyword_init: true,
58
76
  )
59
77
 
@@ -102,6 +120,22 @@ module Idml
102
120
  left_indent: psr.left_indent,
103
121
  right_indent: psr.right_indent,
104
122
  auto_leading: psr.auto_leading,
123
+ rule_above: psr.rule_above,
124
+ rule_above_line_weight: psr.rule_above_line_weight,
125
+ rule_above_color: psr.stroke_color,
126
+ rule_above_tint: psr.rule_above_tint,
127
+ rule_above_offset: psr.rule_above_offset,
128
+ rule_above_left_indent: psr.rule_above_left_indent,
129
+ rule_above_right_indent: psr.rule_above_right_indent,
130
+ rule_above_width: psr.rule_above_width,
131
+ rule_below: psr.rule_below,
132
+ rule_below_line_weight: psr.rule_below_line_weight,
133
+ rule_below_color: psr.stroke_color,
134
+ rule_below_tint: psr.rule_below_tint,
135
+ rule_below_offset: psr.rule_below_offset,
136
+ rule_below_left_indent: psr.rule_below_left_indent,
137
+ rule_below_right_indent: psr.rule_below_right_indent,
138
+ rule_below_width: psr.rule_below_width,
105
139
  )
106
140
  end
107
141
  end
data/lib/idml/render.rb CHANGED
@@ -14,6 +14,7 @@ module Idml
14
14
  autoload :Blending, "#{__dir__}/render/blending"
15
15
  autoload :StrokeStyle, "#{__dir__}/render/stroke_style"
16
16
  autoload :CharacterStyle, "#{__dir__}/render/character_style"
17
+ autoload :ParagraphRules, "#{__dir__}/render/paragraph_rules"
17
18
  autoload :LayerFilter, "#{__dir__}/render/layer_filter"
18
19
  autoload :Placement, "#{__dir__}/render/placement"
19
20
  autoload :ImageCollector, "#{__dir__}/render/image_collector"
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.7.0"
4
+ VERSION = "0.7.1"
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.7.0
4
+ version: 0.7.1
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-08 00:00:00.000000000 Z
11
+ date: 2026-08-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -163,7 +163,11 @@ files:
163
163
  - TODO.pdf/105-whole-package-round-trip.md
164
164
  - TODO.pdf/106-page-item-type-coverage.md
165
165
  - TODO.pdf/107-image-rendering-unification.md
166
+ - TODO.pdf/108-multi-frame-story-flow.md
167
+ - TODO.pdf/109-dry-color-tint.md
166
168
  - TODO.pdf/11-font-embedding.md
169
+ - TODO.pdf/110-conditional-text.md
170
+ - TODO.pdf/111-footnote-support.md
167
171
  - TODO.pdf/12-idml-to-pdf-pipeline.md
168
172
  - TODO.pdf/13-cjk-text-layout.md
169
173
  - TODO.pdf/14-page-item-element-models.md
@@ -435,6 +439,7 @@ files:
435
439
  - lib/idml/render/layer_filter.rb
436
440
  - lib/idml/render/metadata_builder.rb
437
441
  - lib/idml/render/page_item_renderer.rb
442
+ - lib/idml/render/paragraph_rules.rb
438
443
  - lib/idml/render/pdfa_packet.rb
439
444
  - lib/idml/render/pdfrb_writer.rb
440
445
  - lib/idml/render/pipeline.rb