idml 0.9.6 → 0.9.8
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/123-keep-options.md +24 -0
- data/TODO.pdf/124-tatechuyoko-vertical.md +23 -0
- data/TODO.pdf/125-script-spacing.md +22 -0
- data/TODO.pdf/126-part-cache.md +22 -0
- data/TODO.pdf/13-cjk-text-layout.md +5 -3
- data/lib/idml/package.rb +10 -0
- data/lib/idml/render/renderers/text_frame_renderer.rb +94 -17
- data/lib/idml/render/story_chain_controller.rb +3 -0
- data/lib/idml/render/style_resolver.rb +8 -0
- data/lib/idml/text_engine/cjk_layout.rb +32 -0
- data/lib/idml/text_engine/vertical_text_layout.rb +33 -6
- 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: 29f97721ebdd63304b8c879eaa45764906dfa40b85363688aa9d860ccdd6f62c
|
|
4
|
+
data.tar.gz: 439a3e3454be081957617ce5107ae735a47d56c4dc206104d3e483c5a20db669
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c669bcf817d1e2b8ab74c116e3a0e472b8ca5529fdfeba4f0adb85d7f1ddb78da5d422c63907cbe68f4a7c381ce5862d17b674063070bce64e9664584159b97
|
|
7
|
+
data.tar.gz: b427dc2a77703a71074fc541eb8eea36485dc78607dea9faa8f13d3ca69c70e013a749374ef8abca81057bdf5e7563f6717bea30e45ad967a5836ead3e325d6a
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# TODO PDF 123: Keep options (widow/orphan control)
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-20
|
|
4
|
+
|
|
5
|
+
## What was done
|
|
6
|
+
|
|
7
|
+
- `StyleResolver::Paragraph` carries `keep_all_lines_together`
|
|
8
|
+
from the PSR.
|
|
9
|
+
- `TextFrameRenderer.consume_paragraphs` pre-measures the paragraph
|
|
10
|
+
(TextEngine::Measurement) and, when KeepAllLinesTogether is set,
|
|
11
|
+
the paragraph cannot fully fit the remaining frame space, and at
|
|
12
|
+
least one paragraph was already placed, pushes the whole
|
|
13
|
+
paragraph to the next frame via the chain state — no widowed
|
|
14
|
+
first lines. The first paragraph in a frame never pushes, so
|
|
15
|
+
progress is always made.
|
|
16
|
+
|
|
17
|
+
## Known limitations
|
|
18
|
+
|
|
19
|
+
- KeepLinesTogether with per-side KeepFirstLines / KeepLastLines
|
|
20
|
+
counts (partial keep windows) is not modeled — only the
|
|
21
|
+
whole-paragraph KeepAllLinesTogether form.
|
|
22
|
+
- KeepWithNext (binding a paragraph to the next) is not modeled.
|
|
23
|
+
- Vertical writing mode does not apply keep options (column model
|
|
24
|
+
differs).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# TODO PDF 124: Tate-chu-yoko and vertical multi-run columns
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-20
|
|
4
|
+
|
|
5
|
+
## What was done
|
|
6
|
+
|
|
7
|
+
- Multi-run column continuity (bug fix): every run in a vertical
|
|
8
|
+
frame used to restart at column 0, overlapping earlier runs.
|
|
9
|
+
`VerticalTextLayout.layout` takes `start_column`; the frame
|
|
10
|
+
renderer threads the next-unused column across runs and
|
|
11
|
+
paragraphs, and persists it in the chain state (`column_offset`)
|
|
12
|
+
so vertical text continues correctly into the next frame of a
|
|
13
|
+
chain.
|
|
14
|
+
- Tate-chu-yoko (`Tatechuyoko="true"` on the CSR — note the
|
|
15
|
+
schema's lowercase c): the run's glyphs render HORIZONTALLY,
|
|
16
|
+
sharing one baseline at the top of their column slot and centered
|
|
17
|
+
in it, consuming one column. Groups wider than the column height
|
|
18
|
+
fall back to stacked layout. StyledRun carries `tatechuyoko`.
|
|
19
|
+
|
|
20
|
+
## Remaining from TODO 13
|
|
21
|
+
|
|
22
|
+
Mojikumi (punctuation spacing classes) is the last documented
|
|
23
|
+
stretch goal.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# TODO PDF 125: CJK/Latin automatic script spacing (mojikumi subset)
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-20
|
|
4
|
+
|
|
5
|
+
## What was done
|
|
6
|
+
|
|
7
|
+
- `TextEngine::CjkLayout.apply_script_spacing` widens the leading
|
|
8
|
+
glyph by an eighth em at every CJK ↔ ASCII alphanumeric boundary
|
|
9
|
+
— InDesign's default inter-script auto spacing (a mojikumi
|
|
10
|
+
behavior).
|
|
11
|
+
- Wired inside `TextEngine::Shaper#shape`, so every shaping path
|
|
12
|
+
(horizontal frame text, footnotes, ruby, vertical columns)
|
|
13
|
+
applies it uniformly — one integration point, no per-caller
|
|
14
|
+
changes.
|
|
15
|
+
- Punctuation neighbors are ignored (only letters/digits form
|
|
16
|
+
boundaries).
|
|
17
|
+
|
|
18
|
+
## Remaining from TODO 13
|
|
19
|
+
|
|
20
|
+
Class-based mojikumi (per-pair punctuation compression tables,
|
|
21
|
+
line-end punctuation hanging beyond half-em) remains a documented
|
|
22
|
+
stretch goal.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# TODO PDF 126: Package#part memoization
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-20
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
`Package#part(name)` re-read and re-parsed the zip entry on every
|
|
8
|
+
call. Rendering resolves `story_by_id` per frame plus preferences
|
|
9
|
+
(Footnote.option), styles, and graphics repeatedly — each lookup
|
|
10
|
+
decompressed and re-parsed the same XML.
|
|
11
|
+
|
|
12
|
+
## What was done
|
|
13
|
+
|
|
14
|
+
Parsed parts are memoized in a per-`Package` hash
|
|
15
|
+
(`parse_part` remains the single read+parse path; `each_part` /
|
|
16
|
+
`read_part` stay uncached raw reads for round-trip byte fidelity).
|
|
17
|
+
|
|
18
|
+
## Acceptance criteria
|
|
19
|
+
|
|
20
|
+
- [x] Repeated `part(name)` calls return the same object.
|
|
21
|
+
- [x] Different parts do not share objects.
|
|
22
|
+
- [x] Whole-package round-trip specs unchanged (raw reads).
|
|
@@ -20,9 +20,11 @@ kinsoku shori, ruby annotations).
|
|
|
20
20
|
frame width (previously they overflowed as one long line) — this
|
|
21
21
|
also fixes horizontal CJK wrapping.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
vertical
|
|
25
|
-
|
|
23
|
+
2026-08-20 completions: tate-chu-yoko (TODO 124), Latin rotation
|
|
24
|
+
and vertical ruby (TODO 122), CJK/Latin automatic script spacing
|
|
25
|
+
(TODO 125, a mojikumi subset wired into Shaper). The remaining
|
|
26
|
+
stretch goal is full class-based mojikumi (per-pair punctuation
|
|
27
|
+
compression tables). See `lib/idml/text_engine/cjk_layout.rb` and
|
|
26
28
|
`lib/idml/text_engine/vertical_text_layout.rb`.
|
|
27
29
|
|
|
28
30
|
## Progress 2026-08-17
|
data/lib/idml/package.rb
CHANGED
|
@@ -134,13 +134,23 @@ module Idml
|
|
|
134
134
|
# Returns a typed part instance for `name` when a class is registered
|
|
135
135
|
# for that file pattern (e.g. Designmap for designmap.xml). Falls
|
|
136
136
|
# back to Parts::Raw — a lossless XML wrapper — for unmodeled parts.
|
|
137
|
+
# Parsed parts are memoized: rendering resolves stories,
|
|
138
|
+
# preferences, and styles repeatedly (per frame), and each
|
|
139
|
+
# un-memoized read would re-decompress and re-parse the zip
|
|
140
|
+
# entry.
|
|
137
141
|
def part(name)
|
|
142
|
+
@part_cache ||= {}
|
|
143
|
+
@part_cache[name] ||= parse_part(name)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def parse_part(name)
|
|
138
147
|
xml = read_part(name)
|
|
139
148
|
klass = Idml::Parts.class_for(name)
|
|
140
149
|
return Idml::Parts::Raw.from_xml(xml) unless klass
|
|
141
150
|
|
|
142
151
|
klass.from_xml(xml)
|
|
143
152
|
end
|
|
153
|
+
private :parse_part
|
|
144
154
|
|
|
145
155
|
def each_part
|
|
146
156
|
return enum_for(:each_part) unless block_given?
|
|
@@ -177,17 +177,20 @@ module Idml
|
|
|
177
177
|
layout_frame = layout_frame(context.item, box)
|
|
178
178
|
left_limit = layout_frame.x + (layout_frame.inset_left || 0)
|
|
179
179
|
resume_vertical_paragraph(state)
|
|
180
|
+
next_column = state.column_offset || 0
|
|
180
181
|
pending = 0
|
|
181
182
|
|
|
182
183
|
state.paragraphs.each do |paragraph|
|
|
183
|
-
remaining_runs = vertical_paragraph(
|
|
184
|
-
canvas, paragraph, context, layout_frame, font,
|
|
184
|
+
remaining_runs, next_column = vertical_paragraph(
|
|
185
|
+
canvas, paragraph, context, layout_frame, font,
|
|
186
|
+
left_limit, next_column
|
|
185
187
|
)
|
|
186
188
|
if remaining_runs.empty?
|
|
187
189
|
pending += 1
|
|
188
190
|
else
|
|
189
191
|
state.current_paragraph = paragraph
|
|
190
192
|
state.runs_remaining = remaining_runs
|
|
193
|
+
state.column_offset = next_column
|
|
191
194
|
break
|
|
192
195
|
end
|
|
193
196
|
end
|
|
@@ -198,7 +201,9 @@ module Idml
|
|
|
198
201
|
|
|
199
202
|
# Folds a chain-resumed paragraph (runs_remaining) back to
|
|
200
203
|
# the head of the paragraph list so the vertical path sees a
|
|
201
|
-
# uniform sequence.
|
|
204
|
+
# uniform sequence. The consumed column offset persists in
|
|
205
|
+
# the state so the next frame continues right where this
|
|
206
|
+
# frame's columns ended.
|
|
202
207
|
def self.resume_vertical_paragraph(state)
|
|
203
208
|
return unless state.current_paragraph
|
|
204
209
|
|
|
@@ -209,10 +214,13 @@ module Idml
|
|
|
209
214
|
end
|
|
210
215
|
private_class_method :resume_vertical_paragraph
|
|
211
216
|
|
|
212
|
-
# Renders one paragraph's runs as vertical columns
|
|
213
|
-
#
|
|
217
|
+
# Renders one paragraph's runs as vertical columns starting
|
|
218
|
+
# at `start_column`. Returns [runs that did not fit (empty
|
|
219
|
+
# when all placed), the next unused column index].
|
|
214
220
|
def self.vertical_paragraph(canvas, paragraph, context,
|
|
215
|
-
layout_frame, font, left_limit
|
|
221
|
+
layout_frame, font, left_limit,
|
|
222
|
+
start_column)
|
|
223
|
+
next_column = start_column
|
|
216
224
|
paragraph.runs.each_with_index do |run, index|
|
|
217
225
|
size = run.point_size || DEFAULT_SIZE
|
|
218
226
|
leading = TextEngine::VerticalLayout.leading_for(
|
|
@@ -220,25 +228,72 @@ module Idml
|
|
|
220
228
|
)
|
|
221
229
|
glyphs = TextEngine::Shaper.shape(text: run.text, font: font,
|
|
222
230
|
size: size)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
grouped = tatechuyoko_group(run, glyphs, layout_frame,
|
|
232
|
+
leading, size, next_column)
|
|
233
|
+
positioned, next_column = grouped ||
|
|
234
|
+
TextEngine::VerticalTextLayout.layout(
|
|
235
|
+
glyphs: glyphs, frame: layout_frame, leading: leading,
|
|
236
|
+
size: size, start_column: next_column
|
|
237
|
+
)
|
|
238
|
+
unless vertical_fits?(layout_frame, leading, next_column,
|
|
228
239
|
left_limit)
|
|
229
|
-
return paragraph.runs[index..]
|
|
240
|
+
return [paragraph.runs[index..], start_column]
|
|
230
241
|
end
|
|
231
242
|
|
|
232
|
-
positioned
|
|
233
|
-
|
|
234
|
-
end
|
|
243
|
+
emit_vertical_run(canvas, positioned, grouped, run, context,
|
|
244
|
+
size)
|
|
235
245
|
emit_vertical_ruby(canvas, run, positioned, layout_frame,
|
|
236
246
|
size, context)
|
|
237
247
|
end
|
|
238
|
-
[]
|
|
248
|
+
[[], next_column]
|
|
239
249
|
end
|
|
240
250
|
private_class_method :vertical_paragraph
|
|
241
251
|
|
|
252
|
+
# A tate-chu-yoko group for runs that declare it (and whose
|
|
253
|
+
# glyphs fit horizontally in a column slot); nil otherwise.
|
|
254
|
+
def self.tatechuyoko_group(run, glyphs, layout_frame, leading,
|
|
255
|
+
size, start_column)
|
|
256
|
+
return nil unless run.tatechuyoko
|
|
257
|
+
|
|
258
|
+
TextEngine::VerticalTextLayout.tatechuyoko_group(
|
|
259
|
+
glyphs: glyphs, frame: layout_frame, leading: leading,
|
|
260
|
+
size: size, start_column: start_column
|
|
261
|
+
)
|
|
262
|
+
end
|
|
263
|
+
private_class_method :tatechuyoko_group
|
|
264
|
+
|
|
265
|
+
# Emits a vertical run's glyphs: rotated/normal vertical
|
|
266
|
+
# handling for stacked columns, plain upright emission for
|
|
267
|
+
# tate-chu-yoko groups.
|
|
268
|
+
def self.emit_vertical_run(canvas, positioned, grouped, run,
|
|
269
|
+
context, size)
|
|
270
|
+
if grouped
|
|
271
|
+
positioned.each do |glyph|
|
|
272
|
+
emit_upright_glyph(canvas, glyph, run, context, size)
|
|
273
|
+
end
|
|
274
|
+
else
|
|
275
|
+
positioned.each do |glyph|
|
|
276
|
+
emit_vertical_glyph(canvas, glyph, run, context, size)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
private_class_method :emit_vertical_run
|
|
281
|
+
|
|
282
|
+
def self.emit_upright_glyph(canvas, positioned, run, context,
|
|
283
|
+
size)
|
|
284
|
+
text = [positioned.codepoint].pack("U")
|
|
285
|
+
text_kwargs = CharacterStyle.text_kwargs(
|
|
286
|
+
run,
|
|
287
|
+
{
|
|
288
|
+
at: [positioned.x, positioned.y],
|
|
289
|
+
font: font_for_run(run, context),
|
|
290
|
+
size: size,
|
|
291
|
+
},
|
|
292
|
+
)
|
|
293
|
+
canvas.text(text, **text_kwargs)
|
|
294
|
+
end
|
|
295
|
+
private_class_method :emit_upright_glyph
|
|
296
|
+
|
|
242
297
|
def self.vertical_fits?(layout_frame, leading, columns, left_limit)
|
|
243
298
|
right = layout_frame.x + layout_frame.width -
|
|
244
299
|
(layout_frame.inset_right || 0)
|
|
@@ -576,7 +631,9 @@ module Idml
|
|
|
576
631
|
pending = 0
|
|
577
632
|
state.paragraphs.each do |paragraph|
|
|
578
633
|
break if cursor_y < bottom_limit
|
|
579
|
-
break if
|
|
634
|
+
break if paragraph_deferred?(paragraph, layout_frame, font,
|
|
635
|
+
cursor_y, bottom_limit,
|
|
636
|
+
pending.positive?)
|
|
580
637
|
|
|
581
638
|
remaining_runs, cursor_y, char_cursor = render_runs_for_paragraph(
|
|
582
639
|
canvas, paragraph, paragraph.runs, context,
|
|
@@ -908,6 +965,26 @@ module Idml
|
|
|
908
965
|
end
|
|
909
966
|
private_class_method :layout_run
|
|
910
967
|
|
|
968
|
+
# True when the paragraph should move wholly to the next
|
|
969
|
+
# frame: a StartParagraph forced break, or KeepAllLines-
|
|
970
|
+
# Together with insufficient remaining space (never for the
|
|
971
|
+
# frame's first paragraph, so progress is always made).
|
|
972
|
+
def self.paragraph_deferred?(paragraph, layout_frame, font,
|
|
973
|
+
cursor_y, bottom_limit, placed_any)
|
|
974
|
+
return true if paragraph_break?(paragraph) && placed_any
|
|
975
|
+
return false unless paragraph.keep_all_lines_together
|
|
976
|
+
return false unless placed_any
|
|
977
|
+
|
|
978
|
+
wrap_width = TextEngine::VerticalLayout.wrap_width(
|
|
979
|
+
layout_frame, paragraph.right_indent || 0
|
|
980
|
+
)
|
|
981
|
+
height = TextEngine::Measurement.paragraph_height(
|
|
982
|
+
paragraph, font, wrap_width
|
|
983
|
+
)
|
|
984
|
+
(cursor_y - height) < bottom_limit
|
|
985
|
+
end
|
|
986
|
+
private_class_method :paragraph_deferred?
|
|
987
|
+
|
|
911
988
|
# True when the paragraph requests a forced break to the
|
|
912
989
|
# next frame/column (StartParagraph). All break flavors act
|
|
913
990
|
# at frame/column granularity.
|
|
@@ -48,6 +48,7 @@ module Idml
|
|
|
48
48
|
:ruby_string,
|
|
49
49
|
:ruby_font_size,
|
|
50
50
|
:ruby_position,
|
|
51
|
+
:tatechuyoko,
|
|
51
52
|
keyword_init: true,
|
|
52
53
|
)
|
|
53
54
|
|
|
@@ -70,6 +71,9 @@ module Idml
|
|
|
70
71
|
# Forced paragraph break (StartParagraph: NextPage /
|
|
71
72
|
# NextColumn / NextFrame / NextOddPage / NextEvenPage).
|
|
72
73
|
:start_paragraph,
|
|
74
|
+
# Widow/orphan control: KeepAllLinesTogether pushes the whole
|
|
75
|
+
# paragraph to the next frame when it cannot fully fit.
|
|
76
|
+
:keep_all_lines_together,
|
|
73
77
|
:bullets_and_numbering_list_type,
|
|
74
78
|
:bullet_character_value,
|
|
75
79
|
:bullets_text_after,
|
|
@@ -200,6 +204,9 @@ module Idml
|
|
|
200
204
|
),
|
|
201
205
|
start_paragraph: resolve_attr(style_lookup, psr,
|
|
202
206
|
:start_paragraph),
|
|
207
|
+
keep_all_lines_together: resolve_attr(
|
|
208
|
+
style_lookup, psr, :keep_all_lines_together
|
|
209
|
+
),
|
|
203
210
|
drop_cap_lines: resolve_attr(style_lookup, psr, :drop_cap_lines),
|
|
204
211
|
drop_cap_characters: resolve_attr(style_lookup, psr,
|
|
205
212
|
:drop_cap_characters),
|
|
@@ -390,6 +397,7 @@ module Idml
|
|
|
390
397
|
ruby_string: resolve_csr(style_lookup, csr, :ruby_string),
|
|
391
398
|
ruby_font_size: resolve_csr(style_lookup, csr, :ruby_font_size),
|
|
392
399
|
ruby_position: resolve_csr(style_lookup, csr, :ruby_position),
|
|
400
|
+
tatechuyoko: resolve_csr(style_lookup, csr, :tatechuyoko),
|
|
393
401
|
)
|
|
394
402
|
end
|
|
395
403
|
private_class_method :text_run
|
|
@@ -7,6 +7,38 @@ module Idml
|
|
|
7
7
|
module CjkLayout
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
+
# Mojikumi subset: automatic inter-script spacing — an eighth
|
|
11
|
+
# em is inserted between adjacent CJK and ASCII alphanumeric
|
|
12
|
+
# glyphs (widenning the leading glyph of each pair), matching
|
|
13
|
+
# InDesign's default CJK/Latin auto spacing.
|
|
14
|
+
SCRIPT_SPACING_EM = 0.125
|
|
15
|
+
|
|
16
|
+
def apply_script_spacing(glyphs, size)
|
|
17
|
+
spacing = size * SCRIPT_SPACING_EM
|
|
18
|
+
(0...(glyphs.length - 1)).each do |index|
|
|
19
|
+
leading = glyphs[index]
|
|
20
|
+
following = glyphs[index + 1]
|
|
21
|
+
next unless script_boundary?(leading.codepoint,
|
|
22
|
+
following.codepoint)
|
|
23
|
+
|
|
24
|
+
leading.width += spacing
|
|
25
|
+
end
|
|
26
|
+
glyphs
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# True when exactly one of the pair is CJK and the other is
|
|
30
|
+
# an ASCII letter or digit.
|
|
31
|
+
def script_boundary?(left, right)
|
|
32
|
+
(cjk?(left) && ascii_alnum?(right)) ||
|
|
33
|
+
(ascii_alnum?(left) && cjk?(right))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ascii_alnum?(codepoint)
|
|
37
|
+
(0x30..0x39).cover?(codepoint) ||
|
|
38
|
+
(0x41..0x5A).cover?(codepoint) ||
|
|
39
|
+
(0x61..0x7A).cover?(codepoint)
|
|
40
|
+
end
|
|
41
|
+
|
|
10
42
|
CJK_RANGES = [
|
|
11
43
|
0x3000..0x303F, # CJK Symbols and Punctuation
|
|
12
44
|
0x3040..0x309F, # Hiragana
|
|
@@ -13,18 +13,19 @@ module Idml
|
|
|
13
13
|
PositionedGlyph = Struct.new(:codepoint, :x, :y, keyword_init: true)
|
|
14
14
|
|
|
15
15
|
# Stacks `glyphs` into columns of at most the frame's text
|
|
16
|
-
# height; column 0 is the rightmost.
|
|
17
|
-
#
|
|
18
|
-
|
|
16
|
+
# height; column 0 is the rightmost. `start_column` places
|
|
17
|
+
# the first column after already-used columns (multi-run
|
|
18
|
+
# frames). Returns [positioned_glyphs, next_column_index].
|
|
19
|
+
def self.layout(glyphs:, frame:, leading:, size:, start_column: 0)
|
|
19
20
|
lines = TextEngine::LineBreaker.break(
|
|
20
21
|
glyphs: glyphs, frame_width: column_height(frame),
|
|
21
22
|
)
|
|
22
23
|
positioned = []
|
|
23
|
-
lines.each_with_index do |line,
|
|
24
|
+
lines.each_with_index do |line, index|
|
|
24
25
|
stack_column(positioned, line.glyphs, frame, leading, size,
|
|
25
|
-
|
|
26
|
+
index + start_column)
|
|
26
27
|
end
|
|
27
|
-
[positioned, lines.length]
|
|
28
|
+
[positioned, start_column + lines.length]
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
# The frame's usable vertical extent for one column of text.
|
|
@@ -47,6 +48,32 @@ module Idml
|
|
|
47
48
|
end
|
|
48
49
|
private_class_method :stack_column
|
|
49
50
|
|
|
51
|
+
# Positions a tate-chu-yoko group (e.g. the digits "12" kept
|
|
52
|
+
# horizontal inside vertical text): glyphs share one baseline
|
|
53
|
+
# at the top of the given column slot and advance left-to-
|
|
54
|
+
# right, centered in the slot. Returns
|
|
55
|
+
# [positioned_glyphs, next_column_index]; nil when the group
|
|
56
|
+
# is wider than the column height (caller falls back to
|
|
57
|
+
# stacked layout).
|
|
58
|
+
def self.tatechuyoko_group(glyphs:, frame:, leading:, size:,
|
|
59
|
+
start_column:)
|
|
60
|
+
total = glyphs.sum(&:width)
|
|
61
|
+
return nil if total > column_height(frame)
|
|
62
|
+
|
|
63
|
+
right = frame.x + frame.width - (frame.inset_right || 0)
|
|
64
|
+
slot_center = right - ((start_column + 0.5) * leading)
|
|
65
|
+
x = slot_center - (total / 2.0)
|
|
66
|
+
baseline_y = frame.y + frame.height - (frame.inset_top || 0) - size
|
|
67
|
+
positioned = glyphs.map do |glyph|
|
|
68
|
+
glyph_position = PositionedGlyph.new(
|
|
69
|
+
codepoint: glyph.codepoint, x: x, y: baseline_y,
|
|
70
|
+
)
|
|
71
|
+
x += glyph.width
|
|
72
|
+
glyph_position
|
|
73
|
+
end
|
|
74
|
+
[positioned, start_column + 1]
|
|
75
|
+
end
|
|
76
|
+
|
|
50
77
|
# Vertical advance of one glyph — its measured width (the em
|
|
51
78
|
# for the common square-CJK case).
|
|
52
79
|
def self.advance(glyph)
|
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.9.
|
|
4
|
+
version: 0.9.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -181,6 +181,10 @@ files:
|
|
|
181
181
|
- TODO.pdf/120-start-paragraph-breaks.md
|
|
182
182
|
- TODO.pdf/121-cell-edge-strokes.md
|
|
183
183
|
- TODO.pdf/122-vertical-latin-ruby.md
|
|
184
|
+
- TODO.pdf/123-keep-options.md
|
|
185
|
+
- TODO.pdf/124-tatechuyoko-vertical.md
|
|
186
|
+
- TODO.pdf/125-script-spacing.md
|
|
187
|
+
- TODO.pdf/126-part-cache.md
|
|
184
188
|
- TODO.pdf/13-cjk-text-layout.md
|
|
185
189
|
- TODO.pdf/14-page-item-element-models.md
|
|
186
190
|
- TODO.pdf/15-wire-spread-children.md
|