idml 0.10.5 → 0.10.6
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/61-known-limitations.md +17 -17
- 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 +50 -17
- 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/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: f60b5f9020e44d5131e9f50e2ddfb673f9caca65bbdee8428b4670baaf96108f
|
|
4
|
+
data.tar.gz: 1f19b80511a14cea5fab0b76404be809a8f28e680deb91ee1b4caf9353a1b756
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9bea97bee388cae4ab403c3bea6cae233c3a378c569c0c678620bdb59a3ed5534ab9aa560866fad103d8a724351e396c72e9a533671fd44189719958f83d811e
|
|
7
|
+
data.tar.gz: 151917cdee9265410c788c85fb6f28d45d07ffc37cc3c4e3fc14b4578f2be57adee09393c284a53a883f1f1dbe105920ecd745a264f0311cc38c0c8f92d94a05
|
|
@@ -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`
|
|
@@ -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-26
|
|
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;
|
|
@@ -11,10 +11,10 @@ this list reflects the current state.
|
|
|
11
11
|
### Text layout
|
|
12
12
|
- **Hyphenation** is not implemented (Hyphenation* attributes are
|
|
13
13
|
parsed but unused); a hyphenation dictionary would be required.
|
|
14
|
-
- **Justification**: word/letter spacing caps apply (TODO 119)
|
|
15
|
-
MaximumGlyphScaling stretches
|
|
16
|
-
|
|
17
|
-
alignment
|
|
14
|
+
- **Justification**: word/letter spacing caps apply (TODO 119),
|
|
15
|
+
MaximumGlyphScaling stretches (TODO 129) and MinimumGlyphScaling
|
|
16
|
+
compresses overlong lines (TODO 135); ToBinding / RTL binding
|
|
17
|
+
alignment is not applied.
|
|
18
18
|
- **Keep options**: KeepAllLinesTogether (TODO 123), KeepWithNext
|
|
19
19
|
(TODO 127), and the KeepFirstLines / KeepLastLines windows
|
|
20
20
|
(TODO 133, line-count approximation) are honored.
|
|
@@ -23,8 +23,10 @@ this list reflects the current state.
|
|
|
23
23
|
- **StartParagraph** breaks act at frame/column granularity — no
|
|
24
24
|
odd/even page parity.
|
|
25
25
|
- **Text wrap**: BoundingBoxTextWrap, Contour, and Inverse modes
|
|
26
|
-
render (TODOs 130-131)
|
|
27
|
-
|
|
26
|
+
render (TODOs 130-131) with TextWrapSide side-awareness (TODO 138:
|
|
27
|
+
LeftSide / RightSide / LargestArea pick the flow side; spine
|
|
28
|
+
variants approximate as BothSides); JumpObject/NextColumn
|
|
29
|
+
approximate as the box; Contour-mode shapes are not side-aware.
|
|
28
30
|
|
|
29
31
|
### CJK
|
|
30
32
|
- **Mojikumi**: script spacing (TODO 125), line-end compression
|
|
@@ -34,24 +36,22 @@ this list reflects the current state.
|
|
|
34
36
|
ruby placement is beside-column; keep options do not apply.
|
|
35
37
|
|
|
36
38
|
### Structural features
|
|
37
|
-
- **Endnotes** (TODO 117): reference markers render
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
- **Endnotes** (TODO 117): reference markers render and endnote TEXT
|
|
40
|
+
renders end-of-story with ordinal prefixes; multi-story linkage
|
|
41
|
+
(which endnotes belong to which main story) is not modeled.
|
|
40
42
|
- **Footnotes** number per story; overflow balancing (NoSplitting)
|
|
41
|
-
is not modeled; the simple-render fallback shows markers
|
|
42
|
-
footnote text.
|
|
43
|
+
is not modeled; the simple-render fallback shows markers with
|
|
44
|
+
footnote text stacked at the frame bottom (TODO 137).
|
|
43
45
|
- **Anchored objects** render at stored geometry; AnchorType
|
|
44
46
|
text-reflow is not simulated.
|
|
45
47
|
- **Tables** flow across chained frames with repeated header rows
|
|
46
|
-
(TODO 134)
|
|
47
|
-
|
|
48
|
+
(TODO 134) and diagonal cell strokes draw (TODO 136);
|
|
49
|
+
row-spanning cells break at frame boundaries.
|
|
48
50
|
- **Inline anchored objects / footnotes inside table cells** are
|
|
49
51
|
not handled.
|
|
50
52
|
|
|
51
53
|
## Future enhancements
|
|
52
54
|
|
|
53
55
|
- Hyphenation dictionary integration
|
|
54
|
-
- Shape-mode text wrap contours
|
|
55
|
-
- Frame-spanning tables with header repeats
|
|
56
|
-
- Class-based mojikumi tables
|
|
56
|
+
- Shape-mode text wrap contours (side-aware)
|
|
57
57
|
- Performance benchmark for 100+ page documents
|
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).
|
|
@@ -839,14 +839,17 @@ module Idml
|
|
|
839
839
|
rendered_count)
|
|
840
840
|
run_wrap = drop_cap_wrap_width(wrap_width, dc_width,
|
|
841
841
|
dc_lines, rendered_count)
|
|
842
|
-
|
|
843
|
-
|
|
842
|
+
wrap_reduction, wrap_shift = text_wrap_adjust(
|
|
843
|
+
context, layout_frame, cursor_y, run
|
|
844
|
+
)
|
|
845
|
+
run_wrap -= wrap_reduction
|
|
844
846
|
positioned, next_y = layout_run(
|
|
845
847
|
canvas, run, paragraph, context, layout_frame, font,
|
|
846
848
|
run_wrap, cursor_y, space_before,
|
|
847
849
|
para_attrs[:first_line_indent], para_attrs[:left_indent],
|
|
848
850
|
char_cursor, bottom_limit,
|
|
849
|
-
paragraph_last: run.equal?(runs.last)
|
|
851
|
+
paragraph_last: run.equal?(runs.last),
|
|
852
|
+
wrap_shift: wrap_shift
|
|
850
853
|
)
|
|
851
854
|
char_cursor += positioned.length
|
|
852
855
|
cursor_y = next_y
|
|
@@ -905,26 +908,27 @@ module Idml
|
|
|
905
908
|
end
|
|
906
909
|
private_class_method :render_footnote_entries
|
|
907
910
|
|
|
908
|
-
# Computes the text-wrap
|
|
909
|
-
# position
|
|
910
|
-
#
|
|
911
|
-
#
|
|
912
|
-
#
|
|
913
|
-
#
|
|
914
|
-
#
|
|
915
|
-
|
|
911
|
+
# Computes the text-wrap adjustment for a run at its current y
|
|
912
|
+
# position: [width_reduction, x_shift]. The reduction narrows
|
|
913
|
+
# the wrap width; the shift moves wrapped lines right past a
|
|
914
|
+
# contour the text must flow around (TextWrapSide RightSide /
|
|
915
|
+
# LargestArea). Uses the run's point_size as the line height
|
|
916
|
+
# approximation. Returns [0.0, 0.0] when no resolver is wired
|
|
917
|
+
# or no contours overlap. Per-run approximation: all lines in
|
|
918
|
+
# the run get the same reduced width and shift (correct when
|
|
919
|
+
# the run is fully inside or outside the contour; approximate
|
|
920
|
+
# when it spans the boundary).
|
|
921
|
+
def self.text_wrap_adjust(context, layout_frame, cursor_y, run)
|
|
916
922
|
resolver = context.text_wrap_resolver
|
|
917
|
-
return 0.0 unless resolver
|
|
923
|
+
return [0.0, 0.0] unless resolver
|
|
918
924
|
|
|
919
925
|
size = run.point_size || DEFAULT_SIZE
|
|
920
926
|
frame_x = layout_frame.x + (layout_frame.inset_left || 0)
|
|
921
927
|
frame_right = layout_frame.x + layout_frame.width -
|
|
922
928
|
(layout_frame.inset_right || 0)
|
|
923
|
-
|
|
924
|
-
frame_x, frame_right)
|
|
925
|
-
[overlap, 0.0].max
|
|
929
|
+
resolver.wrap_adjustment(cursor_y, size, frame_x, frame_right)
|
|
926
930
|
end
|
|
927
|
-
private_class_method :
|
|
931
|
+
private_class_method :text_wrap_adjust
|
|
928
932
|
|
|
929
933
|
# Strips drop cap characters from the first run so they don't
|
|
930
934
|
# render twice (once as the enlarged drop cap, once as normal
|
|
@@ -1047,7 +1051,8 @@ module Idml
|
|
|
1047
1051
|
def self.layout_run(canvas, run, paragraph, context, layout_frame,
|
|
1048
1052
|
font, wrap_width, cursor_y, space_before,
|
|
1049
1053
|
first_line_indent, left_indent,
|
|
1050
|
-
char_cursor, bottom_limit, paragraph_last: false
|
|
1054
|
+
char_cursor, bottom_limit, paragraph_last: false,
|
|
1055
|
+
wrap_shift: 0.0)
|
|
1051
1056
|
size = run.point_size || DEFAULT_SIZE
|
|
1052
1057
|
glyphs = TextEngine::Shaper.shape(
|
|
1053
1058
|
text: run.text, font: font, size: size,
|
|
@@ -1056,6 +1061,7 @@ module Idml
|
|
|
1056
1061
|
glyphs: glyphs, frame_width: wrap_width,
|
|
1057
1062
|
)
|
|
1058
1063
|
justify_lines(lines, paragraph, wrap_width, paragraph_last)
|
|
1064
|
+
lines.each { |line| line.x_offset += wrap_shift }
|
|
1059
1065
|
leading = leading_for(paragraph, size)
|
|
1060
1066
|
|
|
1061
1067
|
positioned, next_y = TextEngine::VerticalLayout.layout_block(
|
|
@@ -1208,6 +1214,7 @@ module Idml
|
|
|
1208
1214
|
max_word_spacing: paragraph.maximum_word_spacing,
|
|
1209
1215
|
max_letter_spacing: paragraph.maximum_letter_spacing,
|
|
1210
1216
|
max_glyph_scaling: paragraph.maximum_glyph_scaling,
|
|
1217
|
+
min_glyph_scaling: paragraph.minimum_glyph_scaling,
|
|
1211
1218
|
)
|
|
1212
1219
|
lines.each_with_index do |line, index|
|
|
1213
1220
|
TextEngine::Justifier.justify(
|
|
@@ -1351,12 +1358,38 @@ module Idml
|
|
|
1351
1358
|
runs_for,
|
|
1352
1359
|
at: [box[:x], box[:y] + box[:height] - first_size],
|
|
1353
1360
|
)
|
|
1361
|
+
emit_simple_footnotes(canvas, runs, context, box)
|
|
1354
1362
|
state.paragraphs = []
|
|
1355
1363
|
state.current_paragraph = nil
|
|
1356
1364
|
state.runs_remaining = []
|
|
1357
1365
|
end
|
|
1358
1366
|
private_class_method :simple_render
|
|
1359
1367
|
|
|
1368
|
+
# Footnote text for the rough (no-metrics) path: one line
|
|
1369
|
+
# per footnote paragraph, stacked upward from the frame's
|
|
1370
|
+
# bottom edge below a hairline separator. Paragraphs are
|
|
1371
|
+
# already marker-prefixed at extraction.
|
|
1372
|
+
def self.emit_simple_footnotes(canvas, runs, context, box)
|
|
1373
|
+
paragraphs = runs.flat_map(&:footnote_paragraphs).compact
|
|
1374
|
+
return if paragraphs.empty?
|
|
1375
|
+
|
|
1376
|
+
size = paragraphs.first.runs.first&.point_size || DEFAULT_SIZE
|
|
1377
|
+
block_top = box[:y] + (paragraphs.length * size)
|
|
1378
|
+
canvas.line_width = 0.5
|
|
1379
|
+
canvas.stroke_color([:gray, 0.0])
|
|
1380
|
+
canvas.move_to(box[:x], block_top + (size * 0.3))
|
|
1381
|
+
canvas.line_to(box[:x] + (box[:width] * 0.25), block_top + (size * 0.3))
|
|
1382
|
+
canvas.stroke
|
|
1383
|
+
|
|
1384
|
+
paragraphs.reverse_each.with_index do |paragraph, index|
|
|
1385
|
+
canvas.text_rich(
|
|
1386
|
+
build_rich_runs(paragraph.runs, context),
|
|
1387
|
+
at: [box[:x], box[:y] + ((index + 1) * size)],
|
|
1388
|
+
)
|
|
1389
|
+
end
|
|
1390
|
+
end
|
|
1391
|
+
private_class_method :emit_simple_footnotes
|
|
1392
|
+
|
|
1360
1393
|
def self.collect_runs(state)
|
|
1361
1394
|
runs = state.runs_remaining || []
|
|
1362
1395
|
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
|
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.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -197,6 +197,10 @@ 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
|
|
200
204
|
- TODO.pdf/14-page-item-element-models.md
|
|
201
205
|
- TODO.pdf/15-wire-spread-children.md
|
|
202
206
|
- TODO.pdf/16-typed-model-pipeline.md
|