idml 0.10.13 → 0.10.14
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/152-rtl-alignment.md +27 -0
- data/TODO.pdf/153-vertical-keeps.md +31 -0
- data/TODO.pdf/61-known-limitations.md +9 -7
- data/lib/idml/render/footnote.rb +1 -0
- data/lib/idml/render/renderers/text_frame_renderer.rb +2 -1
- data/lib/idml/render/renderers/vertical_emit.rb +70 -2
- data/lib/idml/render/style_resolver.rb +6 -0
- data/lib/idml/text_engine/justifier.rb +16 -5
- data/lib/idml/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00f5eb099cc78c69be52672e179bc0d2c10b102554bb74b28272b04d0ed904c6
|
|
4
|
+
data.tar.gz: c7936ca3c76fef90639d532cc70c0deb7423b2dcd9cd3d73ce6b88bff5a1257c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 354bb00de55fe629d84853a8e1dfdc2c8b821f708fc15584cf9ac509ba3489a56f92f48f71698cfcf9b28c9f3814b4c0366f3a88d40aa3e2e05b33b242e20a60
|
|
7
|
+
data.tar.gz: 52cc665c20cdf7c8c766593e38fd18b847f15da0ac3a09b1accfd5dbbe2b48614a7eadb5452b8f4b295bff1870c7a2acb1a48a0c03174f15916f681d62c4a16c
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# TODO PDF 152: RTL paragraph alignment
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-31
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
ParagraphDirection (LeftToRightDirection /
|
|
8
|
+
RightToLeftDirection) was parsed but unused: RTL paragraphs
|
|
9
|
+
aligned left/right by LTR convention, so Arabic/Hebrew text sat
|
|
10
|
+
on the wrong edge.
|
|
11
|
+
|
|
12
|
+
## Solution
|
|
13
|
+
|
|
14
|
+
`StyleResolver::Paragraph` carries `paragraph_direction` (PSR or
|
|
15
|
+
paragraph style); `TextEngine::Justifier` gains a `direction:`
|
|
16
|
+
argument that mirrors the visual alignment under
|
|
17
|
+
RightToLeftDirection — left↔right, start↔end; center and
|
|
18
|
+
justified unchanged. Both justify call sites (frame renderer and
|
|
19
|
+
footnote layout) pass the paragraph's direction.
|
|
20
|
+
|
|
21
|
+
## Files
|
|
22
|
+
|
|
23
|
+
- `lib/idml/text_engine/justifier.rb`
|
|
24
|
+
- `lib/idml/render/style_resolver.rb`
|
|
25
|
+
- `lib/idml/render/renderers/text_frame_renderer.rb`
|
|
26
|
+
- `lib/idml/render/footnote.rb`
|
|
27
|
+
- `spec/idml/text_engine/text_engine_spec.rb`
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# TODO PDF 153: Keep options in vertical writing
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE (approximation) — implemented 2026-08-31
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
Keep options (TODOs 123/127/133) applied only to the horizontal
|
|
8
|
+
engine; vertical paragraphs split across frames regardless of
|
|
9
|
+
KeepAllLinesTogether / KeepWithNext.
|
|
10
|
+
|
|
11
|
+
## Solution
|
|
12
|
+
|
|
13
|
+
`VerticalEmit.vertical_render` consults a keep check BEFORE
|
|
14
|
+
placing each paragraph (deferral after placement cannot un-emit):
|
|
15
|
+
|
|
16
|
+
- **KeepAllLinesTogether** defers the paragraph wholly to the
|
|
17
|
+
next frame when its column need exceeds the remaining columns.
|
|
18
|
+
- **KeepWithNext** also defers when the follower's forced break
|
|
19
|
+
or column need would strand it.
|
|
20
|
+
- Never defers the frame's first paragraph — the same progress
|
|
21
|
+
guarantee as KeepPolicy (shared: `KeepPolicy.paragraph_break?`).
|
|
22
|
+
|
|
23
|
+
Approximation: column need = runs count (one column per run),
|
|
24
|
+
which under-estimates runs spanning several columns — deferral
|
|
25
|
+
errs toward placing. KeepFirstLines/KeepLastLines windows remain
|
|
26
|
+
horizontal-only (they are line-window concepts).
|
|
27
|
+
|
|
28
|
+
## Files
|
|
29
|
+
|
|
30
|
+
- `lib/idml/render/renderers/vertical_emit.rb`
|
|
31
|
+
- `spec/idml/render/vertical_render_spec.rb`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TODO PDF 61: Known limitations and future work
|
|
2
2
|
|
|
3
|
-
## Status: DOCUMENTED — refreshed 2026-08-
|
|
3
|
+
## Status: DOCUMENTED — refreshed 2026-08-31
|
|
4
4
|
|
|
5
5
|
Previously listed limitations (font embedding, tagged PDF, per-run
|
|
6
6
|
styling, dead code) were resolved by TODOs 52/53/55/67/76 long ago;
|
|
@@ -14,8 +14,9 @@ this list reflects the current state.
|
|
|
14
14
|
wrap after explicit hyphens (TODO 139).
|
|
15
15
|
- **Justification**: word/letter spacing caps apply (TODO 119),
|
|
16
16
|
MaximumGlyphScaling stretches (TODO 129) and MinimumGlyphScaling
|
|
17
|
-
compresses overlong lines (TODO 135);
|
|
18
|
-
alignment
|
|
17
|
+
compresses overlong lines (TODO 135); RTL paragraphs mirror
|
|
18
|
+
left/right alignment (TODO 152); ToBinding / kashida-based
|
|
19
|
+
Arabic justification is not applied.
|
|
19
20
|
- **Keep options**: KeepAllLinesTogether (TODO 123), KeepWithNext
|
|
20
21
|
(TODO 127), and the KeepFirstLines / KeepLastLines windows
|
|
21
22
|
(TODO 133, line-count approximation) are honored.
|
|
@@ -39,8 +40,9 @@ this list reflects the current state.
|
|
|
39
40
|
Desired applies per class pair) are applied; Minimum/Maximum
|
|
40
41
|
aki bounds and CompressionPriority are parsed but unused.
|
|
41
42
|
- **Vertical mode**: Latin runs rotate as one group per segment
|
|
42
|
-
(TODO 142); ruby placement is beside-column;
|
|
43
|
-
|
|
43
|
+
(TODO 142); ruby placement is beside-column; KeepAllLines /
|
|
44
|
+
KeepWithNext apply with a column-count approximation (TODO 153);
|
|
45
|
+
keep windows are horizontal-only.
|
|
44
46
|
|
|
45
47
|
### Structural features
|
|
46
48
|
- **Endnotes** (TODO 117): reference markers render and endnote TEXT
|
|
@@ -62,5 +64,5 @@ this list reflects the current state.
|
|
|
62
64
|
- Hyphenation dictionary integration
|
|
63
65
|
- NextColumn wrap via column-jump chaining (multi-column frames)
|
|
64
66
|
- Minimum/Maximum mojikumi aki bounds + CompressionPriority
|
|
65
|
-
- ToBinding /
|
|
66
|
-
- Vertical
|
|
67
|
+
- ToBinding / kashida Arabic justification
|
|
68
|
+
- Vertical keep windows (KeepFirstLines / KeepLastLines)
|
data/lib/idml/render/footnote.rb
CHANGED
|
@@ -138,6 +138,7 @@ module Idml
|
|
|
138
138
|
TextEngine::Justifier.justify(
|
|
139
139
|
line: line, frame_width: wrap_width,
|
|
140
140
|
alignment: paragraph.alignment || :left,
|
|
141
|
+
direction: paragraph.paragraph_direction,
|
|
141
142
|
last_line: last_line?(paragraph, run, index, lines),
|
|
142
143
|
limits: limits
|
|
143
144
|
)
|
|
@@ -853,7 +853,8 @@ module Idml
|
|
|
853
853
|
line: line, frame_width: wrap_width,
|
|
854
854
|
alignment: paragraph.alignment || :left,
|
|
855
855
|
last_line: paragraph_last && index == lines.length - 1,
|
|
856
|
-
limits: limits
|
|
856
|
+
limits: limits,
|
|
857
|
+
direction: paragraph.paragraph_direction
|
|
857
858
|
)
|
|
858
859
|
end
|
|
859
860
|
end
|
|
@@ -24,10 +24,20 @@ module Idml
|
|
|
24
24
|
layout_frame = layout_frame(context.item, box)
|
|
25
25
|
left_limit = layout_frame.x + (layout_frame.inset_left || 0)
|
|
26
26
|
resume_vertical_paragraph(state)
|
|
27
|
-
|
|
27
|
+
entry_offset = state.column_offset || 0
|
|
28
|
+
next_column = entry_offset
|
|
28
29
|
pending = 0
|
|
29
30
|
|
|
30
|
-
state.paragraphs.
|
|
31
|
+
state.paragraphs.each_with_index do |paragraph, index|
|
|
32
|
+
if vertical_keep_deferred?(paragraph, state.paragraphs[index + 1],
|
|
33
|
+
layout_frame, font, left_limit,
|
|
34
|
+
next_column, pending, entry_offset)
|
|
35
|
+
state.current_paragraph = nil
|
|
36
|
+
state.runs_remaining = []
|
|
37
|
+
state.column_offset = 0
|
|
38
|
+
break
|
|
39
|
+
end
|
|
40
|
+
|
|
31
41
|
remaining_runs, next_column = vertical_paragraph(
|
|
32
42
|
canvas, paragraph, context, layout_frame, font,
|
|
33
43
|
left_limit, next_column
|
|
@@ -45,6 +55,64 @@ module Idml
|
|
|
45
55
|
state.paragraphs = state.paragraphs[pending..]
|
|
46
56
|
end
|
|
47
57
|
|
|
58
|
+
# Keep options in vertical mode (TODO 153, column-count
|
|
59
|
+
# approximation — one column per run under-estimates runs
|
|
60
|
+
# that span several columns, so deferral errs toward
|
|
61
|
+
# placing): KeepAllLinesTogether defers the paragraph
|
|
62
|
+
# wholly to the next frame; KeepWithNext also defers when
|
|
63
|
+
# the follower (or its forced break) would strand. Never
|
|
64
|
+
# defers the frame's first paragraph — the progress
|
|
65
|
+
# guarantee, shared with KeepPolicy.
|
|
66
|
+
def vertical_keep_deferred?(paragraph, next_paragraph,
|
|
67
|
+
layout_frame, _font, left_limit,
|
|
68
|
+
next_column, pending, entry_offset)
|
|
69
|
+
placed = pending.positive? || entry_offset.positive?
|
|
70
|
+
return false unless placed
|
|
71
|
+
return false unless keep_flagged?(paragraph)
|
|
72
|
+
|
|
73
|
+
needed = [paragraph.runs.length, 1].max
|
|
74
|
+
return true if paragraph.keep_all_lines_together &&
|
|
75
|
+
needed > vertical_room(layout_frame, paragraph, left_limit,
|
|
76
|
+
next_column)
|
|
77
|
+
|
|
78
|
+
with_next_deferred?(paragraph, next_paragraph, layout_frame,
|
|
79
|
+
left_limit, next_column, needed)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def keep_flagged?(paragraph)
|
|
83
|
+
paragraph.keep_all_lines_together ||
|
|
84
|
+
!paragraph.keep_with_next.nil?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def with_next_deferred?(paragraph, next_paragraph, layout_frame,
|
|
88
|
+
left_limit, next_column, needed)
|
|
89
|
+
return false if paragraph.keep_with_next.nil?
|
|
90
|
+
|
|
91
|
+
follower_needed = [next_paragraph&.runs&.length || 1, 1].max
|
|
92
|
+
follower_break = KeepPolicy.paragraph_break?(next_paragraph)
|
|
93
|
+
room = vertical_room(layout_frame, paragraph, left_limit,
|
|
94
|
+
next_column)
|
|
95
|
+
follower_break || needed + follower_needed > room
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def vertical_room(layout_frame, paragraph, left_limit,
|
|
99
|
+
next_column)
|
|
100
|
+
leading = TextEngine::VerticalLayout.leading_for(
|
|
101
|
+
paragraph.auto_leading,
|
|
102
|
+
paragraph.runs.first&.point_size ||
|
|
103
|
+
TextFrameRenderer::DEFAULT_SIZE,
|
|
104
|
+
)
|
|
105
|
+
vertical_available_columns(layout_frame, leading, left_limit,
|
|
106
|
+
next_column)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def vertical_available_columns(layout_frame, leading, left_limit,
|
|
110
|
+
next_column)
|
|
111
|
+
right = layout_frame.x + layout_frame.width -
|
|
112
|
+
(layout_frame.inset_right || 0)
|
|
113
|
+
(((right - left_limit) / leading).floor - next_column).to_i
|
|
114
|
+
end
|
|
115
|
+
|
|
48
116
|
# Folds a chain-resumed paragraph (runs_remaining) back to
|
|
49
117
|
# the head of the paragraph list so the vertical path sees a
|
|
50
118
|
# uniform sequence. The consumed column offset persists in
|
|
@@ -70,6 +70,9 @@ module Idml
|
|
|
70
70
|
:maximum_letter_spacing,
|
|
71
71
|
:maximum_glyph_scaling,
|
|
72
72
|
:minimum_glyph_scaling,
|
|
73
|
+
# ParagraphDirection (LeftToRightDirection /
|
|
74
|
+
# RightToLeftDirection) — mirrors alignment under RTL.
|
|
75
|
+
:paragraph_direction,
|
|
73
76
|
# Forced paragraph break (StartParagraph: NextPage /
|
|
74
77
|
# NextColumn / NextFrame / NextOddPage / NextEvenPage).
|
|
75
78
|
:start_paragraph,
|
|
@@ -197,6 +200,9 @@ module Idml
|
|
|
197
200
|
paragraph = Paragraph.new(
|
|
198
201
|
runs: runs,
|
|
199
202
|
alignment: runs.first.alignment,
|
|
203
|
+
paragraph_direction: resolve_attr(
|
|
204
|
+
style_lookup, psr, :paragraph_direction
|
|
205
|
+
),
|
|
200
206
|
space_before: resolve_attr(style_lookup, psr, :space_before),
|
|
201
207
|
space_after: resolve_attr(style_lookup, psr, :space_after),
|
|
202
208
|
first_line_indent: resolve_attr(style_lookup, psr,
|
|
@@ -21,13 +21,13 @@ module Idml
|
|
|
21
21
|
DEFAULT_MIN_GLYPH_SCALING = 100.0
|
|
22
22
|
|
|
23
23
|
def self.justify(line:, frame_width:, alignment: :left,
|
|
24
|
-
last_line: false, limits: nil)
|
|
25
|
-
new(frame_width, alignment, last_line, limits).justify(line)
|
|
24
|
+
last_line: false, limits: nil, direction: nil)
|
|
25
|
+
new(frame_width, alignment, last_line, limits, direction).justify(line)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def initialize(frame_width, alignment, last_line, limits)
|
|
28
|
+
def initialize(frame_width, alignment, last_line, limits, direction)
|
|
29
29
|
@frame_width = frame_width
|
|
30
|
-
@alignment = alignment
|
|
30
|
+
@alignment = mirrored_alignment(alignment, direction)
|
|
31
31
|
@last_line = last_line
|
|
32
32
|
@limits = limits
|
|
33
33
|
end
|
|
@@ -44,7 +44,18 @@ module Idml
|
|
|
44
44
|
line
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
RTL_ALIGNMENT_MIRROR = {
|
|
48
|
+
left: :right, right: :left,
|
|
49
|
+
start: :end, end: :start
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
# RightToLeftDirection mirrors the visual alignment: what is
|
|
53
|
+
# "left" in an LTR paragraph sits right in an RTL one.
|
|
54
|
+
def mirrored_alignment(alignment, direction)
|
|
55
|
+
return alignment unless direction == "RightToLeftDirection"
|
|
56
|
+
|
|
57
|
+
RTL_ALIGNMENT_MIRROR[alignment] || alignment
|
|
58
|
+
end
|
|
48
59
|
|
|
49
60
|
# The paragraph's last line stays ragged under full
|
|
50
61
|
# justification.
|
data/lib/idml/version.rb
CHANGED
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.10.
|
|
4
|
+
version: 0.10.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -216,6 +216,8 @@ files:
|
|
|
216
216
|
- TODO.pdf/15-wire-spread-children.md
|
|
217
217
|
- TODO.pdf/150-architecture-round-three.md
|
|
218
218
|
- TODO.pdf/151-schema-conformance.md
|
|
219
|
+
- TODO.pdf/152-rtl-alignment.md
|
|
220
|
+
- TODO.pdf/153-vertical-keeps.md
|
|
219
221
|
- TODO.pdf/16-typed-model-pipeline.md
|
|
220
222
|
- TODO.pdf/17-shape-rendering.md
|
|
221
223
|
- TODO.pdf/18-text-rendering.md
|