idml 0.9.0 → 0.9.1

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: ff487a296f9cbd5b926b963c7acb7d4864d43060ffed5b1ec7438a75bac1446a
4
- data.tar.gz: f4926fe3add1833a954628aace986839f101722deafde891783c54afdd509a58
3
+ metadata.gz: a82b32708ab694d0d794c39f33d74de7b39a9bdc38b7a6315b99a2156dcef271
4
+ data.tar.gz: 7eee074d6ecf3a46798139fa6ed6fc36c975bcaf93059c52e68e597e7b1a87f3
5
5
  SHA512:
6
- metadata.gz: 043ed38d8ebdcf1af8506e83d8548b6590d923f0b4b1ef01687dca5c7528bc5d9a16c79c67e06dc626abe70334afd72882aefac2f15f8253c71d56bc40224274
7
- data.tar.gz: 0fefb80560e396290585e3677ca395abd1096511b78321c7ff05ff3add4b5bf0242a4c21ea3321244b7da5290651e369b376696400dc19429ecc547b1c534842
6
+ metadata.gz: 6c561f3cd1a83d7688291a2ef3dc23d42616bce723b5ff82a77a641104fae0731247c4cbc50cae2d9d5d5aa979c31ac8e90ff0b38f107235793bdfd7fda4019a
7
+ data.tar.gz: e42f707ec01db8ce171244e95ba378420bdf44c4a195c1f2f285ac7d56e91777c4750f5d5f4b05dc6ac60fb2f59beebaa50755f6c1ee3bb2de13957a7b44edab
@@ -1,9 +1,25 @@
1
1
  # TODO PDF 13: CJK text layout
2
2
 
3
- ## Status: PARTIAL — `Idml::TextEngine::CjkLayout` handles CJK glyph
4
- measurement and basic horizontal layout. Vertical writing mode,
5
- kinsoku shori, tate-chu-yoko, and ruby remain stretch goals as
6
- flagged in the original TODO. See `lib/idml/text_engine/cjk_layout.rb`.
3
+ ## Status: PARTIAL — horizontal-mode CJK complete (2026-08-17):
4
+ CJK glyph measurement, kinsoku shori, and ruby annotations render.
5
+ Vertical writing mode, tate-chu-yoko, and mojikumi remain OPEN
6
+ stretch goals they need a vertical (rotated) layout engine and
7
+ real vertical-mode fixtures to build against. See
8
+ `lib/idml/text_engine/cjk_layout.rb`.
9
+
10
+ ## Progress 2026-08-17
11
+
12
+ - Kinsoku shori is now LIVE in the pipeline: LineBreaker.break
13
+ post-processes any run containing CJK glyphs through
14
+ CjkLayout.apply_kinsoku, so no rendered line starts with
15
+ 、。,」etc. or ends with 「 etc. (previously the logic existed
16
+ but was never called).
17
+ - Ruby (phonetic annotations) render: CSR RubyString /
18
+ RubyFontSize / RubyPosition extract onto the run; the renderer
19
+ emits the annotation centered above the run's first line (half
20
+ the base size by default; Below* positions place it under).
21
+ Approximation: IDML attaches ruby to a character range; we
22
+ annotate the whole first line.
7
23
 
8
24
  ## Goal
9
25
 
@@ -25,6 +25,7 @@ module Idml
25
25
  # FontMetrics is available (no shaper → no wrap → rough output).
26
26
  class TextFrameRenderer
27
27
  DEFAULT_SIZE = 12.0
28
+ RUBY_SIZE_FACTOR = 0.5
28
29
 
29
30
  def self.render(canvas, context)
30
31
  frame = context.item
@@ -738,15 +739,26 @@ module Idml
738
739
  left_indent: left_indent,
739
740
  )
740
741
 
741
- positioned.each do |line|
742
+ emit_run_lines(canvas, positioned, run, context, size, font,
743
+ bottom_limit, char_cursor)
744
+ [positioned, next_y]
745
+ end
746
+ private_class_method :layout_run
747
+
748
+ # Emits the run's positioned lines (clipped at the bottom
749
+ # limit), recording positions and annotating the first line
750
+ # with the run's ruby when present.
751
+ def self.emit_run_lines(canvas, positioned, run, context, size,
752
+ font, bottom_limit, char_cursor)
753
+ positioned.each_with_index do |line, index|
742
754
  break if line.y < bottom_limit
743
755
 
744
756
  emit_line(canvas, line, run, context, size)
745
757
  record_position(context, line, size, char_cursor)
758
+ emit_ruby(canvas, run, line, size, font, context) if index.zero?
746
759
  end
747
- [positioned, next_y]
748
760
  end
749
- private_class_method :layout_run
761
+ private_class_method :emit_run_lines
750
762
 
751
763
  # Emits one positioned line: applies character-level styling
752
764
  # (fill color + tint, capitalization, position, tracking,
@@ -779,6 +791,36 @@ module Idml
779
791
  end
780
792
  private_class_method :emit_line
781
793
 
794
+ # Emits the run's ruby (phonetic annotation) above the first
795
+ # line of the annotated run, centered over the line's width.
796
+ # RubyFontSize defaults to half the base size; RubyPosition
797
+ # values containing "Below" place it under the base text.
798
+ # Approximation: IDML attaches ruby to a character range;
799
+ # we annotate the whole first line.
800
+ def self.emit_ruby(canvas, run, line, base_size, font, context)
801
+ ruby_text = run.ruby_string
802
+ return if ruby_text.nil? || ruby_text.empty?
803
+
804
+ ruby_size = run.ruby_font_size || (base_size * RUBY_SIZE_FACTOR)
805
+ glyphs = TextEngine::Shaper.shape(text: ruby_text, font: font,
806
+ size: ruby_size)
807
+ ruby_width = glyphs.sum(&:width)
808
+ x = line.x + ((line.width - ruby_width) / 2)
809
+ y = ruby_y(run, line, base_size, ruby_size)
810
+ canvas.text(ruby_text, at: [x, y],
811
+ font: font_for_run(run, context), size: ruby_size)
812
+ end
813
+ private_class_method :emit_ruby
814
+
815
+ def self.ruby_y(run, line, base_size, ruby_size)
816
+ if run.ruby_position.to_s.include?("Below")
817
+ line.y - (base_size * 0.25) - ruby_size
818
+ else
819
+ line.y + (base_size * 0.45) + ruby_size
820
+ end
821
+ end
822
+ private_class_method :ruby_y
823
+
782
824
  def self.leading_for(paragraph, size)
783
825
  TextEngine::VerticalLayout.leading_for(paragraph.auto_leading, size)
784
826
  end
@@ -43,6 +43,11 @@ module Idml
43
43
  :baseline_shift,
44
44
  :footnote_number,
45
45
  :footnote_paragraphs,
46
+ # Ruby (phonetic annotation) — from the CSR's Ruby*
47
+ # attributes. Present only on runs with a RubyString.
48
+ :ruby_string,
49
+ :ruby_font_size,
50
+ :ruby_position,
46
51
  keyword_init: true,
47
52
  )
48
53
 
@@ -348,6 +353,9 @@ module Idml
348
353
  :horizontal_scale),
349
354
  vertical_scale: resolve_csr(style_lookup, csr, :vertical_scale),
350
355
  baseline_shift: resolve_csr(style_lookup, csr, :baseline_shift),
356
+ ruby_string: resolve_csr(style_lookup, csr, :ruby_string),
357
+ ruby_font_size: resolve_csr(style_lookup, csr, :ruby_font_size),
358
+ ruby_position: resolve_csr(style_lookup, csr, :ruby_position),
351
359
  )
352
360
  end
353
361
  private_class_method :text_run
@@ -7,10 +7,20 @@ module Idml
7
7
 
8
8
  # Greedy word-wrap line breaker. Accumulates glyphs until
9
9
  # exceeding the frame width, then breaks at the last space.
10
+ # CJK runs get a kinsoku shori post-pass (no line starts or
11
+ # ends with a forbidden character).
10
12
  class LineBreaker
11
13
  def self.break(glyphs:, frame_width:)
12
- new(frame_width).break(glyphs)
14
+ lines = new(frame_width).break(glyphs)
15
+ return lines unless cjk_run?(glyphs)
16
+
17
+ CjkLayout.apply_kinsoku(lines)
18
+ end
19
+
20
+ def self.cjk_run?(glyphs)
21
+ glyphs.any? { |glyph| CjkLayout.cjk?(glyph.codepoint) }
13
22
  end
23
+ private_class_method :cjk_run?
14
24
 
15
25
  def initialize(frame_width)
16
26
  @frame_width = frame_width
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.0"
4
+ VERSION = "0.9.1"
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.9.0
4
+ version: 0.9.1
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-17 00:00:00.000000000 Z
11
+ date: 2026-08-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal