idml 0.8.2 → 0.8.4

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: 27df11baaefb3de6bfc6198404a9b3ad10b54b53de8a44cbe58658afdfdf4d4b
4
- data.tar.gz: 4274cf4d1869bab1457da1427d8ba96d27cf64ced0140174923b7492ee283dd8
3
+ metadata.gz: a20e81d5d762e6dfcb5c374d342210ac6132a96604d83ee305dfa1c64e64e535
4
+ data.tar.gz: e6da886fc60f952b2e9f39f512027b953958c19e14cadf3200dc9952e4c3fa96
5
5
  SHA512:
6
- metadata.gz: '02936232be8a99effaf2f417d765281700c8f6b89004bab18e462d29d8b60b12259a715780fd0c2c1b68d41e5f82cd087b2933753551c72f3235d5cfbe3e3640'
7
- data.tar.gz: e92764f18f109a4ae66dabb4ebfcb3a991cd2926c22e7836de42e99d7c1b45e931cd0aa4f22678193564fb82bdbf24eeccd593405a1ada0322cda7f5ce54f32d
6
+ metadata.gz: f8a51af44eaa9c43cba3bbdc9f332b1fa94f34f3eb08cb32ce01b957ceff14d6903f03a2d458884a538ef6f6e56d224868427abc46f1303d8f04d82324d9aa5b
7
+ data.tar.gz: 68e81d0fc87751c37015ea4a3b905a65c7863145bc074714e3241a02e4f951294703b3a3b4eef8d46601d896b95ddc40d3d267f9e54ee71483b2ba9bccbb742a
@@ -1,6 +1,19 @@
1
1
  # TODO PDF 107: Image rendering unification via PageItemRenderer
2
2
 
3
- ## Status: OPENgap identified in 2026-08-08 audit
3
+ ## Status: DONE for z-order ImageCollector now tags each ref with
4
+ `parent_self` (the containing page item's Self). SpreadRenderer
5
+ groups refs by parent and renders each item's images when that item
6
+ is reached in the each_page_item loop (instead of all upfront as a
7
+ background layer). Images now respect z-order relative to other
8
+ page items.
9
+
10
+ Intra-item ordering (fill behind image, stroke on top) remains
11
+ approximate: the image renders BEFORE the item's fill/stroke dispatch.
12
+ For the common case (image inside unfilled rectangle with stroke)
13
+ this produces correct output — image behind stroke. For filled
14
+ rectangles containing images (rare), the fill would cover the image.
15
+ Full intra-item ordering would require integrating image rendering
16
+ into RectangleRenderer between fill and stroke.
4
17
 
5
18
  ## Problem
6
19
 
@@ -1,6 +1,10 @@
1
1
  # TODO PDF 113: Multi-column text frames (TextColumnCount, TextColumnGutter)
2
2
 
3
- ## Status: OPENgap identified in 2026-08-10 audit
3
+ ## Status: DONEengine_render detects TextColumnCount > 1 and
4
+ splits the frame into N column-specific Frame structs. Text flows
5
+ column 1 → column 2 → ... → chain to next frame via StoryChainController.
6
+ Each column acts as a mini-frame with the same height/insets but a
7
+ narrower width. TextColumnGutter honored as the gap between columns.
4
8
 
5
9
  ## Problem
6
10
 
@@ -56,11 +56,13 @@ module Idml
56
56
 
57
57
  load_new(image, parent, uri)
58
58
  end
59
+ private :register
59
60
 
60
61
  def reuse(name, image, parent)
61
62
  { name: name, placement: placement_for(image, parent),
62
- clip_box: clip_box_for(parent) }
63
+ clip_box: clip_box_for(parent), parent_self: parent.self_attr }
63
64
  end
65
+ private :reuse
64
66
 
65
67
  def load_new(image, parent, uri)
66
68
  path = Image.resolve_path(uri, base_dir: @base_dir)
@@ -73,8 +75,9 @@ module Idml
73
75
  name = @writer.add_image(data: data)
74
76
  @writer.register_image_name(uri, name)
75
77
  { name: name, placement: placement_for(image, parent, dims[1]),
76
- clip_box: clip_box_for(parent) }
78
+ clip_box: clip_box_for(parent), parent_self: parent.self_attr }
77
79
  end
80
+ private :load_new
78
81
 
79
82
  def clip_box_for(parent)
80
83
  return nil unless parent.geometric_bounds
@@ -158,7 +158,22 @@ module Idml
158
158
  # next frame in the chain.
159
159
  def self.engine_render(canvas, state, context, box, font)
160
160
  layout_frame = layout_frame(context.item, box)
161
- top_y = box[:y] + box[:height] - (layout_frame.inset_top || 0)
161
+ col_count = text_column_count(context)
162
+
163
+ if col_count && col_count > 1
164
+ engine_render_multi_column(canvas, state, context, layout_frame,
165
+ font, col_count)
166
+ else
167
+ engine_render_single_column(canvas, state, context, layout_frame,
168
+ font)
169
+ end
170
+ end
171
+ private_class_method :engine_render
172
+
173
+ def self.engine_render_single_column(canvas, state, context,
174
+ layout_frame, font)
175
+ top_y = layout_frame.y + layout_frame.height -
176
+ (layout_frame.inset_top || 0)
162
177
  bottom_limit = TextEngine::VerticalLayout.bottom_limit(layout_frame)
163
178
  cursor_y = vertical_justify_top(top_y, bottom_limit, state, context)
164
179
  char_cursor = state.char_cursor
@@ -174,7 +189,87 @@ module Idml
174
189
  consume_paragraphs(canvas, state, context, layout_frame, font,
175
190
  cursor_y, bottom_limit, char_cursor)
176
191
  end
177
- private_class_method :engine_render
192
+ private_class_method :engine_render_single_column
193
+
194
+ # Renders text across N columns within a single frame. Text
195
+ # flows column 1 → column 2 → ... → chain to next frame. Each
196
+ # column acts as a mini-frame with the same height but a
197
+ # narrower width. The chain state threads across columns
198
+ # transparently.
199
+ def self.engine_render_multi_column(canvas, state, context,
200
+ layout_frame, font, col_count)
201
+ gutter = text_column_gutter(context) || 0
202
+ col_frames = build_column_frames(layout_frame, col_count, gutter)
203
+
204
+ col_frames.each do |col_frame|
205
+ break if state_empty?(state)
206
+
207
+ render_column(canvas, state, context, col_frame, font)
208
+ end
209
+ end
210
+ private_class_method :engine_render_multi_column
211
+
212
+ def self.render_column(canvas, state, context, col_frame, font)
213
+ top_y = col_frame.y + col_frame.height - (col_frame.inset_top || 0)
214
+ bottom_limit = TextEngine::VerticalLayout.bottom_limit(col_frame)
215
+ cursor_y = top_y
216
+ char_cursor = state.char_cursor
217
+
218
+ if state.current_paragraph
219
+ cursor_y, char_cursor = resume_paragraph(
220
+ canvas, state, context, col_frame, font,
221
+ cursor_y, bottom_limit, char_cursor
222
+ )
223
+ return if state.current_paragraph
224
+ end
225
+
226
+ consume_paragraphs(canvas, state, context, col_frame, font,
227
+ cursor_y, bottom_limit, char_cursor)
228
+ end
229
+ private_class_method :render_column
230
+
231
+ def self.state_empty?(state)
232
+ state.paragraphs.empty? && state.current_paragraph.nil?
233
+ end
234
+ private_class_method :state_empty?
235
+
236
+ def self.text_column_count(context)
237
+ pref = context.item&.text_frame_preference&.first
238
+ pref&.text_column_count
239
+ end
240
+ private_class_method :text_column_count
241
+
242
+ def self.text_column_gutter(context)
243
+ pref = context.item&.text_frame_preference&.first
244
+ pref&.text_column_gutter
245
+ end
246
+ private_class_method :text_column_gutter
247
+
248
+ # Builds N column-specific Frame structs from the parent
249
+ # layout frame. Each column has the same y/height/insets but
250
+ # a different x and width so VerticalLayout's geometry math
251
+ # produces correct per-column results.
252
+ def self.build_column_frames(layout_frame, col_count, gutter)
253
+ inset_left = layout_frame.inset_left || 0
254
+ inset_right = layout_frame.inset_right || 0
255
+ content_width = layout_frame.width - inset_left - inset_right
256
+ col_width = (content_width - ((col_count - 1) * gutter)) / col_count
257
+
258
+ Array.new(col_count) do |i|
259
+ col_left = layout_frame.x + inset_left + (i * (col_width + gutter))
260
+ TextEngine::Frame.new(
261
+ x: col_left - inset_left,
262
+ y: layout_frame.y,
263
+ width: col_width + inset_left + inset_right,
264
+ height: layout_frame.height,
265
+ inset_top: layout_frame.inset_top,
266
+ inset_bottom: layout_frame.inset_bottom,
267
+ inset_left: inset_left,
268
+ inset_right: inset_right,
269
+ )
270
+ end
271
+ end
272
+ private_class_method :build_column_frames
178
273
 
179
274
  # Computes the starting cursor_y for vertical justification.
180
275
  # IDML's TextFramePreference.VerticalJustification can be:
@@ -40,14 +40,15 @@ module Idml
40
40
  condition_filter: @condition_filter,
41
41
  style_lookup: @style_lookup,
42
42
  }
43
+ images_by_parent = group_images_by_parent(image_refs)
43
44
 
44
45
  canvas.save_graphics_state do
45
46
  render_background(canvas, page_width, page_height)
46
- render_images(canvas, image_refs)
47
- render_master_items(canvas, spread, context_base)
47
+ render_master_items(canvas, spread, context_base, images_by_parent)
48
48
  spread.each_page_item do |item|
49
49
  next unless @layer_filter.visible?(item)
50
50
 
51
+ render_item_images(canvas, images_by_parent, item.self_attr)
51
52
  context = RenderContext.new(context_base.merge(item: item))
52
53
  PageItemRenderer.render(canvas, context)
53
54
  end
@@ -62,13 +63,30 @@ module Idml
62
63
  canvas.fill
63
64
  end
64
65
 
66
+ # Groups image refs by their parent page item's Self so each
67
+ # item's images render when that item is reached in z-order
68
+ # (instead of all upfront as a background layer).
69
+ def group_images_by_parent(image_refs)
70
+ image_refs.each_with_object({}) do |ref, hash|
71
+ parent = ref[:parent_self]
72
+ next unless parent
73
+
74
+ (hash[parent] ||= []) << ref
75
+ end
76
+ end
77
+
78
+ def render_item_images(canvas, images_by_parent, item_self)
79
+ refs = images_by_parent[item_self]
80
+ return unless refs
81
+
82
+ render_images(canvas, refs)
83
+ end
84
+
65
85
  def render_images(canvas, image_refs)
66
86
  return if image_refs.empty?
67
87
 
68
88
  image_refs.each do |ref|
69
89
  placement = ref[:placement]
70
- placement[:scale_x].abs
71
- placement[:scale_y].abs
72
90
  canvas.save_graphics_state do
73
91
  apply_image_clip(canvas, ref)
74
92
  canvas.draw_image_matrix(ref[:name],
@@ -88,11 +106,12 @@ module Idml
88
106
  canvas.clip
89
107
  end
90
108
 
91
- def render_master_items(canvas, spread, context_base)
109
+ def render_master_items(canvas, spread, context_base, images_by_parent)
92
110
  return unless @package
93
111
 
94
112
  resolve_master_spreads(spread).each do |master|
95
- render_master_page_items(canvas, master, context_base)
113
+ render_master_page_items(canvas, master, context_base,
114
+ images_by_parent)
96
115
  end
97
116
  end
98
117
 
@@ -107,11 +126,13 @@ module Idml
107
126
  masters
108
127
  end
109
128
 
110
- def render_master_page_items(canvas, master_part, context_base)
129
+ def render_master_page_items(canvas, master_part, context_base,
130
+ images_by_parent)
111
131
  master_part.master_spread.each do |master_so|
112
132
  master_so.each_page_item do |item|
113
133
  next unless @layer_filter.visible?(item)
114
134
 
135
+ render_item_images(canvas, images_by_parent, item.self_attr)
115
136
  context = RenderContext.new(context_base.merge(item: item))
116
137
  PageItemRenderer.render(canvas, context)
117
138
  end
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.2"
4
+ VERSION = "0.8.4"
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.8.2
4
+ version: 0.8.4
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-13 00:00:00.000000000 Z
11
+ date: 2026-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal