idml 0.8.1 → 0.8.3
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 +4 -4
- data/TODO.pdf/103-drop-caps.md +7 -8
- data/TODO.pdf/113-multi-column-text-frames.md +5 -1
- data/lib/idml/render/renderers/text_frame_renderer.rb +161 -12
- data/lib/idml/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0672dfcc5cf9d3116f38dd98904f2d2ba10a8373d04814d6fa936f0bffcf93c0
|
|
4
|
+
data.tar.gz: a3e3d982c061380e4201490366bac05c066bec85829b397378f933a6fea27d27
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fac32a9ec144ccf1970834c0724e1eb33667f140955f2b10fad3168823c55f64826b33ccb5b4aa781ecd53b6478515cac62809603d290706a4c433784b10b266
|
|
7
|
+
data.tar.gz: ed671dd23f413602e93506584114efb806a294d2970ccd0c17571b786dba9b64323dcfb03411cf8cd0571aaf9696eda7792e4eac8eee4197100797b37eed3c43
|
data/TODO.pdf/103-drop-caps.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# TODO PDF 103: Drop caps (DropCapLines, DropCapCharacters)
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
Wrap-around text is NOT yet implemented — the drop cap renders as an
|
|
3
|
+
## Status: DONE — `Render::DropCap` helper computes drop-cap geometry.
|
|
4
|
+
TextFrameRenderer.emit_drop_cap emits the enlarged glyph, strips the
|
|
5
|
+
drop cap characters from the first run (no double render), and
|
|
6
|
+
applies reduced wrap width for the first `drop_cap_lines` runs so
|
|
7
|
+
text wraps around the drop cap. Approximation: treats each run as
|
|
8
|
+
one line (real per-line tracking would require line-count-aware
|
|
9
|
+
wrapping).
|
|
11
10
|
enlarged glyph; subsequent paragraph text flows normally and may
|
|
12
11
|
overlap the drop cap. Real wrap-around (indent lines 1..M to the
|
|
13
12
|
right of the drop cap) requires per-line wrap-width control in
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# TODO PDF 113: Multi-column text frames (TextColumnCount, TextColumnGutter)
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
3
|
+
## Status: DONE — engine_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
|
|
|
@@ -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
|
-
|
|
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 :
|
|
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:
|
|
@@ -306,18 +401,44 @@ module Idml
|
|
|
306
401
|
|
|
307
402
|
emit_paragraph_top_rule(canvas, paragraph, context, cursor_y,
|
|
308
403
|
frame_left, frame_right, first_paragraph)
|
|
309
|
-
emit_drop_cap(canvas, paragraph, context, frame_left,
|
|
310
|
-
|
|
404
|
+
drop_cap = emit_drop_cap(canvas, paragraph, context, frame_left,
|
|
405
|
+
cursor_y, first_paragraph)
|
|
406
|
+
|
|
407
|
+
effective_runs = prepare_runs_for_drop_cap(runs, drop_cap)
|
|
408
|
+
dc_width = drop_cap&.wrap_offset || 0
|
|
409
|
+
dc_lines = drop_cap&.lines || 0
|
|
311
410
|
|
|
411
|
+
rendered, cursor_y, char_cursor = iterate_paragraph_runs(
|
|
412
|
+
canvas, paragraph, effective_runs, context, layout_frame,
|
|
413
|
+
font, wrap_width, cursor_y, bottom_limit, char_cursor,
|
|
414
|
+
first_paragraph, para_attrs, dc_width, dc_lines
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
remaining_runs = effective_runs[rendered..]
|
|
418
|
+
cursor_y = emit_paragraph_bottom_rule(canvas, paragraph, context,
|
|
419
|
+
cursor_y, frame_left,
|
|
420
|
+
frame_right, para_attrs,
|
|
421
|
+
remaining_runs)
|
|
422
|
+
[remaining_runs, cursor_y, char_cursor]
|
|
423
|
+
end
|
|
424
|
+
private_class_method :render_runs_for_paragraph
|
|
425
|
+
|
|
426
|
+
def self.iterate_paragraph_runs(canvas, paragraph, runs, context,
|
|
427
|
+
layout_frame, font, wrap_width,
|
|
428
|
+
cursor_y, bottom_limit, char_cursor,
|
|
429
|
+
first_paragraph, para_attrs,
|
|
430
|
+
dc_width, dc_lines)
|
|
312
431
|
rendered_count = 0
|
|
313
432
|
runs.each do |run|
|
|
314
433
|
break if cursor_y < bottom_limit
|
|
315
434
|
|
|
316
435
|
space_before = paragraph_space_before(para_attrs, first_paragraph,
|
|
317
436
|
rendered_count)
|
|
437
|
+
run_wrap = drop_cap_wrap_width(wrap_width, dc_width,
|
|
438
|
+
dc_lines, rendered_count)
|
|
318
439
|
positioned, next_y = layout_run(
|
|
319
440
|
canvas, run, paragraph, context, layout_frame, font,
|
|
320
|
-
|
|
441
|
+
run_wrap, cursor_y, space_before,
|
|
321
442
|
para_attrs[:first_line_indent], para_attrs[:left_indent],
|
|
322
443
|
char_cursor
|
|
323
444
|
)
|
|
@@ -325,15 +446,43 @@ module Idml
|
|
|
325
446
|
cursor_y = next_y
|
|
326
447
|
rendered_count += 1
|
|
327
448
|
end
|
|
449
|
+
[rendered_count, cursor_y, char_cursor]
|
|
450
|
+
end
|
|
451
|
+
private_class_method :iterate_paragraph_runs
|
|
328
452
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
453
|
+
# Strips drop cap characters from the first run so they don't
|
|
454
|
+
# render twice (once as the enlarged drop cap, once as normal
|
|
455
|
+
# text). Returns a new run list with the first run's text
|
|
456
|
+
# adjusted. If stripping empties the first run, removes it.
|
|
457
|
+
def self.prepare_runs_for_drop_cap(runs, drop_cap)
|
|
458
|
+
return runs unless drop_cap
|
|
459
|
+
return runs if runs.empty?
|
|
460
|
+
|
|
461
|
+
stripped_count = drop_cap.text.length
|
|
462
|
+
first = runs.first
|
|
463
|
+
return runs if first.text.length < stripped_count
|
|
464
|
+
|
|
465
|
+
remaining_text = first.text[stripped_count..]
|
|
466
|
+
return runs.drop(1) if remaining_text.nil? || remaining_text.empty?
|
|
467
|
+
|
|
468
|
+
stripped_run = first.dup
|
|
469
|
+
stripped_run.text = remaining_text
|
|
470
|
+
[stripped_run] + runs[1..]
|
|
335
471
|
end
|
|
336
|
-
private_class_method :
|
|
472
|
+
private_class_method :prepare_runs_for_drop_cap
|
|
473
|
+
|
|
474
|
+
# Returns reduced wrap width for runs within the drop cap zone
|
|
475
|
+
# (first `drop_cap_lines` runs). After the zone, returns full
|
|
476
|
+
# width. Approximation: treats each run as one line. Proper
|
|
477
|
+
# implementation would track actual line count per run.
|
|
478
|
+
def self.drop_cap_wrap_width(full_width, drop_cap_width,
|
|
479
|
+
drop_cap_lines, run_index)
|
|
480
|
+
return full_width if drop_cap_width.zero? || drop_cap_lines.zero?
|
|
481
|
+
return full_width if run_index >= drop_cap_lines
|
|
482
|
+
|
|
483
|
+
full_width - drop_cap_width
|
|
484
|
+
end
|
|
485
|
+
private_class_method :drop_cap_wrap_width
|
|
337
486
|
|
|
338
487
|
# Renders the drop cap for a paragraph (when declared and the
|
|
339
488
|
# first paragraph of the chain). Wrap-around text is not yet
|
data/lib/idml/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.8.3
|
|
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-
|
|
11
|
+
date: 2026-08-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|