idml 0.9.3 → 0.9.4
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/119-justification-spacing-limits.md +34 -0
- data/TODO.pdf/120-start-paragraph-breaks.md +26 -0
- data/TODO.pdf/121-cell-edge-strokes.md +29 -0
- data/lib/idml/elements/cell.rb +182 -0
- data/lib/idml/render/footnote.rb +6 -1
- data/lib/idml/render/renderers/table_renderer.rb +95 -4
- data/lib/idml/render/renderers/text_frame_renderer.rb +16 -1
- data/lib/idml/render/style_resolver.rb +14 -0
- data/lib/idml/text_engine/justifier.rb +43 -4
- data/lib/idml/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 41d28cdae1d32efe5815542e9f5f702affff8445c1c77b9e62e3c9229f974dca
|
|
4
|
+
data.tar.gz: d9144aafa235ef8364cd373bc7f62742bc95ed1b6646f159f00e67f72a1706e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca86d99a34bfb4cb1ab62580c3283dda29b21a060435f4dacce15e1069a49bf3009701d34c77700f5308b4c1fcd4e5d11788c89f6802398cb05a872516cae803
|
|
7
|
+
data.tar.gz: 6b7033ff88e4e5ea4edc4fbdfe7ebdb0024fbcc024107a7f5e5c18e3c7196b6323dcc66ec55485dbc743351c6d15f7bfd3eff922531d2d1ce6d713c9efa99dc7
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# TODO PDF 119: Justification word/letter spacing limits
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-19
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
`TextEngine::Justifier` stretched inter-word spaces without limit
|
|
8
|
+
under full justification — a line with one space could widen that
|
|
9
|
+
gap across the whole column. IDML paragraphs declare
|
|
10
|
+
MaximumWordSpacing / MaximumLetterSpacing (percentages,
|
|
11
|
+
InDesign defaults 133% word, 0% letter) that cap how justification
|
|
12
|
+
slack is distributed.
|
|
13
|
+
|
|
14
|
+
## What was done
|
|
15
|
+
|
|
16
|
+
- `Justifier::SpacingLimits` (max_word_spacing, max_letter_spacing
|
|
17
|
+
percentages). `Justifier.justify` takes `limits:`.
|
|
18
|
+
- Distribution: spaces stretch up to the word-spacing cap
|
|
19
|
+
(capacity = Σ natural_space_width × (max−100)/100); any residual
|
|
20
|
+
slack goes to letter spacing — an even per-glyph addition capped
|
|
21
|
+
at max_letter_spacing% of the natural space width. With
|
|
22
|
+
InDesign's default letter cap of 0%, residual slack stays
|
|
23
|
+
unfilled and the line ends short, matching InDesign's behavior
|
|
24
|
+
when limits bind.
|
|
25
|
+
- `StyleResolver::Paragraph` carries maximum_word_spacing /
|
|
26
|
+
maximum_letter_spacing from the PSR; the frame renderer and the
|
|
27
|
+
footnote layout both pass the limits.
|
|
28
|
+
|
|
29
|
+
## Known limitations
|
|
30
|
+
|
|
31
|
+
- Glyph scaling (Minimum/Maximum/DesiredGlyphScaling) is not
|
|
32
|
+
applied as a last resort.
|
|
33
|
+
- Minimum word/letter spacing (line compression) is unused — this
|
|
34
|
+
justifier only ever stretches (slack ≥ 0).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# TODO PDF 120: StartParagraph frame/column breaks
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-19
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
PSR `StartParagraph` (Anywhere / NextPage / NextColumn / NextFrame
|
|
8
|
+
/ NextOddPage / NextEvenPage) was parsed but ignored — forced-break
|
|
9
|
+
paragraphs (chapter headings, magazine copy) rendered mid-frame.
|
|
10
|
+
|
|
11
|
+
## What was done
|
|
12
|
+
|
|
13
|
+
- `StyleResolver::Paragraph` carries `start_paragraph` from the
|
|
14
|
+
PSR.
|
|
15
|
+
- `TextFrameRenderer.consume_paragraphs` stops filling the current
|
|
16
|
+
frame/column before a paragraph that requests a break (NextPage /
|
|
17
|
+
NextColumn / NextFrame / NextOddPage / NextEvenPage) — as long as
|
|
18
|
+
at least one paragraph was already placed there; the paragraph
|
|
19
|
+
and everything after it flow to the next frame via the existing
|
|
20
|
+
chain state. Multi-column frames break per column.
|
|
21
|
+
|
|
22
|
+
## Known limitations
|
|
23
|
+
|
|
24
|
+
- All break flavors act at frame/column granularity (NextPage and
|
|
25
|
+
NextFrame are indistinguishable; odd/even page parity is not
|
|
26
|
+
modeled — the renderer has no page-parity notion at this layer).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# TODO PDF 121: Per-side cell edge strokes
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-19
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
`TableRenderer` stroked every cell as a uniform rectangle with the
|
|
8
|
+
canvas default line width. IDML cells declare per-side edge stroke
|
|
9
|
+
weights (TopEdgeStrokeWeight, LeftEdgeStrokeWeight,
|
|
10
|
+
BottomEdgeStrokeWeight, RightEdgeStrokeWeight — with the matching
|
|
11
|
+
stroke colors/tints), and the table declares fallback defaults
|
|
12
|
+
(DefaultRowStrokeWeight / DefaultColumnStrokeWeight).
|
|
13
|
+
|
|
14
|
+
## What was done
|
|
15
|
+
|
|
16
|
+
- Cell_Object schema completion: `Elements::Cell` declares all 95
|
|
17
|
+
schema attributes (77 added — per-side edge strokes + diagonal
|
|
18
|
+
lines + cell style overrides), generated in schema order like the
|
|
19
|
+
Table completion (TODO 104).
|
|
20
|
+
- `TableRenderer.render_cell_border` draws each side separately
|
|
21
|
+
with the cell's edge stroke weight, falling back to the table's
|
|
22
|
+
default row/column stroke weight, and resolves stroke colors via
|
|
23
|
+
the color resolver (black fallback). Sides with zero weight are
|
|
24
|
+
skipped, so weightless cells no longer draw hairlines.
|
|
25
|
+
|
|
26
|
+
## Known limitations
|
|
27
|
+
|
|
28
|
+
- Diagonal cell strokes (DiagonalLine*) are modeled but not drawn.
|
|
29
|
+
- Edge stroke types (dashes) fall back to solid.
|
data/lib/idml/elements/cell.rb
CHANGED
|
@@ -38,6 +38,88 @@ module Idml
|
|
|
38
38
|
attribute :paragraph_style_range, Idml::Elements::ParagraphStyleRange,
|
|
39
39
|
collection: true
|
|
40
40
|
|
|
41
|
+
# The remaining Cell_Object attributes (per-side edge strokes,
|
|
42
|
+
# diagonal lines, cell style overrides, …) in schema order —
|
|
43
|
+
# generated by `scripts/rnc_to_lutaml.rb` from
|
|
44
|
+
# reference-docs/schemas/package/Stories/Story.rnc.
|
|
45
|
+
attribute :top_inset, :float
|
|
46
|
+
attribute :left_inset, :float
|
|
47
|
+
attribute :bottom_inset, :float
|
|
48
|
+
attribute :right_inset, :float
|
|
49
|
+
attribute :top_left_diagonal_line, :boolean
|
|
50
|
+
attribute :top_right_diagonal_line, :boolean
|
|
51
|
+
attribute :diagonal_line_in_front, :boolean
|
|
52
|
+
attribute :diagonal_line_stroke_weight, :float
|
|
53
|
+
attribute :diagonal_line_stroke_type, :string
|
|
54
|
+
attribute :diagonal_line_stroke_color, :string
|
|
55
|
+
attribute :diagonal_line_stroke_tint, :float
|
|
56
|
+
attribute :diagonal_line_stroke_overprint, :boolean
|
|
57
|
+
attribute :diagonal_line_stroke_gap_color, :string
|
|
58
|
+
attribute :diagonal_line_stroke_gap_tint, :float
|
|
59
|
+
attribute :diagonal_line_stroke_gap_overprint, :boolean
|
|
60
|
+
attribute :clip_content_to_cell, :boolean
|
|
61
|
+
attribute :first_baseline_offset, :string
|
|
62
|
+
attribute :minimum_first_baseline_offset, :float
|
|
63
|
+
attribute :left_edge_stroke_weight, :float
|
|
64
|
+
attribute :left_edge_stroke_type, :string
|
|
65
|
+
attribute :left_edge_stroke_color, :string
|
|
66
|
+
attribute :left_edge_stroke_tint, :float
|
|
67
|
+
attribute :left_edge_stroke_overprint, :boolean
|
|
68
|
+
attribute :left_edge_stroke_gap_color, :string
|
|
69
|
+
attribute :left_edge_stroke_gap_tint, :float
|
|
70
|
+
attribute :left_edge_stroke_gap_overprint, :boolean
|
|
71
|
+
attribute :top_edge_stroke_weight, :float
|
|
72
|
+
attribute :top_edge_stroke_type, :string
|
|
73
|
+
attribute :top_edge_stroke_color, :string
|
|
74
|
+
attribute :top_edge_stroke_tint, :float
|
|
75
|
+
attribute :top_edge_stroke_overprint, :boolean
|
|
76
|
+
attribute :top_edge_stroke_gap_color, :string
|
|
77
|
+
attribute :top_edge_stroke_gap_tint, :float
|
|
78
|
+
attribute :top_edge_stroke_gap_overprint, :boolean
|
|
79
|
+
attribute :right_edge_stroke_weight, :float
|
|
80
|
+
attribute :right_edge_stroke_type, :string
|
|
81
|
+
attribute :right_edge_stroke_color, :string
|
|
82
|
+
attribute :right_edge_stroke_tint, :float
|
|
83
|
+
attribute :right_edge_stroke_overprint, :boolean
|
|
84
|
+
attribute :right_edge_stroke_gap_color, :string
|
|
85
|
+
attribute :right_edge_stroke_gap_tint, :float
|
|
86
|
+
attribute :right_edge_stroke_gap_overprint, :boolean
|
|
87
|
+
attribute :bottom_edge_stroke_weight, :float
|
|
88
|
+
attribute :bottom_edge_stroke_type, :string
|
|
89
|
+
attribute :bottom_edge_stroke_color, :string
|
|
90
|
+
attribute :bottom_edge_stroke_tint, :float
|
|
91
|
+
attribute :bottom_edge_stroke_overprint, :boolean
|
|
92
|
+
attribute :bottom_edge_stroke_gap_color, :string
|
|
93
|
+
attribute :bottom_edge_stroke_gap_tint, :float
|
|
94
|
+
attribute :bottom_edge_stroke_gap_overprint, :boolean
|
|
95
|
+
attribute :inner_row_stroke_weight, :float
|
|
96
|
+
attribute :inner_row_stroke_type, :string
|
|
97
|
+
attribute :inner_row_stroke_color, :string
|
|
98
|
+
attribute :inner_row_stroke_tint, :float
|
|
99
|
+
attribute :inner_row_stroke_overprint, :boolean
|
|
100
|
+
attribute :inner_row_stroke_gap_color, :string
|
|
101
|
+
attribute :inner_row_stroke_gap_tint, :float
|
|
102
|
+
attribute :inner_row_stroke_gap_overprint, :boolean
|
|
103
|
+
attribute :inner_column_stroke_weight, :float
|
|
104
|
+
attribute :inner_column_stroke_type, :string
|
|
105
|
+
attribute :inner_column_stroke_color, :string
|
|
106
|
+
attribute :inner_column_stroke_tint, :float
|
|
107
|
+
attribute :inner_column_stroke_overprint, :boolean
|
|
108
|
+
attribute :inner_column_stroke_gap_color, :string
|
|
109
|
+
attribute :inner_column_stroke_gap_tint, :float
|
|
110
|
+
attribute :inner_column_stroke_gap_overprint, :boolean
|
|
111
|
+
attribute :top_edge_stroke_priority, :integer
|
|
112
|
+
attribute :left_edge_stroke_priority, :integer
|
|
113
|
+
attribute :bottom_edge_stroke_priority, :integer
|
|
114
|
+
attribute :right_edge_stroke_priority, :integer
|
|
115
|
+
attribute :graphic_left_inset, :float
|
|
116
|
+
attribute :graphic_top_inset, :float
|
|
117
|
+
attribute :graphic_right_inset, :float
|
|
118
|
+
attribute :graphic_bottom_inset, :float
|
|
119
|
+
attribute :clip_content_to_graphic_cell, :boolean
|
|
120
|
+
attribute :writing_direction, :boolean
|
|
121
|
+
attribute :applied_cell_style_priority, :integer
|
|
122
|
+
|
|
41
123
|
xml do
|
|
42
124
|
root "Cell"
|
|
43
125
|
map_attribute "Self", to: :self_attr
|
|
@@ -58,6 +140,106 @@ module Idml
|
|
|
58
140
|
map_attribute "ParagraphSpacingLimit", to: :paragraph_spacing_limit
|
|
59
141
|
map_attribute "RotationAngle", to: :rotation_angle
|
|
60
142
|
map_attribute "AppliedCellStyle", to: :applied_cell_style
|
|
143
|
+
map_attribute "TopInset", to: :top_inset
|
|
144
|
+
map_attribute "LeftInset", to: :left_inset
|
|
145
|
+
map_attribute "BottomInset", to: :bottom_inset
|
|
146
|
+
map_attribute "RightInset", to: :right_inset
|
|
147
|
+
map_attribute "TopLeftDiagonalLine", to: :top_left_diagonal_line
|
|
148
|
+
map_attribute "TopRightDiagonalLine", to: :top_right_diagonal_line
|
|
149
|
+
map_attribute "DiagonalLineInFront", to: :diagonal_line_in_front
|
|
150
|
+
map_attribute "DiagonalLineStrokeWeight",
|
|
151
|
+
to: :diagonal_line_stroke_weight
|
|
152
|
+
map_attribute "DiagonalLineStrokeType", to: :diagonal_line_stroke_type
|
|
153
|
+
map_attribute "DiagonalLineStrokeColor", to: :diagonal_line_stroke_color
|
|
154
|
+
map_attribute "DiagonalLineStrokeTint", to: :diagonal_line_stroke_tint
|
|
155
|
+
map_attribute "DiagonalLineStrokeOverprint",
|
|
156
|
+
to: :diagonal_line_stroke_overprint
|
|
157
|
+
map_attribute "DiagonalLineStrokeGapColor",
|
|
158
|
+
to: :diagonal_line_stroke_gap_color
|
|
159
|
+
map_attribute "DiagonalLineStrokeGapTint",
|
|
160
|
+
to: :diagonal_line_stroke_gap_tint
|
|
161
|
+
map_attribute "DiagonalLineStrokeGapOverprint",
|
|
162
|
+
to: :diagonal_line_stroke_gap_overprint
|
|
163
|
+
map_attribute "ClipContentToCell", to: :clip_content_to_cell
|
|
164
|
+
map_attribute "FirstBaselineOffset", to: :first_baseline_offset
|
|
165
|
+
map_attribute "MinimumFirstBaselineOffset",
|
|
166
|
+
to: :minimum_first_baseline_offset
|
|
167
|
+
map_attribute "LeftEdgeStrokeWeight", to: :left_edge_stroke_weight
|
|
168
|
+
map_attribute "LeftEdgeStrokeType", to: :left_edge_stroke_type
|
|
169
|
+
map_attribute "LeftEdgeStrokeColor", to: :left_edge_stroke_color
|
|
170
|
+
map_attribute "LeftEdgeStrokeTint", to: :left_edge_stroke_tint
|
|
171
|
+
map_attribute "LeftEdgeStrokeOverprint", to: :left_edge_stroke_overprint
|
|
172
|
+
map_attribute "LeftEdgeStrokeGapColor", to: :left_edge_stroke_gap_color
|
|
173
|
+
map_attribute "LeftEdgeStrokeGapTint", to: :left_edge_stroke_gap_tint
|
|
174
|
+
map_attribute "LeftEdgeStrokeGapOverprint",
|
|
175
|
+
to: :left_edge_stroke_gap_overprint
|
|
176
|
+
map_attribute "TopEdgeStrokeWeight", to: :top_edge_stroke_weight
|
|
177
|
+
map_attribute "TopEdgeStrokeType", to: :top_edge_stroke_type
|
|
178
|
+
map_attribute "TopEdgeStrokeColor", to: :top_edge_stroke_color
|
|
179
|
+
map_attribute "TopEdgeStrokeTint", to: :top_edge_stroke_tint
|
|
180
|
+
map_attribute "TopEdgeStrokeOverprint", to: :top_edge_stroke_overprint
|
|
181
|
+
map_attribute "TopEdgeStrokeGapColor", to: :top_edge_stroke_gap_color
|
|
182
|
+
map_attribute "TopEdgeStrokeGapTint", to: :top_edge_stroke_gap_tint
|
|
183
|
+
map_attribute "TopEdgeStrokeGapOverprint",
|
|
184
|
+
to: :top_edge_stroke_gap_overprint
|
|
185
|
+
map_attribute "RightEdgeStrokeWeight", to: :right_edge_stroke_weight
|
|
186
|
+
map_attribute "RightEdgeStrokeType", to: :right_edge_stroke_type
|
|
187
|
+
map_attribute "RightEdgeStrokeColor", to: :right_edge_stroke_color
|
|
188
|
+
map_attribute "RightEdgeStrokeTint", to: :right_edge_stroke_tint
|
|
189
|
+
map_attribute "RightEdgeStrokeOverprint",
|
|
190
|
+
to: :right_edge_stroke_overprint
|
|
191
|
+
map_attribute "RightEdgeStrokeGapColor",
|
|
192
|
+
to: :right_edge_stroke_gap_color
|
|
193
|
+
map_attribute "RightEdgeStrokeGapTint", to: :right_edge_stroke_gap_tint
|
|
194
|
+
map_attribute "RightEdgeStrokeGapOverprint",
|
|
195
|
+
to: :right_edge_stroke_gap_overprint
|
|
196
|
+
map_attribute "BottomEdgeStrokeWeight", to: :bottom_edge_stroke_weight
|
|
197
|
+
map_attribute "BottomEdgeStrokeType", to: :bottom_edge_stroke_type
|
|
198
|
+
map_attribute "BottomEdgeStrokeColor", to: :bottom_edge_stroke_color
|
|
199
|
+
map_attribute "BottomEdgeStrokeTint", to: :bottom_edge_stroke_tint
|
|
200
|
+
map_attribute "BottomEdgeStrokeOverprint",
|
|
201
|
+
to: :bottom_edge_stroke_overprint
|
|
202
|
+
map_attribute "BottomEdgeStrokeGapColor",
|
|
203
|
+
to: :bottom_edge_stroke_gap_color
|
|
204
|
+
map_attribute "BottomEdgeStrokeGapTint",
|
|
205
|
+
to: :bottom_edge_stroke_gap_tint
|
|
206
|
+
map_attribute "BottomEdgeStrokeGapOverprint",
|
|
207
|
+
to: :bottom_edge_stroke_gap_overprint
|
|
208
|
+
map_attribute "InnerRowStrokeWeight", to: :inner_row_stroke_weight
|
|
209
|
+
map_attribute "InnerRowStrokeType", to: :inner_row_stroke_type
|
|
210
|
+
map_attribute "InnerRowStrokeColor", to: :inner_row_stroke_color
|
|
211
|
+
map_attribute "InnerRowStrokeTint", to: :inner_row_stroke_tint
|
|
212
|
+
map_attribute "InnerRowStrokeOverprint", to: :inner_row_stroke_overprint
|
|
213
|
+
map_attribute "InnerRowStrokeGapColor", to: :inner_row_stroke_gap_color
|
|
214
|
+
map_attribute "InnerRowStrokeGapTint", to: :inner_row_stroke_gap_tint
|
|
215
|
+
map_attribute "InnerRowStrokeGapOverprint",
|
|
216
|
+
to: :inner_row_stroke_gap_overprint
|
|
217
|
+
map_attribute "InnerColumnStrokeWeight", to: :inner_column_stroke_weight
|
|
218
|
+
map_attribute "InnerColumnStrokeType", to: :inner_column_stroke_type
|
|
219
|
+
map_attribute "InnerColumnStrokeColor", to: :inner_column_stroke_color
|
|
220
|
+
map_attribute "InnerColumnStrokeTint", to: :inner_column_stroke_tint
|
|
221
|
+
map_attribute "InnerColumnStrokeOverprint",
|
|
222
|
+
to: :inner_column_stroke_overprint
|
|
223
|
+
map_attribute "InnerColumnStrokeGapColor",
|
|
224
|
+
to: :inner_column_stroke_gap_color
|
|
225
|
+
map_attribute "InnerColumnStrokeGapTint",
|
|
226
|
+
to: :inner_column_stroke_gap_tint
|
|
227
|
+
map_attribute "InnerColumnStrokeGapOverprint",
|
|
228
|
+
to: :inner_column_stroke_gap_overprint
|
|
229
|
+
map_attribute "TopEdgeStrokePriority", to: :top_edge_stroke_priority
|
|
230
|
+
map_attribute "LeftEdgeStrokePriority", to: :left_edge_stroke_priority
|
|
231
|
+
map_attribute "BottomEdgeStrokePriority",
|
|
232
|
+
to: :bottom_edge_stroke_priority
|
|
233
|
+
map_attribute "RightEdgeStrokePriority", to: :right_edge_stroke_priority
|
|
234
|
+
map_attribute "GraphicLeftInset", to: :graphic_left_inset
|
|
235
|
+
map_attribute "GraphicTopInset", to: :graphic_top_inset
|
|
236
|
+
map_attribute "GraphicRightInset", to: :graphic_right_inset
|
|
237
|
+
map_attribute "GraphicBottomInset", to: :graphic_bottom_inset
|
|
238
|
+
map_attribute "ClipContentToGraphicCell",
|
|
239
|
+
to: :clip_content_to_graphic_cell
|
|
240
|
+
map_attribute "WritingDirection", to: :writing_direction
|
|
241
|
+
map_attribute "AppliedCellStylePriority",
|
|
242
|
+
to: :applied_cell_style_priority
|
|
61
243
|
map_element "ParagraphStyleRange", to: :paragraph_style_range
|
|
62
244
|
end
|
|
63
245
|
|
data/lib/idml/render/footnote.rb
CHANGED
|
@@ -128,11 +128,16 @@ module Idml
|
|
|
128
128
|
size: size)
|
|
129
129
|
lines = TextEngine::LineBreaker.break(glyphs: glyphs,
|
|
130
130
|
frame_width: wrap_width)
|
|
131
|
+
limits = TextEngine::Justifier::SpacingLimits.new(
|
|
132
|
+
max_word_spacing: paragraph.maximum_word_spacing,
|
|
133
|
+
max_letter_spacing: paragraph.maximum_letter_spacing,
|
|
134
|
+
)
|
|
131
135
|
lines.each_with_index do |line, index|
|
|
132
136
|
TextEngine::Justifier.justify(
|
|
133
137
|
line: line, frame_width: wrap_width,
|
|
134
138
|
alignment: paragraph.alignment || :left,
|
|
135
|
-
last_line: last_line?(paragraph, run, index, lines)
|
|
139
|
+
last_line: last_line?(paragraph, run, index, lines),
|
|
140
|
+
limits: limits
|
|
136
141
|
)
|
|
137
142
|
end
|
|
138
143
|
leading = TextEngine::VerticalLayout.leading_for(
|
|
@@ -76,7 +76,8 @@ module Idml
|
|
|
76
76
|
cell_x, cell_y, cell_w, cell_h, context)
|
|
77
77
|
render_cell_background(canvas, cell, cell_x, cell_y, cell_w, cell_h,
|
|
78
78
|
context)
|
|
79
|
-
render_cell_border(canvas,
|
|
79
|
+
render_cell_border(canvas, cell, table, cell_x, cell_y,
|
|
80
|
+
cell_w, cell_h, context)
|
|
80
81
|
render_cell_text(canvas, cell, cell_x, cell_y, cell_h, context)
|
|
81
82
|
end
|
|
82
83
|
end
|
|
@@ -113,12 +114,102 @@ module Idml
|
|
|
113
114
|
end
|
|
114
115
|
private_class_method :render_cell_background
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
# Cell borders: per-side strokes when the cell or table
|
|
118
|
+
# declare edge weights (legacy single rect otherwise, so
|
|
119
|
+
# undeclared tables keep their existing look). Top/bottom
|
|
120
|
+
# fall back to the table's default row stroke, left/right
|
|
121
|
+
# to the default column stroke; zero-weight sides draw
|
|
122
|
+
# nothing.
|
|
123
|
+
def self.render_cell_border(canvas, cell, table, x, y, w, h, context)
|
|
124
|
+
sides = cell_edge_weights(cell, table)
|
|
125
|
+
if sides.nil?
|
|
126
|
+
canvas.rectangle(x, y, w, h)
|
|
127
|
+
canvas.stroke
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
top, right, bottom, left = sides
|
|
132
|
+
stroke_cell_side(canvas, context,
|
|
133
|
+
cell.top_edge_stroke_color,
|
|
134
|
+
table.default_row_stroke_color, top) do
|
|
135
|
+
canvas.move_to(x, y + h)
|
|
136
|
+
canvas.line_to(x + w, y + h)
|
|
137
|
+
end
|
|
138
|
+
stroke_cell_side(canvas, context,
|
|
139
|
+
cell.bottom_edge_stroke_color,
|
|
140
|
+
table.default_row_stroke_color, bottom) do
|
|
141
|
+
canvas.move_to(x, y)
|
|
142
|
+
canvas.line_to(x + w, y)
|
|
143
|
+
end
|
|
144
|
+
stroke_cell_side(canvas, context,
|
|
145
|
+
cell.left_edge_stroke_color,
|
|
146
|
+
table.default_column_stroke_color, left) do
|
|
147
|
+
canvas.move_to(x, y)
|
|
148
|
+
canvas.line_to(x, y + h)
|
|
149
|
+
end
|
|
150
|
+
stroke_cell_side(canvas, context,
|
|
151
|
+
cell.right_edge_stroke_color,
|
|
152
|
+
table.default_column_stroke_color, right) do
|
|
153
|
+
canvas.move_to(x + w, y)
|
|
154
|
+
canvas.line_to(x + w, y + h)
|
|
155
|
+
end
|
|
119
156
|
end
|
|
120
157
|
private_class_method :render_cell_border
|
|
121
158
|
|
|
159
|
+
# [top, right, bottom, left] effective weights, or nil when
|
|
160
|
+
# neither cell edges nor table defaults declare any stroke
|
|
161
|
+
# (the legacy uniform-rect case).
|
|
162
|
+
def self.cell_edge_weights(cell, table)
|
|
163
|
+
return nil unless stroke_declared?(cell, table)
|
|
164
|
+
|
|
165
|
+
[
|
|
166
|
+
side_weight(cell.top_edge_stroke_weight,
|
|
167
|
+
table.default_row_stroke_weight),
|
|
168
|
+
side_weight(cell.right_edge_stroke_weight,
|
|
169
|
+
table.default_column_stroke_weight),
|
|
170
|
+
side_weight(cell.bottom_edge_stroke_weight,
|
|
171
|
+
table.default_row_stroke_weight),
|
|
172
|
+
side_weight(cell.left_edge_stroke_weight,
|
|
173
|
+
table.default_column_stroke_weight),
|
|
174
|
+
]
|
|
175
|
+
end
|
|
176
|
+
private_class_method :cell_edge_weights
|
|
177
|
+
|
|
178
|
+
def self.stroke_declared?(cell, table)
|
|
179
|
+
[
|
|
180
|
+
cell.top_edge_stroke_weight, cell.right_edge_stroke_weight,
|
|
181
|
+
cell.bottom_edge_stroke_weight, cell.left_edge_stroke_weight,
|
|
182
|
+
table.default_row_stroke_weight,
|
|
183
|
+
table.default_column_stroke_weight
|
|
184
|
+
].compact.any?
|
|
185
|
+
end
|
|
186
|
+
private_class_method :stroke_declared?
|
|
187
|
+
|
|
188
|
+
def self.side_weight(cell_weight, default_weight)
|
|
189
|
+
cell_weight || default_weight || 0
|
|
190
|
+
end
|
|
191
|
+
private_class_method :side_weight
|
|
192
|
+
|
|
193
|
+
def self.stroke_cell_side(canvas, context, cell_color,
|
|
194
|
+
table_color, weight)
|
|
195
|
+
thickness = weight.to_f
|
|
196
|
+
return unless thickness.positive?
|
|
197
|
+
|
|
198
|
+
apply_cell_stroke_color(canvas, context, cell_color, table_color)
|
|
199
|
+
canvas.line_width = thickness
|
|
200
|
+
yield
|
|
201
|
+
canvas.stroke
|
|
202
|
+
end
|
|
203
|
+
private_class_method :stroke_cell_side
|
|
204
|
+
|
|
205
|
+
def self.apply_cell_stroke_color(canvas, context, cell_color,
|
|
206
|
+
table_color)
|
|
207
|
+
name = cell_color || table_color
|
|
208
|
+
color = name && context.color_resolver&.resolve(name)
|
|
209
|
+
canvas.stroke_color(color ? ColorHelper.to_canvas(color) : [:gray, 1.0])
|
|
210
|
+
end
|
|
211
|
+
private_class_method :apply_cell_stroke_color
|
|
212
|
+
|
|
122
213
|
def self.cell_fill_color(cell, context)
|
|
123
214
|
return nil unless cell.fill_color
|
|
124
215
|
return nil if cell.fill_color == "Color/None"
|
|
@@ -410,6 +410,7 @@ module Idml
|
|
|
410
410
|
pending = 0
|
|
411
411
|
state.paragraphs.each do |paragraph|
|
|
412
412
|
break if cursor_y < bottom_limit
|
|
413
|
+
break if paragraph_break?(paragraph) && pending.positive?
|
|
413
414
|
|
|
414
415
|
remaining_runs, cursor_y, char_cursor = render_runs_for_paragraph(
|
|
415
416
|
canvas, paragraph, paragraph.runs, context,
|
|
@@ -741,16 +742,30 @@ module Idml
|
|
|
741
742
|
end
|
|
742
743
|
private_class_method :layout_run
|
|
743
744
|
|
|
745
|
+
# True when the paragraph requests a forced break to the
|
|
746
|
+
# next frame/column (StartParagraph). All break flavors act
|
|
747
|
+
# at frame/column granularity.
|
|
748
|
+
def self.paragraph_break?(paragraph)
|
|
749
|
+
%w[NextPage NextColumn NextFrame NextOddPage NextEvenPage]
|
|
750
|
+
.include?(paragraph.start_paragraph)
|
|
751
|
+
end
|
|
752
|
+
private_class_method :paragraph_break?
|
|
753
|
+
|
|
744
754
|
# Justifies the run's lines; only the final line of the
|
|
745
755
|
# paragraph's final run stays ragged under full
|
|
746
756
|
# justification.
|
|
747
757
|
def self.justify_lines(lines, paragraph, wrap_width,
|
|
748
758
|
paragraph_last)
|
|
759
|
+
limits = TextEngine::Justifier::SpacingLimits.new(
|
|
760
|
+
max_word_spacing: paragraph.maximum_word_spacing,
|
|
761
|
+
max_letter_spacing: paragraph.maximum_letter_spacing,
|
|
762
|
+
)
|
|
749
763
|
lines.each_with_index do |line, index|
|
|
750
764
|
TextEngine::Justifier.justify(
|
|
751
765
|
line: line, frame_width: wrap_width,
|
|
752
766
|
alignment: paragraph.alignment || :left,
|
|
753
|
-
last_line: paragraph_last && index == lines.length - 1
|
|
767
|
+
last_line: paragraph_last && index == lines.length - 1,
|
|
768
|
+
limits: limits
|
|
754
769
|
)
|
|
755
770
|
end
|
|
756
771
|
end
|
|
@@ -64,6 +64,12 @@ module Idml
|
|
|
64
64
|
:right_indent,
|
|
65
65
|
:auto_leading,
|
|
66
66
|
:drop_cap_lines,
|
|
67
|
+
# Justification distribution caps (percent; from PSR).
|
|
68
|
+
:maximum_word_spacing,
|
|
69
|
+
:maximum_letter_spacing,
|
|
70
|
+
# Forced paragraph break (StartParagraph: NextPage /
|
|
71
|
+
# NextColumn / NextFrame / NextOddPage / NextEvenPage).
|
|
72
|
+
:start_paragraph,
|
|
67
73
|
:bullets_and_numbering_list_type,
|
|
68
74
|
:bullet_character_value,
|
|
69
75
|
:bullets_text_after,
|
|
@@ -183,6 +189,14 @@ module Idml
|
|
|
183
189
|
left_indent: resolve_attr(style_lookup, psr, :left_indent),
|
|
184
190
|
right_indent: resolve_attr(style_lookup, psr, :right_indent),
|
|
185
191
|
auto_leading: resolve_attr(style_lookup, psr, :auto_leading),
|
|
192
|
+
maximum_word_spacing: resolve_attr(
|
|
193
|
+
style_lookup, psr, :maximum_word_spacing
|
|
194
|
+
),
|
|
195
|
+
maximum_letter_spacing: resolve_attr(
|
|
196
|
+
style_lookup, psr, :maximum_letter_spacing
|
|
197
|
+
),
|
|
198
|
+
start_paragraph: resolve_attr(style_lookup, psr,
|
|
199
|
+
:start_paragraph),
|
|
186
200
|
drop_cap_lines: resolve_attr(style_lookup, psr, :drop_cap_lines),
|
|
187
201
|
drop_cap_characters: resolve_attr(style_lookup, psr,
|
|
188
202
|
:drop_cap_characters),
|
|
@@ -7,15 +7,25 @@ module Idml
|
|
|
7
7
|
# spaces for justified text. `last_line:` (the paragraph's final
|
|
8
8
|
# line) keeps ragged right under :justified, as InDesign does.
|
|
9
9
|
class Justifier
|
|
10
|
+
# Justification distribution caps, in percent (100 = natural).
|
|
11
|
+
# InDesign defaults: word spacing max 133%, letter spacing 0%.
|
|
12
|
+
SpacingLimits = Struct.new(
|
|
13
|
+
:max_word_spacing, :max_letter_spacing, keyword_init: true
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
DEFAULT_MAX_WORD_SPACING = 133.0
|
|
17
|
+
DEFAULT_MAX_LETTER_SPACING = 0.0
|
|
18
|
+
|
|
10
19
|
def self.justify(line:, frame_width:, alignment: :left,
|
|
11
|
-
last_line: false)
|
|
12
|
-
new(frame_width, alignment, last_line).justify(line)
|
|
20
|
+
last_line: false, limits: nil)
|
|
21
|
+
new(frame_width, alignment, last_line, limits).justify(line)
|
|
13
22
|
end
|
|
14
23
|
|
|
15
|
-
def initialize(frame_width, alignment, last_line)
|
|
24
|
+
def initialize(frame_width, alignment, last_line, limits)
|
|
16
25
|
@frame_width = frame_width
|
|
17
26
|
@alignment = alignment
|
|
18
27
|
@last_line = last_line
|
|
28
|
+
@limits = limits
|
|
19
29
|
end
|
|
20
30
|
|
|
21
31
|
def justify(line)
|
|
@@ -45,13 +55,42 @@ module Idml
|
|
|
45
55
|
slack = @frame_width - line.width
|
|
46
56
|
return if spaces.zero? || slack <= 0
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
word_share = [slack, word_capacity(line, spaces)].min
|
|
59
|
+
extra = word_share / spaces
|
|
49
60
|
line.glyphs.each do |glyph|
|
|
50
61
|
next unless glyph.is_space
|
|
51
62
|
|
|
52
63
|
glyph.width += extra
|
|
53
64
|
end
|
|
65
|
+
residual = slack - word_share
|
|
66
|
+
distribute_letter_spacing(line, residual) if residual.positive?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Total room the word spaces may grow within the max-word-
|
|
70
|
+
# spacing cap (percent of each space's natural width).
|
|
71
|
+
def word_capacity(line, _spaces)
|
|
72
|
+
max_pct = @limits&.max_word_spacing || DEFAULT_MAX_WORD_SPACING
|
|
73
|
+
grow = [(max_pct - 100) / 100.0, 0].max
|
|
74
|
+
line.glyphs.sum { |g| g.is_space ? g.width * grow : 0.0 } * 1.0
|
|
75
|
+
end
|
|
76
|
+
private :word_capacity
|
|
77
|
+
|
|
78
|
+
# Even per-glyph letter spacing for residual slack, capped at
|
|
79
|
+
# max_letter_spacing percent of the natural space width. With
|
|
80
|
+
# the default 0% cap nothing is added — the line ends short,
|
|
81
|
+
# as InDesign does when limits bind.
|
|
82
|
+
def distribute_letter_spacing(line, residual)
|
|
83
|
+
max_pct = @limits&.max_letter_spacing || DEFAULT_MAX_LETTER_SPACING
|
|
84
|
+
return unless max_pct.positive?
|
|
85
|
+
|
|
86
|
+
space_ref = line.glyphs.find(&:is_space)&.width || 0
|
|
87
|
+
return unless space_ref.positive?
|
|
88
|
+
|
|
89
|
+
cap = (max_pct / 100.0) * space_ref * line.glyphs.length
|
|
90
|
+
add = [residual, cap].min / line.glyphs.length
|
|
91
|
+
line.glyphs.each { |glyph| glyph.width += add }
|
|
54
92
|
end
|
|
93
|
+
private :distribute_letter_spacing
|
|
55
94
|
end
|
|
56
95
|
end
|
|
57
96
|
end
|
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.9.
|
|
4
|
+
version: 0.9.4
|
|
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-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|
|
@@ -176,7 +176,10 @@ files:
|
|
|
176
176
|
- TODO.pdf/116-justification-last-line.md
|
|
177
177
|
- TODO.pdf/117-endnote-support.md
|
|
178
178
|
- TODO.pdf/118-table-band-fills.md
|
|
179
|
+
- TODO.pdf/119-justification-spacing-limits.md
|
|
179
180
|
- TODO.pdf/12-idml-to-pdf-pipeline.md
|
|
181
|
+
- TODO.pdf/120-start-paragraph-breaks.md
|
|
182
|
+
- TODO.pdf/121-cell-edge-strokes.md
|
|
180
183
|
- TODO.pdf/13-cjk-text-layout.md
|
|
181
184
|
- TODO.pdf/14-page-item-element-models.md
|
|
182
185
|
- TODO.pdf/15-wire-spread-children.md
|