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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a82b32708ab694d0d794c39f33d74de7b39a9bdc38b7a6315b99a2156dcef271
|
|
4
|
+
data.tar.gz: 7eee074d6ecf3a46798139fa6ed6fc36c975bcaf93059c52e68e597e7b1a87f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 —
|
|
4
|
-
measurement and
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
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 :
|
|
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
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.
|
|
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-
|
|
11
|
+
date: 2026-08-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|