idml 0.7.5 → 0.8.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: a713a0a63c6af7b6090c9a6260467db40ee9a7c497f37561dccf2526ceadaa71
4
- data.tar.gz: a6695a7318b96ed6664e15b9e2a99e40d2da6ef05139154ba7ccfab73e3eeecf
3
+ metadata.gz: bdf6a3aba5bbe199af39e5addabae082b795dfef1583dd14f33a9d7415086ead
4
+ data.tar.gz: eb1a78bd1a11fdc7500543b903b4d927114a96a70fea97a7e8b4c62db813451e
5
5
  SHA512:
6
- metadata.gz: 92e8aff4adbb8935c2e3ec7b7ead236458ec2f7171a2923eee9de63b4b4f0ed96d95f7f39c9c184d97643b0aa9096e3aa1b40e890610e82d514bb36b17c6f14b
7
- data.tar.gz: 88c470bb2d55962f57a950fe6c1ef526d34ce36d7fafe38ed045d4f7019821586a9045d1f027e28eb304a1d0a57da9f157256603a83e48e873decd3f2620554c
6
+ metadata.gz: e47ca1b121cd27336a516f8b0cb863d5ad03edbdc43455bfa3efcb41807865a1dae4aec35ccbf380e003dc383d9f0d3704b42e23efa0d56c6925f316b68030c1
7
+ data.tar.gz: 3ac803ec48c5a0e2a2e3eec6f17845993a2a3194ddb8046bd5e8ea7225fcd8709abef22817684c12c51814b26a6ca9d3e07f2b4a735b519b9cb3d99e9eaaec5c
@@ -1,6 +1,9 @@
1
1
  # TODO PDF 116: AppliedParagraphStyle / AppliedCharacterStyle resolution from Resources/Styles.xml
2
2
 
3
- ## Status: OPENcritical gap identified in 2026-08-10 audit
3
+ ## Status: DONE`Render::StyleLookup` indexes ParagraphStyle and
4
+ CharacterStyle from Resources/Styles.xml by Self. StyleResolver
5
+ uses it to fill in formatting attributes that PSR/CSR elements don't
6
+ declare inline. PSR/CSR inline attributes override style values.
4
7
 
5
8
  ## Problem
6
9
 
@@ -39,6 +39,7 @@ module Idml
39
39
  position_tracker = PositionTracker.new
40
40
  layer_filter = LayerFilter.from_designmap(@package.designmap)
41
41
  condition_filter = ConditionFilter.from_designmap(@package.designmap)
42
+ style_lookup = StyleLookup.from_package(@package)
42
43
  font_ref_resolver = FontReferenceResolver.build(@package)
43
44
  font_setup = FontSetup.new(package: @package,
44
45
  font_search_paths: @font_search_paths)
@@ -52,7 +53,7 @@ module Idml
52
53
  font_ref_resolver, font_resource,
53
54
  font_metrics, font_map, structure,
54
55
  position_tracker, condition_filter,
55
- page_index)
56
+ style_lookup, page_index)
56
57
  end
57
58
 
58
59
  structure.flush(writer)
@@ -92,14 +93,16 @@ module Idml
92
93
 
93
94
  def render_spread(writer, spread, layer_filter, font_ref_resolver,
94
95
  font_resource, font_metrics, font_map, structure,
95
- position_tracker, condition_filter, page_offset)
96
+ position_tracker, condition_filter, style_lookup,
97
+ page_offset)
96
98
  pages = spread.spread.flat_map(&:page)
97
99
  image_refs = ImageCollector.new(writer: writer,
98
100
  base_dir: File.dirname(@package.path),
99
101
  page_height: DEFAULT_HEIGHT).collect(spread)
100
102
  renderer = build_renderer(layer_filter, font_ref_resolver,
101
103
  font_resource, font_metrics, font_map,
102
- structure, position_tracker, condition_filter)
104
+ structure, position_tracker, condition_filter,
105
+ style_lookup)
103
106
  current = page_offset
104
107
 
105
108
  pages.each do |page|
@@ -129,7 +132,7 @@ module Idml
129
132
 
130
133
  def build_renderer(layer_filter, font_ref_resolver, font_resource,
131
134
  font_metrics, font_map, structure, position_tracker,
132
- condition_filter)
135
+ condition_filter, style_lookup)
133
136
  SpreadRenderer.new(
134
137
  font_metrics: font_metrics,
135
138
  font_ps_name: font_resource,
@@ -140,6 +143,7 @@ module Idml
140
143
  structure: structure,
141
144
  position_tracker: position_tracker,
142
145
  condition_filter: condition_filter,
146
+ style_lookup: style_lookup,
143
147
  )
144
148
  end
145
149
 
@@ -22,6 +22,7 @@ module Idml
22
22
  :position_tracker,
23
23
  :chain_controller,
24
24
  :condition_filter,
25
+ :style_lookup,
25
26
  keyword_init: true,
26
27
  )
27
28
  end
@@ -71,7 +71,8 @@ module Idml
71
71
 
72
72
  def self.fresh_state(story, context)
73
73
  paragraphs = StyleResolver.extract_paragraphs(
74
- story, condition_filter: context.condition_filter
74
+ story, condition_filter: context.condition_filter,
75
+ style_lookup: context.style_lookup
75
76
  )
76
77
  return nil if paragraphs.empty?
77
78
 
@@ -8,7 +8,7 @@ module Idml
8
8
  package: nil, layer_filter: LayerFilter::EXCLUDE_NONE,
9
9
  font_ref_resolver: nil, structure: nil,
10
10
  position_tracker: nil, font_map: {},
11
- condition_filter: nil)
11
+ condition_filter: nil, style_lookup: nil)
12
12
  @font_metrics = font_metrics
13
13
  @font_ps_name = font_ps_name
14
14
  @package = package
@@ -18,6 +18,7 @@ module Idml
18
18
  @position_tracker = position_tracker
19
19
  @font_map = font_map
20
20
  @condition_filter = condition_filter
21
+ @style_lookup = style_lookup
21
22
  end
22
23
 
23
24
  def render(canvas, spread, page_width:, page_height:, image_refs: [],
@@ -37,6 +38,7 @@ module Idml
37
38
  position_tracker: @position_tracker,
38
39
  chain_controller: StoryChainController.new,
39
40
  condition_filter: @condition_filter,
41
+ style_lookup: @style_lookup,
40
42
  }
41
43
 
42
44
  canvas.save_graphics_state do
@@ -0,0 +1,78 @@
1
+ # frozen_string: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Resolves `AppliedParagraphStyle` and `AppliedCharacterStyle`
6
+ # references to their style definitions in Resources/Styles.xml.
7
+ # Built once per render pass from `Parts::Style`; StyleResolver
8
+ # consults it to fill in formatting attributes that the PSR/CSR
9
+ # element doesn't declare inline.
10
+ #
11
+ # Merge semantics: PSR/CSR inline attributes override style
12
+ # attributes. When the PSR/CSR declares a value (non-nil), that
13
+ # wins. When it doesn't, the style definition's value is used
14
+ # as a default. When neither declares a value, the attribute
15
+ # remains nil and the renderer applies its own default.
16
+ class StyleLookup
17
+ def self.from_package(package)
18
+ styles = package&.style
19
+ return new({}, {}) unless styles
20
+
21
+ para_styles = index_styles(styles.paragraph_style)
22
+ char_styles = index_styles(styles.character_style)
23
+ new(para_styles, char_styles)
24
+ end
25
+
26
+ def initialize(para_styles, char_styles)
27
+ @para_styles = para_styles
28
+ @char_styles = char_styles
29
+ end
30
+
31
+ # Returns the ParagraphStyle referenced by `self_ref`, or nil.
32
+ # `self_ref` is the PSR's AppliedParagraphStyle value (e.g.,
33
+ # "ParagraphStyle/$ID/NormalParagraphStyle").
34
+ def paragraph_style(self_ref)
35
+ return nil unless self_ref
36
+
37
+ @para_styles[self_ref]
38
+ end
39
+
40
+ # Returns the CharacterStyle referenced by `self_ref`, or nil.
41
+ def character_style(self_ref)
42
+ return nil unless self_ref
43
+
44
+ @char_styles[self_ref]
45
+ end
46
+
47
+ # Returns the value of `attr_name` from the PSR, or from the
48
+ # referenced ParagraphStyle when the PSR doesn't declare it.
49
+ # Uses `public_send` (not `send`) to stay within public API
50
+ # boundaries — the attribute readers generated by lutaml-model
51
+ # are public.
52
+ def resolve_para_attr(psr, attr_name)
53
+ psr_value = psr.public_send(attr_name)
54
+ return psr_value unless psr_value.nil?
55
+
56
+ style = paragraph_style(psr.applied_paragraph_style)
57
+ style&.public_send(attr_name)
58
+ end
59
+
60
+ # Returns the value of `attr_name` from the CSR, or from the
61
+ # referenced CharacterStyle when the CSR doesn't declare it.
62
+ def resolve_char_attr(csr, attr_name)
63
+ csr_value = csr.public_send(attr_name)
64
+ return csr_value unless csr_value.nil?
65
+
66
+ style = character_style(csr.applied_character_style)
67
+ style&.public_send(attr_name)
68
+ end
69
+
70
+ def self.index_styles(collection)
71
+ collection.each_with_object({}) do |style, hash|
72
+ hash[style.self_attr] = style if style.self_attr
73
+ end
74
+ end
75
+ private_class_method :index_styles
76
+ end
77
+ end
78
+ end
@@ -110,53 +110,74 @@ module Idml
110
110
  # its runs and carries the paragraph-level attributes from the
111
111
  # owning PSR. Empty paragraphs (no runs after filtering) are
112
112
  # skipped.
113
- def self.extract_paragraphs(story, condition_filter: nil)
113
+ def self.extract_paragraphs(story, condition_filter: nil,
114
+ style_lookup: nil)
114
115
  return [] unless story&.inner
115
116
 
116
117
  story.inner.paragraph_style_range.filter_map do |psr|
117
118
  next unless condition_visible?(psr, condition_filter)
118
119
 
119
- runs = csr_runs(psr, condition_filter: condition_filter)
120
+ runs = csr_runs(psr, condition_filter: condition_filter,
121
+ style_lookup: style_lookup)
120
122
  next if runs.empty?
121
123
 
122
124
  paragraph = Paragraph.new(
123
125
  runs: runs,
124
126
  alignment: runs.first.alignment,
125
- space_before: psr.space_before,
126
- space_after: psr.space_after,
127
- first_line_indent: psr.first_line_indent,
128
- left_indent: psr.left_indent,
129
- right_indent: psr.right_indent,
130
- auto_leading: psr.auto_leading,
131
- drop_cap_lines: psr.drop_cap_lines,
132
- drop_cap_characters: psr.drop_cap_characters,
133
- bullets_and_numbering_list_type: psr.bullets_and_numbering_list_type,
134
- bullet_character_value: psr.bullet_character_value,
135
- bullets_text_after: psr.bullets_text_after,
136
- numbering_expression: psr.numbering_expression,
137
- rule_above: psr.rule_above,
138
- rule_above_line_weight: psr.rule_above_line_weight,
139
- rule_above_color: psr.stroke_color,
140
- rule_above_tint: psr.rule_above_tint,
141
- rule_above_offset: psr.rule_above_offset,
142
- rule_above_left_indent: psr.rule_above_left_indent,
143
- rule_above_right_indent: psr.rule_above_right_indent,
144
- rule_above_width: psr.rule_above_width,
145
- rule_below: psr.rule_below,
146
- rule_below_line_weight: psr.rule_below_line_weight,
147
- rule_below_color: psr.stroke_color,
148
- rule_below_tint: psr.rule_below_tint,
149
- rule_below_offset: psr.rule_below_offset,
150
- rule_below_left_indent: psr.rule_below_left_indent,
151
- rule_below_right_indent: psr.rule_below_right_indent,
152
- rule_below_width: psr.rule_below_width,
127
+ space_before: resolve_attr(style_lookup, psr, :space_before),
128
+ space_after: resolve_attr(style_lookup, psr, :space_after),
129
+ first_line_indent: resolve_attr(style_lookup, psr,
130
+ :first_line_indent),
131
+ left_indent: resolve_attr(style_lookup, psr, :left_indent),
132
+ right_indent: resolve_attr(style_lookup, psr, :right_indent),
133
+ auto_leading: resolve_attr(style_lookup, psr, :auto_leading),
134
+ drop_cap_lines: resolve_attr(style_lookup, psr, :drop_cap_lines),
135
+ drop_cap_characters: resolve_attr(style_lookup, psr,
136
+ :drop_cap_characters),
137
+ bullets_and_numbering_list_type: resolve_attr(
138
+ style_lookup, psr, :bullets_and_numbering_list_type
139
+ ),
140
+ bullet_character_value: resolve_attr(style_lookup, psr,
141
+ :bullet_character_value),
142
+ bullets_text_after: resolve_attr(style_lookup, psr,
143
+ :bullets_text_after),
144
+ numbering_expression: resolve_attr(style_lookup, psr,
145
+ :numbering_expression),
146
+ rule_above: resolve_attr(style_lookup, psr, :rule_above),
147
+ rule_above_line_weight: resolve_attr(style_lookup, psr,
148
+ :rule_above_line_weight),
149
+ rule_above_color: resolve_attr(style_lookup, psr, :stroke_color),
150
+ rule_above_tint: resolve_attr(style_lookup, psr,
151
+ :rule_above_tint),
152
+ rule_above_offset: resolve_attr(style_lookup, psr,
153
+ :rule_above_offset),
154
+ rule_above_left_indent: resolve_attr(style_lookup, psr,
155
+ :rule_above_left_indent),
156
+ rule_above_right_indent: resolve_attr(style_lookup, psr,
157
+ :rule_above_right_indent),
158
+ rule_above_width: resolve_attr(style_lookup, psr,
159
+ :rule_above_width),
160
+ rule_below: resolve_attr(style_lookup, psr, :rule_below),
161
+ rule_below_line_weight: resolve_attr(style_lookup, psr,
162
+ :rule_below_line_weight),
163
+ rule_below_color: resolve_attr(style_lookup, psr, :stroke_color),
164
+ rule_below_tint: resolve_attr(style_lookup, psr,
165
+ :rule_below_tint),
166
+ rule_below_offset: resolve_attr(style_lookup, psr,
167
+ :rule_below_offset),
168
+ rule_below_left_indent: resolve_attr(style_lookup, psr,
169
+ :rule_below_left_indent),
170
+ rule_below_right_indent: resolve_attr(style_lookup, psr,
171
+ :rule_below_right_indent),
172
+ rule_below_width: resolve_attr(style_lookup, psr,
173
+ :rule_below_width),
153
174
  )
154
175
  prepend_list_marker(paragraph)
155
176
  paragraph
156
177
  end
157
178
  end
158
179
 
159
- def self.csr_runs(psr, condition_filter: nil)
180
+ def self.csr_runs(psr, condition_filter: nil, style_lookup: nil)
160
181
  psr.character_style_range.filter_map do |csr|
161
182
  next unless condition_visible?(csr, condition_filter)
162
183
 
@@ -165,29 +186,54 @@ module Idml
165
186
 
166
187
  StyledRun.new(
167
188
  text: text,
168
- font_style: csr.font_style,
169
- point_size: csr.point_size || DEFAULT_POINT_SIZE,
170
- fill_color: csr.fill_color,
171
- fill_tint: csr.fill_tint,
172
- applied_font: csr.applied_font,
189
+ font_style: resolve_csr(style_lookup, csr, :font_style),
190
+ point_size: resolve_csr(style_lookup, csr, :point_size) ||
191
+ DEFAULT_POINT_SIZE,
192
+ fill_color: resolve_csr(style_lookup, csr, :fill_color),
193
+ fill_tint: resolve_csr(style_lookup, csr, :fill_tint),
194
+ applied_font: resolve_csr(style_lookup, csr, :applied_font),
173
195
  alignment: alignment_for(csr),
174
- tracking: csr.tracking,
175
- capitalization: csr.capitalization,
176
- position: csr.position,
177
- underline: csr.underline,
178
- underline_offset: csr.underline_offset,
179
- underline_weight: csr.underline_weight,
180
- strike_thru: csr.strike_thru,
181
- strike_through_offset: csr.strike_through_offset,
182
- strike_through_weight: csr.strike_through_weight,
183
- horizontal_scale: csr.horizontal_scale,
184
- vertical_scale: csr.vertical_scale,
185
- baseline_shift: csr.baseline_shift,
196
+ tracking: resolve_csr(style_lookup, csr, :tracking),
197
+ capitalization: resolve_csr(style_lookup, csr, :capitalization),
198
+ position: resolve_csr(style_lookup, csr, :position),
199
+ underline: resolve_csr(style_lookup, csr, :underline),
200
+ underline_offset: resolve_csr(style_lookup, csr,
201
+ :underline_offset),
202
+ underline_weight: resolve_csr(style_lookup, csr,
203
+ :underline_weight),
204
+ strike_thru: resolve_csr(style_lookup, csr, :strike_thru),
205
+ strike_through_offset: resolve_csr(style_lookup, csr,
206
+ :strike_through_offset),
207
+ strike_through_weight: resolve_csr(style_lookup, csr,
208
+ :strike_through_weight),
209
+ horizontal_scale: resolve_csr(style_lookup, csr,
210
+ :horizontal_scale),
211
+ vertical_scale: resolve_csr(style_lookup, csr, :vertical_scale),
212
+ baseline_shift: resolve_csr(style_lookup, csr, :baseline_shift),
186
213
  )
187
214
  end
188
215
  end
189
216
  private_class_method :csr_runs
190
217
 
218
+ # Resolves a paragraph-level attribute from PSR or its
219
+ # referenced ParagraphStyle. When style_lookup is nil,
220
+ # falls back to reading the PSR directly (backward compat).
221
+ def self.resolve_attr(style_lookup, psr, attr_name)
222
+ return psr.public_send(attr_name) unless style_lookup
223
+
224
+ style_lookup.resolve_para_attr(psr, attr_name)
225
+ end
226
+ private_class_method :resolve_attr
227
+
228
+ # Resolves a character-level attribute from CSR or its
229
+ # referenced CharacterStyle.
230
+ def self.resolve_csr(style_lookup, csr, attr_name)
231
+ return csr.public_send(attr_name) unless style_lookup
232
+
233
+ style_lookup.resolve_char_attr(csr, attr_name)
234
+ end
235
+ private_class_method :resolve_csr
236
+
191
237
  # Drops the PSR/CSR when any of its AppliedConditions
192
238
  # references a hidden Condition. Returns true when no filter
193
239
  # is supplied (caller doesn't care about conditions).
data/lib/idml/render.rb CHANGED
@@ -8,6 +8,7 @@ module Idml
8
8
  autoload :Image, "#{__dir__}/render/image"
9
9
  autoload :ColorResolver, "#{__dir__}/render/color_resolver"
10
10
  autoload :StyleResolver, "#{__dir__}/render/style_resolver"
11
+ autoload :StyleLookup, "#{__dir__}/render/style_lookup"
11
12
  autoload :FontReferenceResolver,
12
13
  "#{__dir__}/render/font_reference_resolver"
13
14
  autoload :GradientResolver, "#{__dir__}/render/gradient_resolver"
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.5"
4
+ VERSION = "0.8.0"
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.5
4
+ version: 0.8.0
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-09 00:00:00.000000000 Z
11
+ date: 2026-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -469,6 +469,7 @@ files:
469
469
  - lib/idml/render/stroke_style.rb
470
470
  - lib/idml/render/structure_mapper.rb
471
471
  - lib/idml/render/structure_tracker.rb
472
+ - lib/idml/render/style_lookup.rb
472
473
  - lib/idml/render/style_resolver.rb
473
474
  - lib/idml/text_engine.rb
474
475
  - lib/idml/text_engine/cjk_layout.rb