idml 0.9.9 → 0.10.0
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/128-first-baseline-offset.md +21 -0
- data/TODO.pdf/129-justification-glyph-scaling.md +18 -0
- data/TODO.pdf/61-known-limitations.md +9 -6
- data/lib/idml/render/footnote.rb +1 -0
- data/lib/idml/render/renderers/text_frame_renderer.rb +50 -1
- data/lib/idml/render/style_resolver.rb +4 -0
- data/lib/idml/text_engine/justifier.rb +26 -2
- data/lib/idml/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 13770ed8937eeed41a40bd223bb8caadc833da870858d691caa0e1806058c286
|
|
4
|
+
data.tar.gz: dec0fe8f24c1aaa8258ecf18506560b1e0793bd7d4ebe56bf361dc1a69ff736a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0394efac9722c854cd4e8c2771d0ec494aaa8d3b50ca6ea471bbe8f5f44daf62a1f1bd4d12031ee9d59b0d7b6c5f65f5378db661d5a110d056734958369a870e'
|
|
7
|
+
data.tar.gz: 07d3a9492ea5a41819f4fe0353176671307ad7263feedeb909bd08f8284eb79b84df0fef0057db1d089a3a02ff7d255459ea2e232e5c929d3d1ffccc70685e81
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# TODO PDF 128: Text frame FirstBaselineOffset
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-23
|
|
4
|
+
|
|
5
|
+
## What was done
|
|
6
|
+
|
|
7
|
+
`TextFrameRenderer` honors
|
|
8
|
+
`TextFramePreference.FirstBaselineOffset` when positioning the
|
|
9
|
+
first line's baseline: AscentOffset places it ascent-below the top
|
|
10
|
+
inset (typically higher than the default leading-based position);
|
|
11
|
+
FixedHeight uses MinimumFirstBaselineOffset. LeadingOffset /
|
|
12
|
+
CapHeight / XHeight / EmboxHeight approximate as the existing
|
|
13
|
+
leading-based default (cap/x-height metrics are not exposed by
|
|
14
|
+
PdfrbFontMetrics). Applied in both the single-column and
|
|
15
|
+
multi-column paths.
|
|
16
|
+
|
|
17
|
+
## Known limitations
|
|
18
|
+
|
|
19
|
+
- CapHeight / XHeight offsets fall back to leading (font metrics
|
|
20
|
+
expose ascent only).
|
|
21
|
+
- Not applied in vertical writing mode.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# TODO PDF 129: Justification glyph scaling
|
|
2
|
+
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-23
|
|
4
|
+
|
|
5
|
+
## What was done
|
|
6
|
+
|
|
7
|
+
`Justifier` distributes any residual slack remaining after the
|
|
8
|
+
word-spacing and letter-spacing caps as uniform glyph scaling,
|
|
9
|
+
capped at `MaximumGlyphScaling` (PSR → Paragraph → SpacingLimits).
|
|
10
|
+
InDesign's default cap (100%) disables scaling — only documents
|
|
11
|
+
that relax the cap get stretched glyphs, matching TODO 119's
|
|
12
|
+
layered distribution: words first, then letters, then glyphs.
|
|
13
|
+
|
|
14
|
+
## Known limitations
|
|
15
|
+
|
|
16
|
+
- MinimumGlyphScaling (compression of overlong lines) is not
|
|
17
|
+
applied — the breaker never produces overlong justified lines
|
|
18
|
+
except forced unbreakable runs.
|
|
@@ -11,12 +11,15 @@ 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
|
-
|
|
16
|
-
ToBinding / RTL binding
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
(
|
|
14
|
+
- **Justification**: word/letter spacing caps apply (TODO 119) and
|
|
15
|
+
MaximumGlyphScaling stretches as the last resort (TODO 129);
|
|
16
|
+
MinimumGlyphScaling compression and ToBinding / RTL binding
|
|
17
|
+
alignment are not applied.
|
|
18
|
+
- **Keep options**: whole-paragraph KeepAllLinesTogether (TODO
|
|
19
|
+
123) and KeepWithNext (TODO 127) are honored; partial keep
|
|
20
|
+
windows (KeepFirstLines / KeepLastLines) are not.
|
|
21
|
+
- **First baseline**: AscentOffset and FixedHeight are honored
|
|
22
|
+
(TODO 128); CapHeight / XHeight approximate as leading.
|
|
20
23
|
- **StartParagraph** breaks act at frame/column granularity — no
|
|
21
24
|
odd/even page parity.
|
|
22
25
|
- **Text wrap** (TODO 114): BoundingBox mode only; Shape contour
|
data/lib/idml/render/footnote.rb
CHANGED
|
@@ -131,6 +131,7 @@ module Idml
|
|
|
131
131
|
limits = TextEngine::Justifier::SpacingLimits.new(
|
|
132
132
|
max_word_spacing: paragraph.maximum_word_spacing,
|
|
133
133
|
max_letter_spacing: paragraph.maximum_letter_spacing,
|
|
134
|
+
max_glyph_scaling: paragraph.maximum_glyph_scaling,
|
|
134
135
|
)
|
|
135
136
|
lines.each_with_index do |line, index|
|
|
136
137
|
TextEngine::Justifier.justify(
|
|
@@ -433,6 +433,7 @@ module Idml
|
|
|
433
433
|
bottom_limit = TextEngine::VerticalLayout.bottom_limit(layout_frame)
|
|
434
434
|
footnotes = []
|
|
435
435
|
cursor_y = vertical_justify_top(top_y, bottom_limit, state, context)
|
|
436
|
+
cursor_y -= first_baseline_offset(context, state, font)
|
|
436
437
|
char_cursor = state.char_cursor
|
|
437
438
|
|
|
438
439
|
if state.current_paragraph
|
|
@@ -473,7 +474,7 @@ module Idml
|
|
|
473
474
|
top_y = col_frame.y + col_frame.height - (col_frame.inset_top || 0)
|
|
474
475
|
bottom_limit = TextEngine::VerticalLayout.bottom_limit(col_frame)
|
|
475
476
|
footnotes = []
|
|
476
|
-
cursor_y = top_y
|
|
477
|
+
cursor_y = top_y - first_baseline_offset(context, state, font)
|
|
477
478
|
char_cursor = state.char_cursor
|
|
478
479
|
|
|
479
480
|
if state.current_paragraph
|
|
@@ -564,6 +565,53 @@ module Idml
|
|
|
564
565
|
end
|
|
565
566
|
private_class_method :vertical_justify_top
|
|
566
567
|
|
|
568
|
+
# FirstBaselineOffset: how far below the frame's top inset
|
|
569
|
+
# the first line's baseline sits. The layout's default is
|
|
570
|
+
# leading-based; AscentOffset shifts by (ascent − leading)
|
|
571
|
+
# and FixedHeight by (MinimumFirstBaselineOffset − leading).
|
|
572
|
+
# CapHeight / XHeight / EmboxHeight approximate as leading.
|
|
573
|
+
# Returns the extra shift to subtract from the cursor.
|
|
574
|
+
def self.first_baseline_offset(context, state, font)
|
|
575
|
+
pref = context.item&.text_frame_preference&.first
|
|
576
|
+
mode = pref&.first_baseline_offset
|
|
577
|
+
return 0.0 unless %w[AscentOffset FixedHeight].include?(mode)
|
|
578
|
+
|
|
579
|
+
baseline_target(pref, mode, state, font) -
|
|
580
|
+
first_paragraph_leading(state)
|
|
581
|
+
end
|
|
582
|
+
private_class_method :first_baseline_offset
|
|
583
|
+
|
|
584
|
+
# Distance from the top inset to the first baseline under
|
|
585
|
+
# the given mode.
|
|
586
|
+
def self.baseline_target(pref, mode, state, font)
|
|
587
|
+
if mode == "AscentOffset"
|
|
588
|
+
ascent_scaled(font) * first_paragraph_leading(state)
|
|
589
|
+
else
|
|
590
|
+
pref.minimum_first_baseline_offset ||
|
|
591
|
+
first_paragraph_leading(state)
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
private_class_method :baseline_target
|
|
595
|
+
|
|
596
|
+
def self.ascent_scaled(font)
|
|
597
|
+
upem = font.units_per_em.zero? ? 1 : font.units_per_em
|
|
598
|
+
font.ascent / upem
|
|
599
|
+
end
|
|
600
|
+
private_class_method :ascent_scaled
|
|
601
|
+
|
|
602
|
+
def self.first_paragraph_leading(state)
|
|
603
|
+
paragraph = state.current_paragraph || state.paragraphs.first
|
|
604
|
+
unless paragraph
|
|
605
|
+
return DEFAULT_SIZE *
|
|
606
|
+
TextEngine::VerticalLayout::DEFAULT_LEADING_FACTOR
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
size = paragraph.runs.first&.point_size || DEFAULT_SIZE
|
|
610
|
+
TextEngine::VerticalLayout.leading_for(paragraph.auto_leading,
|
|
611
|
+
size)
|
|
612
|
+
end
|
|
613
|
+
private_class_method :first_paragraph_leading
|
|
614
|
+
|
|
567
615
|
def self.text_frame_vertical_justification(context)
|
|
568
616
|
pref = context.item&.text_frame_preference&.first
|
|
569
617
|
pref&.vertical_justification
|
|
@@ -1051,6 +1099,7 @@ module Idml
|
|
|
1051
1099
|
limits = TextEngine::Justifier::SpacingLimits.new(
|
|
1052
1100
|
max_word_spacing: paragraph.maximum_word_spacing,
|
|
1053
1101
|
max_letter_spacing: paragraph.maximum_letter_spacing,
|
|
1102
|
+
max_glyph_scaling: paragraph.maximum_glyph_scaling,
|
|
1054
1103
|
)
|
|
1055
1104
|
lines.each_with_index do |line, index|
|
|
1056
1105
|
TextEngine::Justifier.justify(
|
|
@@ -68,6 +68,7 @@ module Idml
|
|
|
68
68
|
# Justification distribution caps (percent; from PSR).
|
|
69
69
|
:maximum_word_spacing,
|
|
70
70
|
:maximum_letter_spacing,
|
|
71
|
+
:maximum_glyph_scaling,
|
|
71
72
|
# Forced paragraph break (StartParagraph: NextPage /
|
|
72
73
|
# NextColumn / NextFrame / NextOddPage / NextEvenPage).
|
|
73
74
|
:start_paragraph,
|
|
@@ -205,6 +206,9 @@ module Idml
|
|
|
205
206
|
maximum_letter_spacing: resolve_attr(
|
|
206
207
|
style_lookup, psr, :maximum_letter_spacing
|
|
207
208
|
),
|
|
209
|
+
maximum_glyph_scaling: resolve_attr(
|
|
210
|
+
style_lookup, psr, :maximum_glyph_scaling
|
|
211
|
+
),
|
|
208
212
|
start_paragraph: resolve_attr(style_lookup, psr,
|
|
209
213
|
:start_paragraph),
|
|
210
214
|
keep_all_lines_together: resolve_attr(
|
|
@@ -10,11 +10,13 @@ module Idml
|
|
|
10
10
|
# Justification distribution caps, in percent (100 = natural).
|
|
11
11
|
# InDesign defaults: word spacing max 133%, letter spacing 0%.
|
|
12
12
|
SpacingLimits = Struct.new(
|
|
13
|
-
:max_word_spacing, :max_letter_spacing,
|
|
13
|
+
:max_word_spacing, :max_letter_spacing, :max_glyph_scaling,
|
|
14
|
+
keyword_init: true
|
|
14
15
|
)
|
|
15
16
|
|
|
16
17
|
DEFAULT_MAX_WORD_SPACING = 133.0
|
|
17
18
|
DEFAULT_MAX_LETTER_SPACING = 0.0
|
|
19
|
+
DEFAULT_MAX_GLYPH_SCALING = 100.0
|
|
18
20
|
|
|
19
21
|
def self.justify(line:, frame_width:, alignment: :left,
|
|
20
22
|
last_line: false, limits: nil)
|
|
@@ -63,7 +65,12 @@ module Idml
|
|
|
63
65
|
glyph.width += extra
|
|
64
66
|
end
|
|
65
67
|
residual = slack - word_share
|
|
66
|
-
|
|
68
|
+
return unless residual.positive?
|
|
69
|
+
|
|
70
|
+
before = line.glyphs.sum(&:width)
|
|
71
|
+
distribute_letter_spacing(line, residual)
|
|
72
|
+
residual -= (line.glyphs.sum(&:width) - before)
|
|
73
|
+
distribute_glyph_scaling(line, residual) if residual.positive?
|
|
67
74
|
end
|
|
68
75
|
|
|
69
76
|
# Total room the word spaces may grow within the max-word-
|
|
@@ -91,6 +98,23 @@ module Idml
|
|
|
91
98
|
line.glyphs.each { |glyph| glyph.width += add }
|
|
92
99
|
end
|
|
93
100
|
private :distribute_letter_spacing
|
|
101
|
+
|
|
102
|
+
# Last resort: uniform glyph scaling for any remaining slack,
|
|
103
|
+
# capped at max_glyph_scaling percent. InDesign's default cap
|
|
104
|
+
# (100%) disables it; only documents that relax the cap get
|
|
105
|
+
# stretched glyphs.
|
|
106
|
+
def distribute_glyph_scaling(line, residual)
|
|
107
|
+
max_pct = @limits&.max_glyph_scaling || DEFAULT_MAX_GLYPH_SCALING
|
|
108
|
+
grow = (max_pct - 100) / 100.0
|
|
109
|
+
return unless grow.positive?
|
|
110
|
+
|
|
111
|
+
total = line.glyphs.sum(&:width)
|
|
112
|
+
return unless total.positive?
|
|
113
|
+
|
|
114
|
+
factor = [residual / total, grow].min
|
|
115
|
+
line.glyphs.each { |glyph| glyph.width *= (1 + factor) }
|
|
116
|
+
end
|
|
117
|
+
private :distribute_glyph_scaling
|
|
94
118
|
end
|
|
95
119
|
end
|
|
96
120
|
end
|
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.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -186,6 +186,8 @@ files:
|
|
|
186
186
|
- TODO.pdf/125-script-spacing.md
|
|
187
187
|
- TODO.pdf/126-part-cache.md
|
|
188
188
|
- TODO.pdf/127-keep-with-next.md
|
|
189
|
+
- TODO.pdf/128-first-baseline-offset.md
|
|
190
|
+
- TODO.pdf/129-justification-glyph-scaling.md
|
|
189
191
|
- TODO.pdf/13-cjk-text-layout.md
|
|
190
192
|
- TODO.pdf/14-page-item-element-models.md
|
|
191
193
|
- TODO.pdf/15-wire-spread-children.md
|