idml 0.10.5 → 0.10.7
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/135-minimum-glyph-scaling.md +28 -0
- data/TODO.pdf/136-diagonal-cell-strokes.md +24 -0
- data/TODO.pdf/137-simple-render-footnote-text.md +22 -0
- data/TODO.pdf/138-text-wrap-sides.md +36 -0
- data/TODO.pdf/139-hyphen-break-opportunities.md +24 -0
- data/TODO.pdf/140-cap-xheight-first-baselines.md +29 -0
- data/TODO.pdf/141-perf-hundred-pages.md +22 -0
- data/TODO.pdf/61-known-limitations.md +26 -22
- data/TODO.pdf/README.md +9 -7
- data/lib/idml/render/footnote.rb +1 -0
- data/lib/idml/render/renderers/table_renderer.rb +52 -0
- data/lib/idml/render/renderers/text_frame_renderer.rb +99 -28
- data/lib/idml/render/style_resolver.rb +4 -0
- data/lib/idml/render/text_wrap_resolver.rb +85 -6
- data/lib/idml/text_engine/justifier.rb +21 -0
- data/lib/idml/text_engine/line_breaker.rb +24 -13
- data/lib/idml/text_engine/pdfrb_font_metrics.rb +6 -0
- data/lib/idml/version.rb +1 -1
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 943028d5bf17ca306e3cce0d23c4b042ddaa9473295f2fa7f4af43009a7423a0
|
|
4
|
+
data.tar.gz: b9c3c1e5421ffa4de44396dbc94124434dda470c3f10fabd5ddd81146c3991c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86717ea81b1ede4576d3d41e7026cd2731ec40024ea887d33a03c4ff7771384cb0ae9fd04f5948a322d48986dae2946adcebf167a7c51ae3ce7890a28726e7ee
|
|
7
|
+
data.tar.gz: 12a2089f4600a04fe5cd1d74e409c0933dc6f691156ffca4eeb87fcb25fd5d89e2a1ec58261c8a087ea00b93cf42abee0bb54d64f72032dc3b6bbe919f6cc6b4
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# TODO PDF 135: MinimumGlyphScaling justification compression
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-26
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
InDesign's justification dialog exposes MinimumGlyphScaling: overlong
|
|
8
|
+
justified lines may squeeze glyphs uniformly down to that percent.
|
|
9
|
+
The gem applied only MaximumGlyphScaling (stretching, TODO 129) —
|
|
10
|
+
overlong lines overflowed the frame instead of compressing.
|
|
11
|
+
|
|
12
|
+
## Solution
|
|
13
|
+
|
|
14
|
+
`TextEngine::Justifier#justify_line` now handles negative slack:
|
|
15
|
+
`compress_glyphs` scales every glyph uniformly, capped at
|
|
16
|
+
`min_glyph_scaling` percent (default 100 disables it, matching
|
|
17
|
+
InDesign). The limit threads through `SpacingLimits#min_glyph_scaling`,
|
|
18
|
+
resolved from the PSR's `MinimumGlyphScaling` via `StyleResolver`
|
|
19
|
+
(`Paragraph#minimum_glyph_scaling`) and applied at both call sites
|
|
20
|
+
(frame renderer and footnote layout).
|
|
21
|
+
|
|
22
|
+
## Files
|
|
23
|
+
|
|
24
|
+
- `lib/idml/text_engine/justifier.rb`
|
|
25
|
+
- `lib/idml/render/style_resolver.rb`
|
|
26
|
+
- `lib/idml/render/renderers/text_frame_renderer.rb`
|
|
27
|
+
- `lib/idml/render/footnote.rb`
|
|
28
|
+
- `spec/idml/text_engine/text_engine_spec.rb`
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# TODO PDF 136: Diagonal cell strokes drawn
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-26
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
The Table schema (TODO 104) modeled `TopLeftDiagonalLine`,
|
|
8
|
+
`TopRightDiagonalLine`, and the `DiagonalLineStroke*` attributes, but
|
|
9
|
+
the renderer never drew them — struck-through table cells (common in
|
|
10
|
+
forms) rendered as plain boxes.
|
|
11
|
+
|
|
12
|
+
## Solution
|
|
13
|
+
|
|
14
|
+
`TableRenderer#render_cell_diagonals` draws after cell borders:
|
|
15
|
+
TopLeftDiagonalLine from top-left to bottom-right corner,
|
|
16
|
+
TopRightDiagonalLine from top-right to bottom-left. Weight defaults
|
|
17
|
+
to 1pt and color to black when the flags are set without explicit
|
|
18
|
+
stroke attributes (InDesign defaults); `DiagonalLineStrokeTint`
|
|
19
|
+
applies via `ColorHelper.apply_tint`.
|
|
20
|
+
|
|
21
|
+
## Files
|
|
22
|
+
|
|
23
|
+
- `lib/idml/render/renderers/table_renderer.rb`
|
|
24
|
+
- `spec/idml/render/table_renderer_spec.rb`
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# TODO PDF 137: Footnote text in the simple-render fallback
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-26
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
Without FontMetrics the text renderer falls back to `simple_render`
|
|
8
|
+
(one `text_rich` per frame). Footnote markers rendered but the
|
|
9
|
+
footnote text itself was dropped entirely.
|
|
10
|
+
|
|
11
|
+
## Solution
|
|
12
|
+
|
|
13
|
+
`emit_simple_footnotes` emits after the body block: one `text_rich`
|
|
14
|
+
line per footnote paragraph, stacked upward from the frame's bottom
|
|
15
|
+
edge below a hairline separator rule (0.5pt, quarter-width — InDesign's
|
|
16
|
+
footnote rule proportions). Paragraphs carry their marker prefix from
|
|
17
|
+
extraction, so numbering reads correctly.
|
|
18
|
+
|
|
19
|
+
## Files
|
|
20
|
+
|
|
21
|
+
- `lib/idml/render/renderers/text_frame_renderer.rb`
|
|
22
|
+
- `spec/idml/render/simple_footnote_render_spec.rb`
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# TODO PDF 138: Side-aware text wrap (TextWrapSide)
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-26
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
TextWrapSide was parsed but ignored: every wrap mode narrowed the run
|
|
8
|
+
width by the contour overlap, which smears text over objects that sit
|
|
9
|
+
at a frame edge (text starts at the frame's left edge regardless of
|
|
10
|
+
where the object is).
|
|
11
|
+
|
|
12
|
+
## Solution
|
|
13
|
+
|
|
14
|
+
`TextWrapResolver#wrap_adjustment` returns
|
|
15
|
+
`[width_reduction, x_shift]` per line band:
|
|
16
|
+
|
|
17
|
+
- `LeftSide` — text stays left of the contour: reduce so the line
|
|
18
|
+
ends at the contour's left edge, no shift.
|
|
19
|
+
- `RightSide` — text flows right of the contour: shift lines past the
|
|
20
|
+
contour's right edge (`Line#x_offset += shift` after justification)
|
|
21
|
+
and reduce by the same amount.
|
|
22
|
+
- `LargestArea` — pick the roomier side of the two.
|
|
23
|
+
- `BothSides` (default) and the spine variants — narrow by the full
|
|
24
|
+
overlap; true two-segment lines and binding-side context remain
|
|
25
|
+
approximations.
|
|
26
|
+
- Inverse contours keep their flip-to-inside semantics.
|
|
27
|
+
|
|
28
|
+
Multiple contours combine by max (per-run approximation, as before).
|
|
29
|
+
Shape contours (Contour mode) narrow by polygon overlap without side
|
|
30
|
+
awareness.
|
|
31
|
+
|
|
32
|
+
## Files
|
|
33
|
+
|
|
34
|
+
- `lib/idml/render/text_wrap_resolver.rb`
|
|
35
|
+
- `lib/idml/render/renderers/text_frame_renderer.rb`
|
|
36
|
+
- `spec/idml/render/text_wrap_resolver_spec.rb`
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# TODO PDF 139: Hyphen break opportunities in the LineBreaker
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-27
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
The greedy line breaker only treated spaces as break opportunities.
|
|
8
|
+
Long compound words ("state-of-the-art", "e-mail") overflowed the
|
|
9
|
+
frame as one unbreakable run instead of wrapping after a hyphen.
|
|
10
|
+
|
|
11
|
+
## Solution
|
|
12
|
+
|
|
13
|
+
`LineBreaker#break_after?` treats hyphen-minus (U+002D), Unicode
|
|
14
|
+
hyphen (U+2010), non-breaking hyphen (U+2011), and soft hyphen
|
|
15
|
+
(U+00AD) as break-after points: the hyphen stays on the first line
|
|
16
|
+
and the remainder wraps. Soft hyphens break without adding a
|
|
17
|
+
visible hyphen. Dictionary-based hyphenation (Knuth-Liang) remains
|
|
18
|
+
out of scope — the Hyphenation* PSR attributes stay parsed but
|
|
19
|
+
unused.
|
|
20
|
+
|
|
21
|
+
## Files
|
|
22
|
+
|
|
23
|
+
- `lib/idml/text_engine/line_breaker.rb`
|
|
24
|
+
- `spec/idml/text_engine/text_engine_spec.rb`
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# TODO PDF 140: CapHeight / XHeight / EmboxHeight first baselines
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-27
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
FirstBaselineOffset honored AscentOffset and FixedHeight (TODO 128)
|
|
8
|
+
but approximated CapHeight, XHeight, and EmboxHeight as leading —
|
|
9
|
+
text sat noticeably lower than InDesign places it.
|
|
10
|
+
|
|
11
|
+
## Solution
|
|
12
|
+
|
|
13
|
+
`TextFrameRenderer#baseline_target` now handles all five modes:
|
|
14
|
+
|
|
15
|
+
- **CapHeight** — the font's cap-height metric when the provider
|
|
16
|
+
fills it; pdfrb 0.7.1 never does, so the fallback is the standard
|
|
17
|
+
0.72 em proportion (Arial and Helvetica both sit within 1%).
|
|
18
|
+
- **XHeight** — 0.52 em (no x-height metric exists in pdfrb).
|
|
19
|
+
- **EmboxHeight** — the em box itself: the first paragraph's point
|
|
20
|
+
size.
|
|
21
|
+
|
|
22
|
+
`PdfrbFontMetrics#cap_height` exposes the metric for when pdfrb
|
|
23
|
+
starts populating it.
|
|
24
|
+
|
|
25
|
+
## Files
|
|
26
|
+
|
|
27
|
+
- `lib/idml/render/renderers/text_frame_renderer.rb`
|
|
28
|
+
- `lib/idml/text_engine/pdfrb_font_metrics.rb`
|
|
29
|
+
- `spec/idml/render/first_baseline_spec.rb`
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# TODO PDF 141: 100+ page performance regression guard
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-27
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
No test guarded against per-page complexity blowups — a regression
|
|
8
|
+
to O(document) work per page would only surface as slow user
|
|
9
|
+
renders.
|
|
10
|
+
|
|
11
|
+
## Solution
|
|
12
|
+
|
|
13
|
+
`perf_hundred_pages_spec.rb` builds a synthetic 120-spread package
|
|
14
|
+
(one page + one text frame each, all sharing a wordy story) and
|
|
15
|
+
renders it end-to-end through `Idml::Render.render`. Asserts all
|
|
16
|
+
120 PDF pages emit and the render completes under a generous 60s
|
|
17
|
+
ceiling — sized to trip only on pathological blowups, not machine
|
|
18
|
+
noise. Baseline on this branch: ~2s for 120 pages.
|
|
19
|
+
|
|
20
|
+
## Files
|
|
21
|
+
|
|
22
|
+
- `spec/idml/render/perf_hundred_pages_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-27
|
|
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;
|
|
@@ -9,22 +9,26 @@ this list reflects the current state.
|
|
|
9
9
|
## Known limitations
|
|
10
10
|
|
|
11
11
|
### Text layout
|
|
12
|
-
- **Hyphenation
|
|
13
|
-
parsed but unused);
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
- **Hyphenation**: dictionary-based hyphenation is not implemented
|
|
13
|
+
(Hyphenation* attributes are parsed but unused); compound words
|
|
14
|
+
wrap after explicit hyphens (TODO 139).
|
|
15
|
+
- **Justification**: word/letter spacing caps apply (TODO 119),
|
|
16
|
+
MaximumGlyphScaling stretches (TODO 129) and MinimumGlyphScaling
|
|
17
|
+
compresses overlong lines (TODO 135); ToBinding / RTL binding
|
|
18
|
+
alignment is not applied.
|
|
18
19
|
- **Keep options**: KeepAllLinesTogether (TODO 123), KeepWithNext
|
|
19
20
|
(TODO 127), and the KeepFirstLines / KeepLastLines windows
|
|
20
21
|
(TODO 133, line-count approximation) are honored.
|
|
21
|
-
- **First baseline**:
|
|
22
|
-
|
|
22
|
+
- **First baseline**: all five modes honor their metrics (TODOs
|
|
23
|
+
128/140); CapHeight / XHeight use standard font proportions
|
|
24
|
+
(0.72 / 0.52 em) until a metrics provider fills them.
|
|
23
25
|
- **StartParagraph** breaks act at frame/column granularity — no
|
|
24
26
|
odd/even page parity.
|
|
25
27
|
- **Text wrap**: BoundingBoxTextWrap, Contour, and Inverse modes
|
|
26
|
-
render (TODOs 130-131)
|
|
27
|
-
|
|
28
|
+
render (TODOs 130-131) with TextWrapSide side-awareness (TODO 138:
|
|
29
|
+
LeftSide / RightSide / LargestArea pick the flow side; spine
|
|
30
|
+
variants approximate as BothSides); JumpObject/NextColumn
|
|
31
|
+
approximate as the box; Contour-mode shapes are not side-aware.
|
|
28
32
|
|
|
29
33
|
### CJK
|
|
30
34
|
- **Mojikumi**: script spacing (TODO 125), line-end compression
|
|
@@ -34,24 +38,24 @@ this list reflects the current state.
|
|
|
34
38
|
ruby placement is beside-column; keep options do not apply.
|
|
35
39
|
|
|
36
40
|
### Structural features
|
|
37
|
-
- **Endnotes** (TODO 117): reference markers render
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
- **Endnotes** (TODO 117): reference markers render and endnote TEXT
|
|
42
|
+
renders end-of-story with ordinal prefixes; multi-story linkage
|
|
43
|
+
(which endnotes belong to which main story) is not modeled.
|
|
40
44
|
- **Footnotes** number per story; overflow balancing (NoSplitting)
|
|
41
|
-
is not modeled; the simple-render fallback shows markers
|
|
42
|
-
footnote text.
|
|
45
|
+
is not modeled; the simple-render fallback shows markers with
|
|
46
|
+
footnote text stacked at the frame bottom (TODO 137).
|
|
43
47
|
- **Anchored objects** render at stored geometry; AnchorType
|
|
44
48
|
text-reflow is not simulated.
|
|
45
49
|
- **Tables** flow across chained frames with repeated header rows
|
|
46
|
-
(TODO 134)
|
|
47
|
-
|
|
50
|
+
(TODO 134) and diagonal cell strokes draw (TODO 136);
|
|
51
|
+
row-spanning cells break at frame boundaries.
|
|
48
52
|
- **Inline anchored objects / footnotes inside table cells** are
|
|
49
53
|
not handled.
|
|
50
54
|
|
|
51
55
|
## Future enhancements
|
|
52
56
|
|
|
53
57
|
- Hyphenation dictionary integration
|
|
54
|
-
- Shape-mode text wrap contours
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
+
- Shape-mode text wrap contours (side-aware)
|
|
59
|
+
- Named mojikumi sets (詳細 etc.)
|
|
60
|
+
- ToBinding / RTL binding alignment
|
|
61
|
+
- Vertical-mode keep options and run-grouped Latin rotation
|
data/TODO.pdf/README.md
CHANGED
|
@@ -12,10 +12,12 @@ without InDesign Server. The pipeline has three layers:
|
|
|
12
12
|
output together. IDML Package → typed model → render plan →
|
|
13
13
|
pdfrb Document → PDF file.
|
|
14
14
|
|
|
15
|
-
All entries are DONE
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
All entries are DONE — including TODO 117 (end-of-story endnote
|
|
16
|
+
TEXT rendering), TODO 13's class-based mojikumi pair compression,
|
|
17
|
+
and the final known-limitation sweep (TODOs 135–138:
|
|
18
|
+
MinimumGlyphScaling compression, diagonal cell strokes, footnote
|
|
19
|
+
text in the simple-render fallback, and side-aware TextWrapSide).
|
|
20
|
+
Vertical writing, kinsoku, ruby, and tate-chu-yoko all landed
|
|
21
|
+
(TODOs 122-125). TODO 61 is the live known-limitations index. Each
|
|
22
|
+
file carries a `## Status:` header with a brief implementation
|
|
23
|
+
summary.
|
data/lib/idml/render/footnote.rb
CHANGED
|
@@ -132,6 +132,7 @@ module Idml
|
|
|
132
132
|
max_word_spacing: paragraph.maximum_word_spacing,
|
|
133
133
|
max_letter_spacing: paragraph.maximum_letter_spacing,
|
|
134
134
|
max_glyph_scaling: paragraph.maximum_glyph_scaling,
|
|
135
|
+
min_glyph_scaling: paragraph.minimum_glyph_scaling,
|
|
135
136
|
)
|
|
136
137
|
lines.each_with_index do |line, index|
|
|
137
138
|
TextEngine::Justifier.justify(
|
|
@@ -104,6 +104,8 @@ module Idml
|
|
|
104
104
|
cell_h, context)
|
|
105
105
|
render_cell_border(canvas, cell, table, cell_x, cell_y,
|
|
106
106
|
cell_w, cell_h, context)
|
|
107
|
+
render_cell_diagonals(canvas, cell, cell_x, cell_y,
|
|
108
|
+
cell_w, cell_h, context)
|
|
107
109
|
render_cell_text(canvas, cell, cell_x, cell_y, cell_h, context)
|
|
108
110
|
end
|
|
109
111
|
end
|
|
@@ -220,6 +222,56 @@ module Idml
|
|
|
220
222
|
end
|
|
221
223
|
private_class_method :render_cell_border
|
|
222
224
|
|
|
225
|
+
# TopLeftDiagonalLine runs from the cell's top-left to its
|
|
226
|
+
# bottom-right corner; TopRightDiagonalLine from top-right
|
|
227
|
+
# to bottom-left. Stroke weight defaults to 1pt and color
|
|
228
|
+
# to black when the flags are set without explicit stroke
|
|
229
|
+
# attributes (InDesign defaults). DiagonalLineInFront is
|
|
230
|
+
# accepted; diagonals always paint after the cell fill.
|
|
231
|
+
def self.render_cell_diagonals(canvas, cell, x, y, w, h, context)
|
|
232
|
+
return unless cell.top_left_diagonal_line ||
|
|
233
|
+
cell.top_right_diagonal_line
|
|
234
|
+
|
|
235
|
+
weight = cell.diagonal_line_stroke_weight
|
|
236
|
+
thickness = weight.nil? ? 1.0 : weight.to_f
|
|
237
|
+
return unless thickness.positive?
|
|
238
|
+
|
|
239
|
+
canvas.line_width = thickness
|
|
240
|
+
canvas.stroke_color(diagonal_color(cell, context))
|
|
241
|
+
diagonals_for(cell, x, y, w, h).each do |from, to|
|
|
242
|
+
canvas.move_to(from[0], from[1])
|
|
243
|
+
canvas.line_to(to[0], to[1])
|
|
244
|
+
canvas.stroke
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
private_class_method :render_cell_diagonals
|
|
248
|
+
|
|
249
|
+
def self.diagonals_for(cell, x, y, w, h)
|
|
250
|
+
[].tap do |diagonals|
|
|
251
|
+
diagonals << [[x, y + h], [x + w, y]] if cell.top_left_diagonal_line
|
|
252
|
+
diagonals << [[x + w, y + h], [x, y]] if cell.top_right_diagonal_line
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
private_class_method :diagonals_for
|
|
256
|
+
|
|
257
|
+
def self.diagonal_color(cell, context)
|
|
258
|
+
color = diagonal_resolved_color(cell, context)
|
|
259
|
+
color ? ColorHelper.to_canvas(color) : [:gray, 0.0]
|
|
260
|
+
end
|
|
261
|
+
private_class_method :diagonal_color
|
|
262
|
+
|
|
263
|
+
def self.diagonal_resolved_color(cell, context)
|
|
264
|
+
name = cell.diagonal_line_stroke_color
|
|
265
|
+
return nil if name.nil? || name == "Color/None"
|
|
266
|
+
|
|
267
|
+
resolved = context.color_resolver&.resolve(name)
|
|
268
|
+
return nil unless resolved
|
|
269
|
+
|
|
270
|
+
tint = cell.diagonal_line_stroke_tint
|
|
271
|
+
tint ? ColorHelper.apply_tint(resolved, tint) : resolved
|
|
272
|
+
end
|
|
273
|
+
private_class_method :diagonal_resolved_color
|
|
274
|
+
|
|
223
275
|
# [top, right, bottom, left] effective weights, or nil when
|
|
224
276
|
# neither cell edges nor table defaults declare any stroke
|
|
225
277
|
# (the legacy uniform-rect case).
|
|
@@ -629,12 +629,19 @@ module Idml
|
|
|
629
629
|
# the first line's baseline sits. The layout's default is
|
|
630
630
|
# leading-based; AscentOffset shifts by (ascent − leading)
|
|
631
631
|
# and FixedHeight by (MinimumFirstBaselineOffset − leading).
|
|
632
|
-
# CapHeight
|
|
633
|
-
#
|
|
632
|
+
# CapHeight uses the font's cap-height metric (leading
|
|
633
|
+
# fallback when the font reports none); XHeight approximates
|
|
634
|
+
# as 70% of cap height; EmboxHeight uses the em box (the
|
|
635
|
+
# first paragraph's point size). Returns the extra shift to
|
|
636
|
+
# subtract from the cursor.
|
|
637
|
+
FIRST_BASELINE_MODES = %w[
|
|
638
|
+
AscentOffset FixedHeight CapHeight XHeight EmboxHeight
|
|
639
|
+
].freeze
|
|
640
|
+
|
|
634
641
|
def self.first_baseline_offset(context, state, font)
|
|
635
642
|
pref = context.item&.text_frame_preference&.first
|
|
636
643
|
mode = pref&.first_baseline_offset
|
|
637
|
-
return 0.0 unless
|
|
644
|
+
return 0.0 unless FIRST_BASELINE_MODES.include?(mode)
|
|
638
645
|
|
|
639
646
|
baseline_target(pref, mode, state, font) -
|
|
640
647
|
first_paragraph_leading(state)
|
|
@@ -643,21 +650,52 @@ module Idml
|
|
|
643
650
|
|
|
644
651
|
# Distance from the top inset to the first baseline under
|
|
645
652
|
# the given mode.
|
|
653
|
+
# pdfrb 0.7.1 never fills the cap_height metric, so
|
|
654
|
+
# CapHeight / XHeight fall back to standard font
|
|
655
|
+
# proportions (cap ≈ 0.72 em, x ≈ 0.52 em — Arial and
|
|
656
|
+
# Helvetica both sit within 1% of these).
|
|
657
|
+
CAP_HEIGHT_RATIO = 0.72
|
|
658
|
+
X_HEIGHT_RATIO = 0.52
|
|
659
|
+
|
|
646
660
|
def self.baseline_target(pref, mode, state, font)
|
|
647
|
-
|
|
648
|
-
|
|
661
|
+
leading = first_paragraph_leading(state)
|
|
662
|
+
case mode
|
|
663
|
+
when "AscentOffset"
|
|
664
|
+
ratio_scaled(font.ascent, font) * leading
|
|
665
|
+
when "CapHeight"
|
|
666
|
+
cap_ratio(font) * leading
|
|
667
|
+
when "XHeight"
|
|
668
|
+
X_HEIGHT_RATIO * leading
|
|
669
|
+
when "EmboxHeight"
|
|
670
|
+
first_paragraph_size(state)
|
|
649
671
|
else
|
|
650
|
-
pref.minimum_first_baseline_offset ||
|
|
651
|
-
first_paragraph_leading(state)
|
|
672
|
+
pref.minimum_first_baseline_offset || leading
|
|
652
673
|
end
|
|
653
674
|
end
|
|
654
675
|
private_class_method :baseline_target
|
|
655
676
|
|
|
656
|
-
def self.
|
|
657
|
-
|
|
658
|
-
|
|
677
|
+
def self.cap_ratio(font)
|
|
678
|
+
cap = font.cap_height
|
|
679
|
+
return CAP_HEIGHT_RATIO unless cap
|
|
680
|
+
|
|
681
|
+
ratio_scaled(cap, font)
|
|
682
|
+
end
|
|
683
|
+
private_class_method :cap_ratio
|
|
684
|
+
|
|
685
|
+
def self.ratio_scaled(metric, font)
|
|
686
|
+
upem = font.units_per_em
|
|
687
|
+
return 1.0 if upem.zero?
|
|
688
|
+
|
|
689
|
+
metric / upem
|
|
690
|
+
end
|
|
691
|
+
private_class_method :ratio_scaled
|
|
692
|
+
|
|
693
|
+
def self.first_paragraph_size(state)
|
|
694
|
+
paragraph = state.current_paragraph || state.paragraphs.first
|
|
695
|
+
first_run = paragraph&.runs&.first
|
|
696
|
+
first_run&.point_size || DEFAULT_SIZE
|
|
659
697
|
end
|
|
660
|
-
private_class_method :
|
|
698
|
+
private_class_method :first_paragraph_size
|
|
661
699
|
|
|
662
700
|
def self.first_paragraph_leading(state)
|
|
663
701
|
paragraph = state.current_paragraph || state.paragraphs.first
|
|
@@ -839,14 +877,17 @@ module Idml
|
|
|
839
877
|
rendered_count)
|
|
840
878
|
run_wrap = drop_cap_wrap_width(wrap_width, dc_width,
|
|
841
879
|
dc_lines, rendered_count)
|
|
842
|
-
|
|
843
|
-
|
|
880
|
+
wrap_reduction, wrap_shift = text_wrap_adjust(
|
|
881
|
+
context, layout_frame, cursor_y, run
|
|
882
|
+
)
|
|
883
|
+
run_wrap -= wrap_reduction
|
|
844
884
|
positioned, next_y = layout_run(
|
|
845
885
|
canvas, run, paragraph, context, layout_frame, font,
|
|
846
886
|
run_wrap, cursor_y, space_before,
|
|
847
887
|
para_attrs[:first_line_indent], para_attrs[:left_indent],
|
|
848
888
|
char_cursor, bottom_limit,
|
|
849
|
-
paragraph_last: run.equal?(runs.last)
|
|
889
|
+
paragraph_last: run.equal?(runs.last),
|
|
890
|
+
wrap_shift: wrap_shift
|
|
850
891
|
)
|
|
851
892
|
char_cursor += positioned.length
|
|
852
893
|
cursor_y = next_y
|
|
@@ -905,26 +946,27 @@ module Idml
|
|
|
905
946
|
end
|
|
906
947
|
private_class_method :render_footnote_entries
|
|
907
948
|
|
|
908
|
-
# Computes the text-wrap
|
|
909
|
-
# position
|
|
910
|
-
#
|
|
911
|
-
#
|
|
912
|
-
#
|
|
913
|
-
#
|
|
914
|
-
#
|
|
915
|
-
|
|
949
|
+
# Computes the text-wrap adjustment for a run at its current y
|
|
950
|
+
# position: [width_reduction, x_shift]. The reduction narrows
|
|
951
|
+
# the wrap width; the shift moves wrapped lines right past a
|
|
952
|
+
# contour the text must flow around (TextWrapSide RightSide /
|
|
953
|
+
# LargestArea). Uses the run's point_size as the line height
|
|
954
|
+
# approximation. Returns [0.0, 0.0] when no resolver is wired
|
|
955
|
+
# or no contours overlap. Per-run approximation: all lines in
|
|
956
|
+
# the run get the same reduced width and shift (correct when
|
|
957
|
+
# the run is fully inside or outside the contour; approximate
|
|
958
|
+
# when it spans the boundary).
|
|
959
|
+
def self.text_wrap_adjust(context, layout_frame, cursor_y, run)
|
|
916
960
|
resolver = context.text_wrap_resolver
|
|
917
|
-
return 0.0 unless resolver
|
|
961
|
+
return [0.0, 0.0] unless resolver
|
|
918
962
|
|
|
919
963
|
size = run.point_size || DEFAULT_SIZE
|
|
920
964
|
frame_x = layout_frame.x + (layout_frame.inset_left || 0)
|
|
921
965
|
frame_right = layout_frame.x + layout_frame.width -
|
|
922
966
|
(layout_frame.inset_right || 0)
|
|
923
|
-
|
|
924
|
-
frame_x, frame_right)
|
|
925
|
-
[overlap, 0.0].max
|
|
967
|
+
resolver.wrap_adjustment(cursor_y, size, frame_x, frame_right)
|
|
926
968
|
end
|
|
927
|
-
private_class_method :
|
|
969
|
+
private_class_method :text_wrap_adjust
|
|
928
970
|
|
|
929
971
|
# Strips drop cap characters from the first run so they don't
|
|
930
972
|
# render twice (once as the enlarged drop cap, once as normal
|
|
@@ -1047,7 +1089,8 @@ module Idml
|
|
|
1047
1089
|
def self.layout_run(canvas, run, paragraph, context, layout_frame,
|
|
1048
1090
|
font, wrap_width, cursor_y, space_before,
|
|
1049
1091
|
first_line_indent, left_indent,
|
|
1050
|
-
char_cursor, bottom_limit, paragraph_last: false
|
|
1092
|
+
char_cursor, bottom_limit, paragraph_last: false,
|
|
1093
|
+
wrap_shift: 0.0)
|
|
1051
1094
|
size = run.point_size || DEFAULT_SIZE
|
|
1052
1095
|
glyphs = TextEngine::Shaper.shape(
|
|
1053
1096
|
text: run.text, font: font, size: size,
|
|
@@ -1056,6 +1099,7 @@ module Idml
|
|
|
1056
1099
|
glyphs: glyphs, frame_width: wrap_width,
|
|
1057
1100
|
)
|
|
1058
1101
|
justify_lines(lines, paragraph, wrap_width, paragraph_last)
|
|
1102
|
+
lines.each { |line| line.x_offset += wrap_shift }
|
|
1059
1103
|
leading = leading_for(paragraph, size)
|
|
1060
1104
|
|
|
1061
1105
|
positioned, next_y = TextEngine::VerticalLayout.layout_block(
|
|
@@ -1208,6 +1252,7 @@ module Idml
|
|
|
1208
1252
|
max_word_spacing: paragraph.maximum_word_spacing,
|
|
1209
1253
|
max_letter_spacing: paragraph.maximum_letter_spacing,
|
|
1210
1254
|
max_glyph_scaling: paragraph.maximum_glyph_scaling,
|
|
1255
|
+
min_glyph_scaling: paragraph.minimum_glyph_scaling,
|
|
1211
1256
|
)
|
|
1212
1257
|
lines.each_with_index do |line, index|
|
|
1213
1258
|
TextEngine::Justifier.justify(
|
|
@@ -1351,12 +1396,38 @@ module Idml
|
|
|
1351
1396
|
runs_for,
|
|
1352
1397
|
at: [box[:x], box[:y] + box[:height] - first_size],
|
|
1353
1398
|
)
|
|
1399
|
+
emit_simple_footnotes(canvas, runs, context, box)
|
|
1354
1400
|
state.paragraphs = []
|
|
1355
1401
|
state.current_paragraph = nil
|
|
1356
1402
|
state.runs_remaining = []
|
|
1357
1403
|
end
|
|
1358
1404
|
private_class_method :simple_render
|
|
1359
1405
|
|
|
1406
|
+
# Footnote text for the rough (no-metrics) path: one line
|
|
1407
|
+
# per footnote paragraph, stacked upward from the frame's
|
|
1408
|
+
# bottom edge below a hairline separator. Paragraphs are
|
|
1409
|
+
# already marker-prefixed at extraction.
|
|
1410
|
+
def self.emit_simple_footnotes(canvas, runs, context, box)
|
|
1411
|
+
paragraphs = runs.flat_map(&:footnote_paragraphs).compact
|
|
1412
|
+
return if paragraphs.empty?
|
|
1413
|
+
|
|
1414
|
+
size = paragraphs.first.runs.first&.point_size || DEFAULT_SIZE
|
|
1415
|
+
block_top = box[:y] + (paragraphs.length * size)
|
|
1416
|
+
canvas.line_width = 0.5
|
|
1417
|
+
canvas.stroke_color([:gray, 0.0])
|
|
1418
|
+
canvas.move_to(box[:x], block_top + (size * 0.3))
|
|
1419
|
+
canvas.line_to(box[:x] + (box[:width] * 0.25), block_top + (size * 0.3))
|
|
1420
|
+
canvas.stroke
|
|
1421
|
+
|
|
1422
|
+
paragraphs.reverse_each.with_index do |paragraph, index|
|
|
1423
|
+
canvas.text_rich(
|
|
1424
|
+
build_rich_runs(paragraph.runs, context),
|
|
1425
|
+
at: [box[:x], box[:y] + ((index + 1) * size)],
|
|
1426
|
+
)
|
|
1427
|
+
end
|
|
1428
|
+
end
|
|
1429
|
+
private_class_method :emit_simple_footnotes
|
|
1430
|
+
|
|
1360
1431
|
def self.collect_runs(state)
|
|
1361
1432
|
runs = state.runs_remaining || []
|
|
1362
1433
|
if state.current_paragraph
|
|
@@ -69,6 +69,7 @@ module Idml
|
|
|
69
69
|
:maximum_word_spacing,
|
|
70
70
|
:maximum_letter_spacing,
|
|
71
71
|
:maximum_glyph_scaling,
|
|
72
|
+
:minimum_glyph_scaling,
|
|
72
73
|
# Forced paragraph break (StartParagraph: NextPage /
|
|
73
74
|
# NextColumn / NextFrame / NextOddPage / NextEvenPage).
|
|
74
75
|
:start_paragraph,
|
|
@@ -212,6 +213,9 @@ module Idml
|
|
|
212
213
|
maximum_glyph_scaling: resolve_attr(
|
|
213
214
|
style_lookup, psr, :maximum_glyph_scaling
|
|
214
215
|
),
|
|
216
|
+
minimum_glyph_scaling: resolve_attr(
|
|
217
|
+
style_lookup, psr, :minimum_glyph_scaling
|
|
218
|
+
),
|
|
215
219
|
start_paragraph: resolve_attr(style_lookup, psr,
|
|
216
220
|
:start_paragraph),
|
|
217
221
|
keep_all_lines_together: resolve_attr(
|
|
@@ -9,12 +9,16 @@ module Idml
|
|
|
9
9
|
# text line to compute the effective wrap width — lines that
|
|
10
10
|
# overlap a contour get their width reduced by the overlap.
|
|
11
11
|
#
|
|
12
|
-
# Supported: BoundingBox mode (rectangle intersection)
|
|
13
|
-
#
|
|
12
|
+
# Supported: BoundingBox mode (rectangle intersection) with
|
|
13
|
+
# TextWrapSide awareness (LeftSide / RightSide / LargestArea
|
|
14
|
+
# pick the side text flows on; BothSides and the spine variants
|
|
15
|
+
# narrow by the full overlap). Shape contours (Contour mode)
|
|
16
|
+
# narrow by polygon overlap without side awareness.
|
|
17
|
+
# Unsupported: Inverse mode's side behavior.
|
|
14
18
|
class TextWrapResolver
|
|
15
19
|
# A wrap contour: a PDF-coordinate rectangle the text avoids.
|
|
16
20
|
Contour = Struct.new(
|
|
17
|
-
:x, :y, :width, :height, :inverse,
|
|
21
|
+
:x, :y, :width, :height, :inverse, :side,
|
|
18
22
|
keyword_init: true
|
|
19
23
|
)
|
|
20
24
|
|
|
@@ -37,6 +41,80 @@ module Idml
|
|
|
37
41
|
|
|
38
42
|
attr_reader :contours
|
|
39
43
|
|
|
44
|
+
# Side-aware adjustment for a text line: returns
|
|
45
|
+
# [width_reduction, x_shift]. LeftSide keeps text left of the
|
|
46
|
+
# contour (reduce, no shift); RightSide moves text past the
|
|
47
|
+
# contour's right edge (reduce + shift); LargestArea picks the
|
|
48
|
+
# roomier side; BothSides and the spine variants (which need
|
|
49
|
+
# binding-side context) reduce by the full overlap. Multiple
|
|
50
|
+
# contours combine by max — the per-run approximation.
|
|
51
|
+
def wrap_adjustment(line_y, line_height, frame_x, frame_right)
|
|
52
|
+
reduction = 0.0
|
|
53
|
+
shift = 0.0
|
|
54
|
+
@contours.each do |contour|
|
|
55
|
+
unless contour.is_a?(WrapContour::Shape)
|
|
56
|
+
r, s = box_adjustment(contour, line_y, line_height,
|
|
57
|
+
frame_x, frame_right)
|
|
58
|
+
reduction = [reduction, r].max
|
|
59
|
+
shift = [shift, s].max
|
|
60
|
+
next
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
width = WrapContour.overlap_width(contour, line_y,
|
|
64
|
+
line_height, frame_x,
|
|
65
|
+
frame_right)
|
|
66
|
+
next unless contour.inverse
|
|
67
|
+
|
|
68
|
+
reduction = [reduction,
|
|
69
|
+
(frame_right - frame_x) - width].max
|
|
70
|
+
end
|
|
71
|
+
[reduction, shift]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def box_adjustment(contour, line_y, line_height, frame_x,
|
|
75
|
+
frame_right)
|
|
76
|
+
overlap = if y_overlap?(contour, line_y, line_height) &&
|
|
77
|
+
x_overlap?(contour, frame_x, frame_right)
|
|
78
|
+
x_overlap_width(contour, frame_x, frame_right)
|
|
79
|
+
else
|
|
80
|
+
0.0
|
|
81
|
+
end
|
|
82
|
+
# Inverse contours: text flows only inside, so the reduction
|
|
83
|
+
# is the frame width minus the covered part (the full width
|
|
84
|
+
# when the line misses the object entirely).
|
|
85
|
+
return [(frame_right - frame_x) - overlap, 0.0] if contour.inverse
|
|
86
|
+
|
|
87
|
+
return [0.0, 0.0] if overlap.zero?
|
|
88
|
+
|
|
89
|
+
side_adjustment(contour, overlap, frame_x, frame_right)
|
|
90
|
+
end
|
|
91
|
+
private :box_adjustment
|
|
92
|
+
|
|
93
|
+
def side_adjustment(contour, overlap, frame_x, frame_right)
|
|
94
|
+
case contour.side
|
|
95
|
+
when "LeftSide"
|
|
96
|
+
[frame_right - contour.x, 0.0]
|
|
97
|
+
when "RightSide"
|
|
98
|
+
shift = contour.x + contour.width - frame_x
|
|
99
|
+
[shift, shift]
|
|
100
|
+
when "LargestArea"
|
|
101
|
+
largest_side_adjustment(contour, frame_x, frame_right)
|
|
102
|
+
else
|
|
103
|
+
[overlap, 0.0]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
private :side_adjustment
|
|
107
|
+
|
|
108
|
+
def largest_side_adjustment(contour, frame_x, frame_right)
|
|
109
|
+
left_free = contour.x - frame_x
|
|
110
|
+
right_free = frame_right - (contour.x + contour.width)
|
|
111
|
+
return [frame_right - contour.x, 0.0] if left_free >= right_free
|
|
112
|
+
|
|
113
|
+
shift = contour.x + contour.width - frame_x
|
|
114
|
+
[shift, shift]
|
|
115
|
+
end
|
|
116
|
+
private :largest_side_adjustment
|
|
117
|
+
|
|
40
118
|
# Returns the total horizontal overlap of all contours with a
|
|
41
119
|
# text line at the given y range within the given x range.
|
|
42
120
|
# Sum, not max — multiple overlapping contours compound.
|
|
@@ -73,12 +151,12 @@ module Idml
|
|
|
73
151
|
if contour_mode?(pref)
|
|
74
152
|
WrapContour.shape(item, pref, page_height)
|
|
75
153
|
else
|
|
76
|
-
box_contour(item, page_height)
|
|
154
|
+
box_contour(item, page_height, pref)
|
|
77
155
|
end
|
|
78
156
|
end
|
|
79
157
|
private_class_method :contour_for
|
|
80
158
|
|
|
81
|
-
def self.box_contour(item, page_height)
|
|
159
|
+
def self.box_contour(item, page_height, pref = nil)
|
|
82
160
|
bounds = item.geometric_bounds
|
|
83
161
|
return nil unless bounds
|
|
84
162
|
|
|
@@ -86,7 +164,8 @@ module Idml
|
|
|
86
164
|
page_height)
|
|
87
165
|
Contour.new(
|
|
88
166
|
x: rect[:x], y: rect[:y],
|
|
89
|
-
width: rect[:width], height: rect[:height]
|
|
167
|
+
width: rect[:width], height: rect[:height],
|
|
168
|
+
side: pref&.text_wrap_side
|
|
90
169
|
)
|
|
91
170
|
end
|
|
92
171
|
private_class_method :box_contour
|
|
@@ -11,12 +11,14 @@ module Idml
|
|
|
11
11
|
# InDesign defaults: word spacing max 133%, letter spacing 0%.
|
|
12
12
|
SpacingLimits = Struct.new(
|
|
13
13
|
:max_word_spacing, :max_letter_spacing, :max_glyph_scaling,
|
|
14
|
+
:min_glyph_scaling,
|
|
14
15
|
keyword_init: true
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
DEFAULT_MAX_WORD_SPACING = 133.0
|
|
18
19
|
DEFAULT_MAX_LETTER_SPACING = 0.0
|
|
19
20
|
DEFAULT_MAX_GLYPH_SCALING = 100.0
|
|
21
|
+
DEFAULT_MIN_GLYPH_SCALING = 100.0
|
|
20
22
|
|
|
21
23
|
def self.justify(line:, frame_width:, alignment: :left,
|
|
22
24
|
last_line: false, limits: nil)
|
|
@@ -55,6 +57,8 @@ module Idml
|
|
|
55
57
|
def justify_line(line)
|
|
56
58
|
spaces = line.glyphs.count(&:is_space)
|
|
57
59
|
slack = @frame_width - line.width
|
|
60
|
+
return compress_glyphs(line, slack) if slack.negative?
|
|
61
|
+
|
|
58
62
|
return if spaces.zero? || slack <= 0
|
|
59
63
|
|
|
60
64
|
word_share = [slack, word_capacity(line, spaces)].min
|
|
@@ -99,6 +103,23 @@ module Idml
|
|
|
99
103
|
end
|
|
100
104
|
private :distribute_letter_spacing
|
|
101
105
|
|
|
106
|
+
# Overlong justified line: compress all glyphs uniformly,
|
|
107
|
+
# capped at min_glyph_scaling percent. InDesign's default cap
|
|
108
|
+
# (100%) disables it; only documents that lower the cap get
|
|
109
|
+
# squeezed glyphs.
|
|
110
|
+
def compress_glyphs(line, slack)
|
|
111
|
+
min_pct = @limits&.min_glyph_scaling || DEFAULT_MIN_GLYPH_SCALING
|
|
112
|
+
shrink = (100 - min_pct) / 100.0
|
|
113
|
+
return unless shrink.positive?
|
|
114
|
+
|
|
115
|
+
total = line.glyphs.sum(&:width)
|
|
116
|
+
return unless total.positive?
|
|
117
|
+
|
|
118
|
+
factor = [-slack / total, shrink].min
|
|
119
|
+
line.glyphs.each { |glyph| glyph.width *= (1 - factor) }
|
|
120
|
+
end
|
|
121
|
+
private :compress_glyphs
|
|
122
|
+
|
|
102
123
|
# Last resort: uniform glyph scaling for any remaining slack,
|
|
103
124
|
# capped at max_glyph_scaling percent. InDesign's default cap
|
|
104
125
|
# (100%) disables it; only documents that relax the cap get
|
|
@@ -6,10 +6,16 @@ module Idml
|
|
|
6
6
|
Line = Struct.new(:glyphs, :width, :x_offset)
|
|
7
7
|
|
|
8
8
|
# Greedy word-wrap line breaker. Accumulates glyphs until
|
|
9
|
-
# exceeding the frame width, then breaks at the last
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# exceeding the frame width, then breaks at the last break
|
|
10
|
+
# opportunity: a space, or a hyphen (compound words wrap after
|
|
11
|
+
# the hyphen, which stays on the first line). CJK runs get a
|
|
12
|
+
# kinsoku shori post-pass (no line starts or ends with a
|
|
13
|
+
# forbidden character).
|
|
12
14
|
class LineBreaker
|
|
15
|
+
# Break-after opportunities besides spaces: hyphen-minus,
|
|
16
|
+
# the Unicode hyphens, and the soft hyphen (which breaks
|
|
17
|
+
# without adding a visible hyphen).
|
|
18
|
+
HYPHEN_CODEPOINTS = [0x2D, 0x2010, 0x2011, 0x00AD].freeze
|
|
13
19
|
def self.break(glyphs:, frame_width:)
|
|
14
20
|
lines = new(frame_width).break(glyphs)
|
|
15
21
|
return lines unless cjk_run?(glyphs)
|
|
@@ -44,26 +50,26 @@ module Idml
|
|
|
44
50
|
lines = []
|
|
45
51
|
current = []
|
|
46
52
|
current_width = 0
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
break_idx = -1
|
|
54
|
+
width_at_break = 0
|
|
49
55
|
|
|
50
56
|
glyphs.each_with_index do |glyph, _idx|
|
|
51
57
|
current << glyph
|
|
52
58
|
current_width += glyph.width
|
|
53
59
|
|
|
54
|
-
if glyph
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
if break_after?(glyph)
|
|
61
|
+
break_idx = current.length - 1
|
|
62
|
+
width_at_break = current_width
|
|
57
63
|
end
|
|
58
64
|
|
|
59
65
|
next unless current_width > @frame_width && current.length > 1
|
|
60
66
|
|
|
61
|
-
if
|
|
62
|
-
line_glyphs = current[0..
|
|
63
|
-
lines << Line.new(line_glyphs,
|
|
64
|
-
current = current[(
|
|
67
|
+
if break_idx >= 0
|
|
68
|
+
line_glyphs = current[0..break_idx]
|
|
69
|
+
lines << Line.new(line_glyphs, width_at_break, 0)
|
|
70
|
+
current = current[(break_idx + 1)..]
|
|
65
71
|
current_width = current.sum(&:width)
|
|
66
|
-
|
|
72
|
+
break_idx = -1
|
|
67
73
|
elsif cjk_break?(current)
|
|
68
74
|
lines << Line.new(current[0..-2], current_width - glyph.width, 0)
|
|
69
75
|
current = [glyph]
|
|
@@ -78,6 +84,11 @@ module Idml
|
|
|
78
84
|
lines << Line.new(current, current_width, 0) if current.any?
|
|
79
85
|
lines
|
|
80
86
|
end
|
|
87
|
+
|
|
88
|
+
def break_after?(glyph)
|
|
89
|
+
glyph.is_space || HYPHEN_CODEPOINTS.include?(glyph.codepoint)
|
|
90
|
+
end
|
|
91
|
+
private :break_after?
|
|
81
92
|
end
|
|
82
93
|
end
|
|
83
94
|
end
|
|
@@ -37,6 +37,12 @@ module Idml
|
|
|
37
37
|
0
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
+
# Cap height in font units; nil when the font tables don't
|
|
41
|
+
# report one (some pdfrb-loaded fonts).
|
|
42
|
+
def cap_height
|
|
43
|
+
metrics_data[:cap_height]
|
|
44
|
+
end
|
|
45
|
+
|
|
40
46
|
# Returns raw advance width in font units (not scaled by size).
|
|
41
47
|
def glyph_width(codepoint)
|
|
42
48
|
cp = codepoint.is_a?(String) ? codepoint.each_codepoint.first : codepoint
|
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.10.
|
|
4
|
+
version: 0.10.7
|
|
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-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|
|
@@ -197,7 +197,14 @@ files:
|
|
|
197
197
|
- TODO.pdf/132-line-end-compression.md
|
|
198
198
|
- TODO.pdf/133-keep-windows.md
|
|
199
199
|
- TODO.pdf/134-table-row-flow.md
|
|
200
|
+
- TODO.pdf/135-minimum-glyph-scaling.md
|
|
201
|
+
- TODO.pdf/136-diagonal-cell-strokes.md
|
|
202
|
+
- TODO.pdf/137-simple-render-footnote-text.md
|
|
203
|
+
- TODO.pdf/138-text-wrap-sides.md
|
|
204
|
+
- TODO.pdf/139-hyphen-break-opportunities.md
|
|
200
205
|
- TODO.pdf/14-page-item-element-models.md
|
|
206
|
+
- TODO.pdf/140-cap-xheight-first-baselines.md
|
|
207
|
+
- TODO.pdf/141-perf-hundred-pages.md
|
|
201
208
|
- TODO.pdf/15-wire-spread-children.md
|
|
202
209
|
- TODO.pdf/16-typed-model-pipeline.md
|
|
203
210
|
- TODO.pdf/17-shape-rendering.md
|