idml 0.8.0 → 0.8.2

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: bdf6a3aba5bbe199af39e5addabae082b795dfef1583dd14f33a9d7415086ead
4
- data.tar.gz: eb1a78bd1a11fdc7500543b903b4d927114a96a70fea97a7e8b4c62db813451e
3
+ metadata.gz: 27df11baaefb3de6bfc6198404a9b3ad10b54b53de8a44cbe58658afdfdf4d4b
4
+ data.tar.gz: 4274cf4d1869bab1457da1427d8ba96d27cf64ced0140174923b7492ee283dd8
5
5
  SHA512:
6
- metadata.gz: e47ca1b121cd27336a516f8b0cb863d5ad03edbdc43455bfa3efcb41807865a1dae4aec35ccbf380e003dc383d9f0d3704b42e23efa0d56c6925f316b68030c1
7
- data.tar.gz: 3ac803ec48c5a0e2a2e3eec6f17845993a2a3194ddb8046bd5e8ea7225fcd8709abef22817684c12c51814b26a6ca9d3e07f2b4a735b519b9cb3d99e9eaaec5c
6
+ metadata.gz: '02936232be8a99effaf2f417d765281700c8f6b89004bab18e462d29d8b60b12259a715780fd0c2c1b68d41e5f82cd087b2933753551c72f3235d5cfbe3e3640'
7
+ data.tar.gz: e92764f18f109a4ae66dabb4ebfcb3a991cd2926c22e7836de42e99d7c1b45e931cd0aa4f22678193564fb82bdbf24eeccd593405a1ada0322cda7f5ce54f32d
@@ -1,13 +1,12 @@
1
1
  # TODO PDF 103: Drop caps (DropCapLines, DropCapCharacters)
2
2
 
3
- ## Status: PARTIAL — `Render::DropCap` helper computes drop-cap
4
- geometry (font_size = base × lines, width via font_metrics.measure_text,
5
- height = leading × lines). TextFrameRenderer.emit_drop_cap emits the
6
- drop cap as an enlarged glyph at the paragraph's top-left when the
7
- paragraph declares DropCapLines/DropCapCharacters > 0 and is the
8
- chain's first paragraph.
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,9 +1,10 @@
1
1
  # TODO PDF 115: Bullet and numbered list rendering
2
2
 
3
- ## Status: DONE for inline marker emission. Hanging-indent layout
4
- (bullet in a left gutter with wrapped lines indented to align with
5
- text after marker) is a follow-up currently the marker sits inline
6
- before the first run's text.
3
+ ## Status: DONE markers are prepended inline AND a hanging indent
4
+ (18pt) is applied so wrapped lines align with text after the marker.
5
+ The indent is approximate (fixed width) since tab-stop measurement
6
+ requires font metrics at style-resolution time; future refinement
7
+ would measure the actual marker width.
7
8
 
8
9
  ## Problem
9
10
 
@@ -306,18 +306,44 @@ module Idml
306
306
 
307
307
  emit_paragraph_top_rule(canvas, paragraph, context, cursor_y,
308
308
  frame_left, frame_right, first_paragraph)
309
- emit_drop_cap(canvas, paragraph, context, frame_left, cursor_y,
310
- first_paragraph)
309
+ drop_cap = emit_drop_cap(canvas, paragraph, context, frame_left,
310
+ cursor_y, first_paragraph)
311
311
 
312
+ effective_runs = prepare_runs_for_drop_cap(runs, drop_cap)
313
+ dc_width = drop_cap&.wrap_offset || 0
314
+ dc_lines = drop_cap&.lines || 0
315
+
316
+ rendered, cursor_y, char_cursor = iterate_paragraph_runs(
317
+ canvas, paragraph, effective_runs, context, layout_frame,
318
+ font, wrap_width, cursor_y, bottom_limit, char_cursor,
319
+ first_paragraph, para_attrs, dc_width, dc_lines
320
+ )
321
+
322
+ remaining_runs = effective_runs[rendered..]
323
+ cursor_y = emit_paragraph_bottom_rule(canvas, paragraph, context,
324
+ cursor_y, frame_left,
325
+ frame_right, para_attrs,
326
+ remaining_runs)
327
+ [remaining_runs, cursor_y, char_cursor]
328
+ end
329
+ private_class_method :render_runs_for_paragraph
330
+
331
+ def self.iterate_paragraph_runs(canvas, paragraph, runs, context,
332
+ layout_frame, font, wrap_width,
333
+ cursor_y, bottom_limit, char_cursor,
334
+ first_paragraph, para_attrs,
335
+ dc_width, dc_lines)
312
336
  rendered_count = 0
313
337
  runs.each do |run|
314
338
  break if cursor_y < bottom_limit
315
339
 
316
340
  space_before = paragraph_space_before(para_attrs, first_paragraph,
317
341
  rendered_count)
342
+ run_wrap = drop_cap_wrap_width(wrap_width, dc_width,
343
+ dc_lines, rendered_count)
318
344
  positioned, next_y = layout_run(
319
345
  canvas, run, paragraph, context, layout_frame, font,
320
- wrap_width, cursor_y, space_before,
346
+ run_wrap, cursor_y, space_before,
321
347
  para_attrs[:first_line_indent], para_attrs[:left_indent],
322
348
  char_cursor
323
349
  )
@@ -325,15 +351,43 @@ module Idml
325
351
  cursor_y = next_y
326
352
  rendered_count += 1
327
353
  end
354
+ [rendered_count, cursor_y, char_cursor]
355
+ end
356
+ private_class_method :iterate_paragraph_runs
328
357
 
329
- remaining_runs = runs[rendered_count..]
330
- cursor_y = emit_paragraph_bottom_rule(canvas, paragraph, context,
331
- cursor_y, frame_left,
332
- frame_right, para_attrs,
333
- remaining_runs)
334
- [remaining_runs, cursor_y, char_cursor]
358
+ # Strips drop cap characters from the first run so they don't
359
+ # render twice (once as the enlarged drop cap, once as normal
360
+ # text). Returns a new run list with the first run's text
361
+ # adjusted. If stripping empties the first run, removes it.
362
+ def self.prepare_runs_for_drop_cap(runs, drop_cap)
363
+ return runs unless drop_cap
364
+ return runs if runs.empty?
365
+
366
+ stripped_count = drop_cap.text.length
367
+ first = runs.first
368
+ return runs if first.text.length < stripped_count
369
+
370
+ remaining_text = first.text[stripped_count..]
371
+ return runs.drop(1) if remaining_text.nil? || remaining_text.empty?
372
+
373
+ stripped_run = first.dup
374
+ stripped_run.text = remaining_text
375
+ [stripped_run] + runs[1..]
335
376
  end
336
- private_class_method :render_runs_for_paragraph
377
+ private_class_method :prepare_runs_for_drop_cap
378
+
379
+ # Returns reduced wrap width for runs within the drop cap zone
380
+ # (first `drop_cap_lines` runs). After the zone, returns full
381
+ # width. Approximation: treats each run as one line. Proper
382
+ # implementation would track actual line count per run.
383
+ def self.drop_cap_wrap_width(full_width, drop_cap_width,
384
+ drop_cap_lines, run_index)
385
+ return full_width if drop_cap_width.zero? || drop_cap_lines.zero?
386
+ return full_width if run_index >= drop_cap_lines
387
+
388
+ full_width - drop_cap_width
389
+ end
390
+ private_class_method :drop_cap_wrap_width
337
391
 
338
392
  # Renders the drop cap for a paragraph (when declared and the
339
393
  # first paragraph of the chain). Wrap-around text is not yet
@@ -249,9 +249,12 @@ module Idml
249
249
  end
250
250
  private_class_method :alignment_for
251
251
 
252
+ HANGING_INDENT_WIDTH = 18.0
253
+
252
254
  # Prepends the list marker (bullet glyph or numbered expression)
253
255
  # to the paragraph's first run when the paragraph is part of
254
- # a list. Mutates the first run's text in place.
256
+ # a list. Also applies a hanging indent so wrapped lines align
257
+ # with the text after the marker, not with the marker itself.
255
258
  def self.prepend_list_marker(paragraph)
256
259
  marker = ListMarker.marker_for(paragraph)
257
260
  return unless marker
@@ -259,9 +262,21 @@ module Idml
259
262
 
260
263
  first_run = paragraph.runs.first
261
264
  first_run.text = "#{marker}#{first_run.text}"
265
+ apply_hanging_indent(paragraph)
262
266
  end
263
267
  private_class_method :prepend_list_marker
264
268
 
269
+ # Adjusts left_indent and first_line_indent for a hanging
270
+ # indent effect: first line starts at the original left edge
271
+ # (with the marker), wrapped lines indent by HANGING_INDENT_WIDTH.
272
+ def self.apply_hanging_indent(paragraph)
273
+ current_left = paragraph.left_indent || 0
274
+ current_first = paragraph.first_line_indent || 0
275
+ paragraph.left_indent = current_left + HANGING_INDENT_WIDTH
276
+ paragraph.first_line_indent = current_first - HANGING_INDENT_WIDTH
277
+ end
278
+ private_class_method :apply_hanging_indent
279
+
265
280
  # Concatenate runs into a single block. Used when the renderer
266
281
  # can't handle per-run styling (e.g., no font metrics available).
267
282
  def self.concatenate(runs)
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.0"
4
+ VERSION = "0.8.2"
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.0
4
+ version: 0.8.2
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-10 00:00:00.000000000 Z
11
+ date: 2026-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal