idml 0.10.13 → 0.10.15
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/154-vertical-keep-windows.md +21 -0
- data/TODO.pdf/155-next-column-jump.md +29 -0
- data/TODO.pdf/61-known-limitations.md +11 -13
- data/lib/idml/render/footnote.rb +1 -0
- data/lib/idml/render/renderers/text_frame_renderer.rb +38 -1
- data/lib/idml/render/renderers/vertical_emit.rb +93 -2
- data/lib/idml/render/style_resolver.rb +6 -0
- data/lib/idml/render/text_wrap_resolver.rb +36 -6
- data/lib/idml/text_engine/justifier.rb +16 -5
- data/lib/idml/version.rb +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 578a2a0fbd712f1931eaff74db316971441be4a967afd67d47ab55ecdc9679d1
|
|
4
|
+
data.tar.gz: e93c08f8694da2e0522a787ff0f58b4f607952f7024a0dd0d54bab42599ac8e9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 260e3fdbcd2339d2e461aae7f2992b311120a0b15544d48b65c39d915dee9fd0219fcb3ee6d9e07ce40eaeb9bf68846fa72bdf2475d9ce12d1982cdd1503afd5
|
|
7
|
+
data.tar.gz: 6d760f50d28216bbe1a620aa4ba2b43028ab3051384e0fee559764892f8495b04f5623b2a3ba14698f89cf6bf1e3de9f0426be158e3e7faf0d8d7a3b639e289e
|
|
@@ -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`
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# TODO PDF 154: Vertical keep windows
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE (approximation) — implemented 2026-09-01
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
KeepFirstLines / KeepLastLines windows (TODO 133) applied only to
|
|
8
|
+
the horizontal engine; vertical paragraphs ignored them.
|
|
9
|
+
|
|
10
|
+
## Solution
|
|
11
|
+
|
|
12
|
+
`VerticalEmit`'s pre-placement keep check (TODO 153) also honors
|
|
13
|
+
the windows on the same column-count approximation: a splitting
|
|
14
|
+
paragraph (needed columns > available) defers when fewer than
|
|
15
|
+
KeepFirstLines columns fit, or when the stranded tail would hold
|
|
16
|
+
fewer than KeepLastLines columns.
|
|
17
|
+
|
|
18
|
+
## Files
|
|
19
|
+
|
|
20
|
+
- `lib/idml/render/renderers/vertical_emit.rb`
|
|
21
|
+
- `spec/idml/render/vertical_render_spec.rb`
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# TODO PDF 155: NextColumn column jumping in multi-column frames
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-09-01
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
NextColumnTextWrap approximated as JumpObject-below (TODO 146):
|
|
8
|
+
in a multi-column frame, InDesign moves the text to the NEXT
|
|
9
|
+
COLUMN, not below the object.
|
|
10
|
+
|
|
11
|
+
## Solution
|
|
12
|
+
|
|
13
|
+
The wrap contour carries the mode: `jump` (JumpObjectTextWrap)
|
|
14
|
+
vs `next_column` (NextColumnTextWrap). Both block the full column
|
|
15
|
+
width; the renderer chooses the response —
|
|
16
|
+
`TextFrameRenderer#next_column_jump?` abandons the column when a
|
|
17
|
+
NextColumn contour blocks the run's band in a multi-column frame:
|
|
18
|
+
the cursor drops to the bottom limit mid-run, the unplaced runs
|
|
19
|
+
return to the chain state, and the next column (or frame)
|
|
20
|
+
resumes them. Single-column frames and JumpObject keep the
|
|
21
|
+
skip-below path (`jump_contour_bottom` reports the resume point
|
|
22
|
+
for both flavors).
|
|
23
|
+
|
|
24
|
+
## Files
|
|
25
|
+
|
|
26
|
+
- `lib/idml/render/text_wrap_resolver.rb`
|
|
27
|
+
- `lib/idml/render/renderers/text_frame_renderer.rb`
|
|
28
|
+
- `spec/idml/render/text_wrap_resolver_spec.rb`
|
|
29
|
+
- `spec/idml/render/jump_object_render_spec.rb` (end-to-end)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# TODO PDF 61: Known limitations and future work
|
|
2
2
|
|
|
3
|
-
## Status: DOCUMENTED — refreshed 2026-
|
|
3
|
+
## Status: DOCUMENTED — refreshed 2026-09-01
|
|
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.
|
|
@@ -26,11 +27,10 @@ this list reflects the current state.
|
|
|
26
27
|
odd/even page parity.
|
|
27
28
|
- **Text wrap**: BoundingBoxTextWrap, Contour, and Inverse modes
|
|
28
29
|
render (TODOs 130-131) with TextWrapSide side-awareness for both
|
|
29
|
-
box contours and Contour shapes (TODOs 138/147); JumpObject
|
|
30
|
-
|
|
31
|
-
in
|
|
32
|
-
(
|
|
33
|
-
BothSides.
|
|
30
|
+
box contours and Contour shapes (TODOs 138/147); JumpObject moves
|
|
31
|
+
text below the object (TODO 143); NextColumn jumps to the next
|
|
32
|
+
column in multi-column frames and below the object otherwise
|
|
33
|
+
(TODOs 146/155); spine variants approximate as BothSides.
|
|
34
34
|
|
|
35
35
|
### CJK
|
|
36
36
|
- **Mojikumi**: script spacing (TODO 125, now actually wired),
|
|
@@ -39,8 +39,8 @@ this list reflects the current state.
|
|
|
39
39
|
Desired applies per class pair) are applied; Minimum/Maximum
|
|
40
40
|
aki bounds and CompressionPriority are parsed but unused.
|
|
41
41
|
- **Vertical mode**: Latin runs rotate as one group per segment
|
|
42
|
-
(TODO 142); ruby placement is beside-column; keep options
|
|
43
|
-
|
|
42
|
+
(TODO 142); ruby placement is beside-column; keep options apply
|
|
43
|
+
with a column-count approximation (TODOs 153-154).
|
|
44
44
|
|
|
45
45
|
### Structural features
|
|
46
46
|
- **Endnotes** (TODO 117): reference markers render and endnote TEXT
|
|
@@ -60,7 +60,5 @@ this list reflects the current state.
|
|
|
60
60
|
## Future enhancements
|
|
61
61
|
|
|
62
62
|
- Hyphenation dictionary integration
|
|
63
|
-
- NextColumn wrap via column-jump chaining (multi-column frames)
|
|
64
63
|
- Minimum/Maximum mojikumi aki bounds + CompressionPriority
|
|
65
|
-
- ToBinding /
|
|
66
|
-
- Vertical-mode keep options
|
|
64
|
+
- ToBinding / kashida Arabic justification
|
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
|
)
|
|
@@ -543,6 +543,11 @@ module Idml
|
|
|
543
543
|
context, layout_frame, cursor_y, run
|
|
544
544
|
)
|
|
545
545
|
run_wrap -= wrap_reduction
|
|
546
|
+
if next_column_jump?(context, layout_frame, cursor_y, run,
|
|
547
|
+
run_wrap)
|
|
548
|
+
cursor_y = bottom_limit
|
|
549
|
+
break
|
|
550
|
+
end
|
|
546
551
|
cursor_y, run_wrap, wrap_shift = skip_below_jump_object(
|
|
547
552
|
context, layout_frame, cursor_y, run, run_wrap, wrap_shift,
|
|
548
553
|
wrap_reduction, bottom_limit
|
|
@@ -612,6 +617,37 @@ module Idml
|
|
|
612
617
|
end
|
|
613
618
|
private_class_method :render_footnote_entries
|
|
614
619
|
|
|
620
|
+
# NextColumnTextWrap in a multi-column frame (TODO 155):
|
|
621
|
+
# abandon the column — the unplaced runs return to the
|
|
622
|
+
# chain state and the next column resumes them. Falls
|
|
623
|
+
# through to skip-below everywhere else (single-column
|
|
624
|
+
# frames included).
|
|
625
|
+
def self.next_column_jump?(context, layout_frame, cursor_y, run,
|
|
626
|
+
run_wrap)
|
|
627
|
+
size = run.point_size || DEFAULT_SIZE
|
|
628
|
+
return false unless run_wrap < size
|
|
629
|
+
return false unless multi_column_frame?(context)
|
|
630
|
+
|
|
631
|
+
blocked_in_band?(context, layout_frame, cursor_y, size)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def self.multi_column_frame?(context)
|
|
635
|
+
text_column_count(context).to_i > 1
|
|
636
|
+
end
|
|
637
|
+
private_class_method :multi_column_frame?
|
|
638
|
+
|
|
639
|
+
def self.blocked_in_band?(context, layout_frame, cursor_y, size)
|
|
640
|
+
resolver = context.text_wrap_resolver
|
|
641
|
+
return false unless resolver
|
|
642
|
+
|
|
643
|
+
frame_x = layout_frame.x + (layout_frame.inset_left || 0)
|
|
644
|
+
frame_right = layout_frame.x + layout_frame.width -
|
|
645
|
+
(layout_frame.inset_right || 0)
|
|
646
|
+
resolver.next_column_block?(cursor_y, size, frame_x, frame_right)
|
|
647
|
+
end
|
|
648
|
+
private_class_method :blocked_in_band?
|
|
649
|
+
private_class_method :next_column_jump?
|
|
650
|
+
|
|
615
651
|
# JumpObjectTextWrap blocks the full column width: when the
|
|
616
652
|
# run's band is fully blocked, move the cursor below the
|
|
617
653
|
# object's bottom edge and re-measure once. InDesign's
|
|
@@ -853,7 +889,8 @@ module Idml
|
|
|
853
889
|
line: line, frame_width: wrap_width,
|
|
854
890
|
alignment: paragraph.alignment || :left,
|
|
855
891
|
last_line: paragraph_last && index == lines.length - 1,
|
|
856
|
-
limits: limits
|
|
892
|
+
limits: limits,
|
|
893
|
+
direction: paragraph.paragraph_direction
|
|
857
894
|
)
|
|
858
895
|
end
|
|
859
896
|
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,87 @@ 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
|
+
room = vertical_room(layout_frame, paragraph, left_limit,
|
|
75
|
+
next_column)
|
|
76
|
+
return true if keep_all_deferred?(paragraph, needed, room)
|
|
77
|
+
return true if window_deferred?(paragraph, needed, room)
|
|
78
|
+
|
|
79
|
+
with_next_deferred?(paragraph, next_paragraph, layout_frame,
|
|
80
|
+
left_limit, next_column, needed)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def keep_flagged?(paragraph)
|
|
84
|
+
paragraph.keep_all_lines_together ||
|
|
85
|
+
!paragraph.keep_with_next.nil? ||
|
|
86
|
+
paragraph.keep_first_lines ||
|
|
87
|
+
paragraph.keep_last_lines
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def keep_all_deferred?(paragraph, needed, room)
|
|
91
|
+
paragraph.keep_all_lines_together && needed > room
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Keep windows on the same column-count approximation:
|
|
95
|
+
# defer a splitting paragraph when fewer than
|
|
96
|
+
# KeepFirstLines columns fit, or when the stranded tail
|
|
97
|
+
# would hold fewer than KeepLastLines columns.
|
|
98
|
+
def window_deferred?(paragraph, needed, room)
|
|
99
|
+
return false unless needed > room
|
|
100
|
+
|
|
101
|
+
if paragraph.keep_first_lines &&
|
|
102
|
+
room < paragraph.keep_first_lines
|
|
103
|
+
return true
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
paragraph.keep_last_lines &&
|
|
107
|
+
(needed - room) < paragraph.keep_last_lines
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def with_next_deferred?(paragraph, next_paragraph, layout_frame,
|
|
111
|
+
left_limit, next_column, needed)
|
|
112
|
+
return false if paragraph.keep_with_next.nil?
|
|
113
|
+
|
|
114
|
+
follower_needed = [next_paragraph&.runs&.length || 1, 1].max
|
|
115
|
+
follower_break = KeepPolicy.paragraph_break?(next_paragraph)
|
|
116
|
+
room = vertical_room(layout_frame, paragraph, left_limit,
|
|
117
|
+
next_column)
|
|
118
|
+
follower_break || needed + follower_needed > room
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def vertical_room(layout_frame, paragraph, left_limit,
|
|
122
|
+
next_column)
|
|
123
|
+
leading = TextEngine::VerticalLayout.leading_for(
|
|
124
|
+
paragraph.auto_leading,
|
|
125
|
+
paragraph.runs.first&.point_size ||
|
|
126
|
+
TextFrameRenderer::DEFAULT_SIZE,
|
|
127
|
+
)
|
|
128
|
+
vertical_available_columns(layout_frame, leading, left_limit,
|
|
129
|
+
next_column)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def vertical_available_columns(layout_frame, leading, left_limit,
|
|
133
|
+
next_column)
|
|
134
|
+
right = layout_frame.x + layout_frame.width -
|
|
135
|
+
(layout_frame.inset_right || 0)
|
|
136
|
+
(((right - left_limit) / leading).floor - next_column).to_i
|
|
137
|
+
end
|
|
138
|
+
|
|
48
139
|
# Folds a chain-resumed paragraph (runs_remaining) back to
|
|
49
140
|
# the head of the paragraph list so the vertical path sees a
|
|
50
141
|
# 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,
|
|
@@ -18,7 +18,7 @@ module Idml
|
|
|
18
18
|
class TextWrapResolver
|
|
19
19
|
# A wrap contour: a PDF-coordinate rectangle the text avoids.
|
|
20
20
|
Contour = Struct.new(
|
|
21
|
-
:x, :y, :width, :height, :inverse, :side, :jump,
|
|
21
|
+
:x, :y, :width, :height, :inverse, :side, :jump, :next_column,
|
|
22
22
|
keyword_init: true
|
|
23
23
|
)
|
|
24
24
|
|
|
@@ -100,9 +100,10 @@ module Idml
|
|
|
100
100
|
return [0.0, 0.0] if overlap.zero?
|
|
101
101
|
|
|
102
102
|
# JumpObject / NextColumn: text never flows beside the
|
|
103
|
-
# object — the full frame width is blocked
|
|
104
|
-
# skips
|
|
105
|
-
|
|
103
|
+
# object — the full frame width is blocked. The renderer
|
|
104
|
+
# either skips below (JumpObject) or jumps column
|
|
105
|
+
# (NextColumn in multi-column frames).
|
|
106
|
+
return [frame_right - frame_x, 0.0] if blocks_full_width?(contour)
|
|
106
107
|
|
|
107
108
|
interval_side_adjustment(contour.side, contour.x,
|
|
108
109
|
contour.x + contour.width, overlap,
|
|
@@ -115,7 +116,7 @@ module Idml
|
|
|
115
116
|
# jump contour blocks the band.
|
|
116
117
|
def jump_contour_bottom(line_y, line_height, frame_x, frame_right)
|
|
117
118
|
bottoms = @contours.filter_map do |contour|
|
|
118
|
-
next unless
|
|
119
|
+
next unless skip_contour?(contour)
|
|
119
120
|
next unless y_overlap?(contour, line_y, line_height) &&
|
|
120
121
|
x_overlap?(contour, frame_x, frame_right)
|
|
121
122
|
|
|
@@ -124,6 +125,25 @@ module Idml
|
|
|
124
125
|
bottoms.min
|
|
125
126
|
end
|
|
126
127
|
|
|
128
|
+
# Contours whose block resolves as skip-below (both jump
|
|
129
|
+
# flavors report a resume point; the renderer chooses).
|
|
130
|
+
def skip_contour?(contour)
|
|
131
|
+
box_contour?(contour) &&
|
|
132
|
+
(contour.jump || contour.next_column)
|
|
133
|
+
end
|
|
134
|
+
private :skip_contour?
|
|
135
|
+
|
|
136
|
+
# True when a NextColumn contour blocks the given band: in a
|
|
137
|
+
# multi-column frame the renderer abandons the column and
|
|
138
|
+
# the chain resumes in the next one.
|
|
139
|
+
def next_column_block?(line_y, line_height, frame_x, frame_right)
|
|
140
|
+
@contours.any? do |contour|
|
|
141
|
+
box_contour?(contour) && contour.next_column &&
|
|
142
|
+
y_overlap?(contour, line_y, line_height) &&
|
|
143
|
+
x_overlap?(contour, frame_x, frame_right)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
127
147
|
def box_contour?(contour)
|
|
128
148
|
!contour.is_a?(WrapContour::Shape)
|
|
129
149
|
end
|
|
@@ -136,6 +156,14 @@ module Idml
|
|
|
136
156
|
# shift); RightSide moves text past its right edge (reduce +
|
|
137
157
|
# shift); LargestArea picks the roomier side; BothSides and
|
|
138
158
|
# the spine variants reduce by `overlap` only.
|
|
159
|
+
# JumpObject / NextColumn both forbid text beside the
|
|
160
|
+
# object; the renderer decides between skip-below and
|
|
161
|
+
# column jump.
|
|
162
|
+
def blocks_full_width?(contour)
|
|
163
|
+
contour.jump || contour.next_column
|
|
164
|
+
end
|
|
165
|
+
private :blocks_full_width?
|
|
166
|
+
|
|
139
167
|
def interval_side_adjustment(side, obj_left, obj_right, overlap,
|
|
140
168
|
frame_x, frame_right)
|
|
141
169
|
case side
|
|
@@ -185,11 +213,13 @@ module Idml
|
|
|
185
213
|
|
|
186
214
|
rect = Geometry.placement_rect(bounds, item.item_transform,
|
|
187
215
|
page_height)
|
|
216
|
+
mode = pref&.text_wrap_mode
|
|
188
217
|
Contour.new(
|
|
189
218
|
x: rect[:x], y: rect[:y],
|
|
190
219
|
width: rect[:width], height: rect[:height],
|
|
191
220
|
side: pref&.text_wrap_side,
|
|
192
|
-
jump:
|
|
221
|
+
jump: mode == "JumpObjectTextWrap",
|
|
222
|
+
next_column: mode == "NextColumnTextWrap"
|
|
193
223
|
)
|
|
194
224
|
end
|
|
195
225
|
private_class_method :box_contour
|
|
@@ -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.15
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -216,6 +216,10 @@ 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
|
|
221
|
+
- TODO.pdf/154-vertical-keep-windows.md
|
|
222
|
+
- TODO.pdf/155-next-column-jump.md
|
|
219
223
|
- TODO.pdf/16-typed-model-pipeline.md
|
|
220
224
|
- TODO.pdf/17-shape-rendering.md
|
|
221
225
|
- TODO.pdf/18-text-rendering.md
|