idml 0.9.6 → 0.9.7

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: 76ef12540a7d7d1805189664f88fc0c5b400b8d1240fbc495239ddb35f64822c
4
- data.tar.gz: 941893920b3e23a32c196de4286a64f10edb0e377bb716a5287ba110bc3052bd
3
+ metadata.gz: 0d49d9954838f3326eab4e65e2915eab9cd18a526ce7c4dd58669e6dacb8910e
4
+ data.tar.gz: 56713c3d26980c24c6baf8d080543cd2471aef5dea4ca83c33be82f73fe2e83e
5
5
  SHA512:
6
- metadata.gz: 95da409c1987bac0a36ab73793c02e349d3060f313f3f4fc2670f22efffd89bd2d5f0bcfba4096dab595cf39a7843134ee0fc4f06f9e4136d477c65f44870586
7
- data.tar.gz: a74c7d9a0a1ccd22e57308890a0f6b257e9ec2d3aab7d3d8a407c23b4b52f466cd2c39f86be48d4b89e912e26fcd3a582dbdf27455d8db49a405219ede022357
6
+ metadata.gz: d52298b420e04ae5c4b8292e01d951eb23d03d4e01357f463111477e842aea9dd89f6e162a88ad05dd50fa8b721faf6522f891eb66e3405dd6207769f7a9bfe8
7
+ data.tar.gz: 1d7f89c274a8aac75868064826567901fbc0503ead631953fc7b24eaa282e0f1d8870d2e33f1dc6e81c834b4e3c00fc4b2b465b1a12cc349484b0e653628e63f
@@ -0,0 +1,24 @@
1
+ # TODO PDF 123: Keep options (widow/orphan control)
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-20
4
+
5
+ ## What was done
6
+
7
+ - `StyleResolver::Paragraph` carries `keep_all_lines_together`
8
+ from the PSR.
9
+ - `TextFrameRenderer.consume_paragraphs` pre-measures the paragraph
10
+ (TextEngine::Measurement) and, when KeepAllLinesTogether is set,
11
+ the paragraph cannot fully fit the remaining frame space, and at
12
+ least one paragraph was already placed, pushes the whole
13
+ paragraph to the next frame via the chain state — no widowed
14
+ first lines. The first paragraph in a frame never pushes, so
15
+ progress is always made.
16
+
17
+ ## Known limitations
18
+
19
+ - KeepLinesTogether with per-side KeepFirstLines / KeepLastLines
20
+ counts (partial keep windows) is not modeled — only the
21
+ whole-paragraph KeepAllLinesTogether form.
22
+ - KeepWithNext (binding a paragraph to the next) is not modeled.
23
+ - Vertical writing mode does not apply keep options (column model
24
+ differs).
@@ -0,0 +1,23 @@
1
+ # TODO PDF 124: Tate-chu-yoko and vertical multi-run columns
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-20
4
+
5
+ ## What was done
6
+
7
+ - Multi-run column continuity (bug fix): every run in a vertical
8
+ frame used to restart at column 0, overlapping earlier runs.
9
+ `VerticalTextLayout.layout` takes `start_column`; the frame
10
+ renderer threads the next-unused column across runs and
11
+ paragraphs, and persists it in the chain state (`column_offset`)
12
+ so vertical text continues correctly into the next frame of a
13
+ chain.
14
+ - Tate-chu-yoko (`Tatechuyoko="true"` on the CSR — note the
15
+ schema's lowercase c): the run's glyphs render HORIZONTALLY,
16
+ sharing one baseline at the top of their column slot and centered
17
+ in it, consuming one column. Groups wider than the column height
18
+ fall back to stacked layout. StyledRun carries `tatechuyoko`.
19
+
20
+ ## Remaining from TODO 13
21
+
22
+ Mojikumi (punctuation spacing classes) is the last documented
23
+ stretch goal.
@@ -177,17 +177,20 @@ module Idml
177
177
  layout_frame = layout_frame(context.item, box)
178
178
  left_limit = layout_frame.x + (layout_frame.inset_left || 0)
179
179
  resume_vertical_paragraph(state)
180
+ next_column = state.column_offset || 0
180
181
  pending = 0
181
182
 
182
183
  state.paragraphs.each do |paragraph|
183
- remaining_runs = vertical_paragraph(
184
- canvas, paragraph, context, layout_frame, font, left_limit
184
+ remaining_runs, next_column = vertical_paragraph(
185
+ canvas, paragraph, context, layout_frame, font,
186
+ left_limit, next_column
185
187
  )
186
188
  if remaining_runs.empty?
187
189
  pending += 1
188
190
  else
189
191
  state.current_paragraph = paragraph
190
192
  state.runs_remaining = remaining_runs
193
+ state.column_offset = next_column
191
194
  break
192
195
  end
193
196
  end
@@ -198,7 +201,9 @@ module Idml
198
201
 
199
202
  # Folds a chain-resumed paragraph (runs_remaining) back to
200
203
  # the head of the paragraph list so the vertical path sees a
201
- # uniform sequence.
204
+ # uniform sequence. The consumed column offset persists in
205
+ # the state so the next frame continues right where this
206
+ # frame's columns ended.
202
207
  def self.resume_vertical_paragraph(state)
203
208
  return unless state.current_paragraph
204
209
 
@@ -209,10 +214,13 @@ module Idml
209
214
  end
210
215
  private_class_method :resume_vertical_paragraph
211
216
 
212
- # Renders one paragraph's runs as vertical columns. Returns
213
- # the runs that did not fit (empty when all placed).
217
+ # Renders one paragraph's runs as vertical columns starting
218
+ # at `start_column`. Returns [runs that did not fit (empty
219
+ # when all placed), the next unused column index].
214
220
  def self.vertical_paragraph(canvas, paragraph, context,
215
- layout_frame, font, left_limit)
221
+ layout_frame, font, left_limit,
222
+ start_column)
223
+ next_column = start_column
216
224
  paragraph.runs.each_with_index do |run, index|
217
225
  size = run.point_size || DEFAULT_SIZE
218
226
  leading = TextEngine::VerticalLayout.leading_for(
@@ -220,25 +228,72 @@ module Idml
220
228
  )
221
229
  glyphs = TextEngine::Shaper.shape(text: run.text, font: font,
222
230
  size: size)
223
- positioned, columns = TextEngine::VerticalTextLayout.layout(
224
- glyphs: glyphs, frame: layout_frame, leading: leading,
225
- size: size
226
- )
227
- unless vertical_fits?(layout_frame, leading, columns,
231
+ grouped = tatechuyoko_group(run, glyphs, layout_frame,
232
+ leading, size, next_column)
233
+ positioned, next_column = grouped ||
234
+ TextEngine::VerticalTextLayout.layout(
235
+ glyphs: glyphs, frame: layout_frame, leading: leading,
236
+ size: size, start_column: next_column
237
+ )
238
+ unless vertical_fits?(layout_frame, leading, next_column,
228
239
  left_limit)
229
- return paragraph.runs[index..]
240
+ return [paragraph.runs[index..], start_column]
230
241
  end
231
242
 
232
- positioned.each do |glyph|
233
- emit_vertical_glyph(canvas, glyph, run, context, size)
234
- end
243
+ emit_vertical_run(canvas, positioned, grouped, run, context,
244
+ size)
235
245
  emit_vertical_ruby(canvas, run, positioned, layout_frame,
236
246
  size, context)
237
247
  end
238
- []
248
+ [[], next_column]
239
249
  end
240
250
  private_class_method :vertical_paragraph
241
251
 
252
+ # A tate-chu-yoko group for runs that declare it (and whose
253
+ # glyphs fit horizontally in a column slot); nil otherwise.
254
+ def self.tatechuyoko_group(run, glyphs, layout_frame, leading,
255
+ size, start_column)
256
+ return nil unless run.tatechuyoko
257
+
258
+ TextEngine::VerticalTextLayout.tatechuyoko_group(
259
+ glyphs: glyphs, frame: layout_frame, leading: leading,
260
+ size: size, start_column: start_column
261
+ )
262
+ end
263
+ private_class_method :tatechuyoko_group
264
+
265
+ # Emits a vertical run's glyphs: rotated/normal vertical
266
+ # handling for stacked columns, plain upright emission for
267
+ # tate-chu-yoko groups.
268
+ def self.emit_vertical_run(canvas, positioned, grouped, run,
269
+ context, size)
270
+ if grouped
271
+ positioned.each do |glyph|
272
+ emit_upright_glyph(canvas, glyph, run, context, size)
273
+ end
274
+ else
275
+ positioned.each do |glyph|
276
+ emit_vertical_glyph(canvas, glyph, run, context, size)
277
+ end
278
+ end
279
+ end
280
+ private_class_method :emit_vertical_run
281
+
282
+ def self.emit_upright_glyph(canvas, positioned, run, context,
283
+ size)
284
+ text = [positioned.codepoint].pack("U")
285
+ text_kwargs = CharacterStyle.text_kwargs(
286
+ run,
287
+ {
288
+ at: [positioned.x, positioned.y],
289
+ font: font_for_run(run, context),
290
+ size: size,
291
+ },
292
+ )
293
+ canvas.text(text, **text_kwargs)
294
+ end
295
+ private_class_method :emit_upright_glyph
296
+
242
297
  def self.vertical_fits?(layout_frame, leading, columns, left_limit)
243
298
  right = layout_frame.x + layout_frame.width -
244
299
  (layout_frame.inset_right || 0)
@@ -576,7 +631,9 @@ module Idml
576
631
  pending = 0
577
632
  state.paragraphs.each do |paragraph|
578
633
  break if cursor_y < bottom_limit
579
- break if paragraph_break?(paragraph) && pending.positive?
634
+ break if paragraph_deferred?(paragraph, layout_frame, font,
635
+ cursor_y, bottom_limit,
636
+ pending.positive?)
580
637
 
581
638
  remaining_runs, cursor_y, char_cursor = render_runs_for_paragraph(
582
639
  canvas, paragraph, paragraph.runs, context,
@@ -908,6 +965,26 @@ module Idml
908
965
  end
909
966
  private_class_method :layout_run
910
967
 
968
+ # True when the paragraph should move wholly to the next
969
+ # frame: a StartParagraph forced break, or KeepAllLines-
970
+ # Together with insufficient remaining space (never for the
971
+ # frame's first paragraph, so progress is always made).
972
+ def self.paragraph_deferred?(paragraph, layout_frame, font,
973
+ cursor_y, bottom_limit, placed_any)
974
+ return true if paragraph_break?(paragraph) && placed_any
975
+ return false unless paragraph.keep_all_lines_together
976
+ return false unless placed_any
977
+
978
+ wrap_width = TextEngine::VerticalLayout.wrap_width(
979
+ layout_frame, paragraph.right_indent || 0
980
+ )
981
+ height = TextEngine::Measurement.paragraph_height(
982
+ paragraph, font, wrap_width
983
+ )
984
+ (cursor_y - height) < bottom_limit
985
+ end
986
+ private_class_method :paragraph_deferred?
987
+
911
988
  # True when the paragraph requests a forced break to the
912
989
  # next frame/column (StartParagraph). All break flavors act
913
990
  # at frame/column granularity.
@@ -25,6 +25,9 @@ module Idml
25
25
  :current_paragraph,
26
26
  :runs_remaining,
27
27
  :char_cursor,
28
+ # Vertical-writing path: columns already consumed when the
29
+ # paragraph chain continues into the next frame.
30
+ :column_offset,
28
31
  keyword_init: true,
29
32
  )
30
33
 
@@ -48,6 +48,7 @@ module Idml
48
48
  :ruby_string,
49
49
  :ruby_font_size,
50
50
  :ruby_position,
51
+ :tatechuyoko,
51
52
  keyword_init: true,
52
53
  )
53
54
 
@@ -70,6 +71,9 @@ module Idml
70
71
  # Forced paragraph break (StartParagraph: NextPage /
71
72
  # NextColumn / NextFrame / NextOddPage / NextEvenPage).
72
73
  :start_paragraph,
74
+ # Widow/orphan control: KeepAllLinesTogether pushes the whole
75
+ # paragraph to the next frame when it cannot fully fit.
76
+ :keep_all_lines_together,
73
77
  :bullets_and_numbering_list_type,
74
78
  :bullet_character_value,
75
79
  :bullets_text_after,
@@ -200,6 +204,9 @@ module Idml
200
204
  ),
201
205
  start_paragraph: resolve_attr(style_lookup, psr,
202
206
  :start_paragraph),
207
+ keep_all_lines_together: resolve_attr(
208
+ style_lookup, psr, :keep_all_lines_together
209
+ ),
203
210
  drop_cap_lines: resolve_attr(style_lookup, psr, :drop_cap_lines),
204
211
  drop_cap_characters: resolve_attr(style_lookup, psr,
205
212
  :drop_cap_characters),
@@ -390,6 +397,7 @@ module Idml
390
397
  ruby_string: resolve_csr(style_lookup, csr, :ruby_string),
391
398
  ruby_font_size: resolve_csr(style_lookup, csr, :ruby_font_size),
392
399
  ruby_position: resolve_csr(style_lookup, csr, :ruby_position),
400
+ tatechuyoko: resolve_csr(style_lookup, csr, :tatechuyoko),
393
401
  )
394
402
  end
395
403
  private_class_method :text_run
@@ -13,18 +13,19 @@ module Idml
13
13
  PositionedGlyph = Struct.new(:codepoint, :x, :y, keyword_init: true)
14
14
 
15
15
  # Stacks `glyphs` into columns of at most the frame's text
16
- # height; column 0 is the rightmost. Returns
17
- # [positioned_glyphs, column_count].
18
- def self.layout(glyphs:, frame:, leading:, size:)
16
+ # height; column 0 is the rightmost. `start_column` places
17
+ # the first column after already-used columns (multi-run
18
+ # frames). Returns [positioned_glyphs, next_column_index].
19
+ def self.layout(glyphs:, frame:, leading:, size:, start_column: 0)
19
20
  lines = TextEngine::LineBreaker.break(
20
21
  glyphs: glyphs, frame_width: column_height(frame),
21
22
  )
22
23
  positioned = []
23
- lines.each_with_index do |line, column_index|
24
+ lines.each_with_index do |line, index|
24
25
  stack_column(positioned, line.glyphs, frame, leading, size,
25
- column_index)
26
+ index + start_column)
26
27
  end
27
- [positioned, lines.length]
28
+ [positioned, start_column + lines.length]
28
29
  end
29
30
 
30
31
  # The frame's usable vertical extent for one column of text.
@@ -47,6 +48,32 @@ module Idml
47
48
  end
48
49
  private_class_method :stack_column
49
50
 
51
+ # Positions a tate-chu-yoko group (e.g. the digits "12" kept
52
+ # horizontal inside vertical text): glyphs share one baseline
53
+ # at the top of the given column slot and advance left-to-
54
+ # right, centered in the slot. Returns
55
+ # [positioned_glyphs, next_column_index]; nil when the group
56
+ # is wider than the column height (caller falls back to
57
+ # stacked layout).
58
+ def self.tatechuyoko_group(glyphs:, frame:, leading:, size:,
59
+ start_column:)
60
+ total = glyphs.sum(&:width)
61
+ return nil if total > column_height(frame)
62
+
63
+ right = frame.x + frame.width - (frame.inset_right || 0)
64
+ slot_center = right - ((start_column + 0.5) * leading)
65
+ x = slot_center - (total / 2.0)
66
+ baseline_y = frame.y + frame.height - (frame.inset_top || 0) - size
67
+ positioned = glyphs.map do |glyph|
68
+ glyph_position = PositionedGlyph.new(
69
+ codepoint: glyph.codepoint, x: x, y: baseline_y,
70
+ )
71
+ x += glyph.width
72
+ glyph_position
73
+ end
74
+ [positioned, start_column + 1]
75
+ end
76
+
50
77
  # Vertical advance of one glyph — its measured width (the em
51
78
  # for the common square-CJK case).
52
79
  def self.advance(glyph)
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.9.6"
4
+ VERSION = "0.9.7"
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.9.6
4
+ version: 0.9.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -181,6 +181,8 @@ files:
181
181
  - TODO.pdf/120-start-paragraph-breaks.md
182
182
  - TODO.pdf/121-cell-edge-strokes.md
183
183
  - TODO.pdf/122-vertical-latin-ruby.md
184
+ - TODO.pdf/123-keep-options.md
185
+ - TODO.pdf/124-tatechuyoko-vertical.md
184
186
  - TODO.pdf/13-cjk-text-layout.md
185
187
  - TODO.pdf/14-page-item-element-models.md
186
188
  - TODO.pdf/15-wire-spread-children.md