idml 0.10.3 → 0.10.5
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/Gemfile +6 -4
- data/TODO.pdf/117-endnote-support.md +21 -3
- data/TODO.pdf/13-cjk-text-layout.md +10 -7
- data/TODO.pdf/61-known-limitations.md +3 -3
- data/TODO.pdf/README.md +7 -4
- data/idml.gemspec +8 -1
- data/lib/idml/render/renderers/text_frame_renderer.rb +50 -0
- data/lib/idml/render.rb +1 -2
- data/lib/idml/text_engine/cjk_layout.rb +62 -0
- data/lib/idml/text_engine/line_breaker.rb +1 -0
- data/lib/idml/version.rb +1 -1
- metadata +7 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 92cf9efcb8cb1f39e0047561634dd472186e817399f86ec0ffb1052e6de31db9
|
|
4
|
+
data.tar.gz: 1226c8e6cc234f21ed9f39ed79d371fb816bed45452eb28fba5cef62517b9dce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b369f0215c2e82053016ffd629bd14894d3d790c4dd053737a8645a919dc7c8054648a882fd6a1494df335e26d252a89c61e4c85f58f7dd7f630f54ed0686107
|
|
7
|
+
data.tar.gz: 79604135da4d6126775c9ae9ac044034180f89a034b1e56116c278b13a07a1b728839b4114cf0c317c8ff8af5b67150d1d62b111f02981796fea389767bbc758
|
data/Gemfile
CHANGED
|
@@ -9,10 +9,12 @@ gem "lutaml-model", "~> 0.8.0"
|
|
|
9
9
|
gem "pdfrb", "~> 0.7"
|
|
10
10
|
gem "rake"
|
|
11
11
|
gem "rspec"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
gem "rubocop
|
|
15
|
-
gem "rubocop-
|
|
12
|
+
# Pinned: new rubocop majors introduce cops that fail CI
|
|
13
|
+
# unexpectedly (1.90 did on 2026-08-25). Bump deliberately.
|
|
14
|
+
gem "rubocop", "1.90.0"
|
|
15
|
+
gem "rubocop-performance", "1.27.0"
|
|
16
|
+
gem "rubocop-rake", "0.7.1"
|
|
17
|
+
gem "rubocop-rspec", "3.10.2"
|
|
16
18
|
gem "simplecov", require: false
|
|
17
19
|
gem "thor"
|
|
18
20
|
gem "yard"
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# TODO PDF 117: Endnote support
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
4
|
-
2026-08-18/19; endnote TEXT rendering OPEN pending a real fixture)
|
|
3
|
+
## Status: COMPLETE — implemented 2026-08-26
|
|
5
4
|
|
|
6
5
|
## Problem
|
|
7
6
|
|
|
@@ -27,7 +26,7 @@ story flagged `IsEndnoteStory="true"`. Documents with endnotes
|
|
|
27
26
|
references instead of dropping them. `StoryInner` also parses
|
|
28
27
|
`StoryPreference` (vertical writing support).
|
|
29
28
|
|
|
30
|
-
##
|
|
29
|
+
## End-of-story text rendering (2026-08-26)
|
|
31
30
|
|
|
32
31
|
1. ~~Marker~~ DONE 2026-08-19: CSR-level EndnoteRange emits a
|
|
33
32
|
superscript marker run at the CSR's position (same pattern as
|
|
@@ -48,3 +47,22 @@ The marker-position machinery (text-range resolution) cannot be
|
|
|
48
47
|
validated without a real fixture; blind implementation risks wrong
|
|
49
48
|
anchor placement on production files. Schema modeling + round-trip
|
|
50
49
|
is complete and verified.
|
|
50
|
+
|
|
51
|
+
## Final implementation notes
|
|
52
|
+
|
|
53
|
+
When a main story carries EndnoteRange markers, the package's
|
|
54
|
+
endnote stories (IsEndnoteStory="true", package order) append
|
|
55
|
+
after the main flow via the chain state — end-of-story placement
|
|
56
|
+
per the design above. Each endnote story's first run is prefixed
|
|
57
|
+
with its ordinal ("1 …", "2 …") matching the body markers'
|
|
58
|
+
sequential numbering. Verified by an end-to-end synthetic-package
|
|
59
|
+
spec (3 text blocks with the marker, 1 without).
|
|
60
|
+
|
|
61
|
+
## Known limitations
|
|
62
|
+
|
|
63
|
+
- Multi-story linkage is not modeled: ALL endnote stories follow
|
|
64
|
+
the marker-bearing story (correct for single-article documents;
|
|
65
|
+
the EndnoteTextRange chain remains the fixture-gated refinement
|
|
66
|
+
for multi-article layouts).
|
|
67
|
+
- EndnoteOption (Scope / StartAt / Prefix / Suffix) is not yet
|
|
68
|
+
modeled in Preferences; numbering is ordinal.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# TODO PDF 13: CJK text layout
|
|
2
2
|
|
|
3
|
-
## Status:
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
## Status: COMPLETE — final mojikumi pair compression landed
|
|
4
|
+
2026-08-26. Vertical writing (2026-08-19), kinsoku shori, ruby,
|
|
5
|
+
tate-chu-yoko, script spacing, and line-end compression were
|
|
6
|
+
already in.
|
|
6
7
|
|
|
7
8
|
## Vertical mode (2026-08-19)
|
|
8
9
|
|
|
@@ -22,10 +23,12 @@ kinsoku shori, ruby annotations).
|
|
|
22
23
|
|
|
23
24
|
2026-08-20 completions: tate-chu-yoko (TODO 124), Latin rotation
|
|
24
25
|
and vertical ruby (TODO 122), CJK/Latin automatic script spacing
|
|
25
|
-
(TODO 125, a mojikumi subset wired into Shaper).
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
(TODO 125, a mojikumi subset wired into Shaper). Final piece (2026-08-26): class-based pair compression — a
|
|
27
|
+
full-width closing/middle glyph followed by another full-width
|
|
28
|
+
punctuation compresses to half advance within the line (。」、
|
|
29
|
+
ー)… pairs), completing the three real mojikumi behaviors of
|
|
30
|
+
InDesign's default sets alongside script spacing (TODO 125) and
|
|
31
|
+
line-end compression (TODO 132). See `lib/idml/text_engine/cjk_layout.rb` and
|
|
29
32
|
`lib/idml/text_engine/vertical_text_layout.rb`.
|
|
30
33
|
|
|
31
34
|
## Progress 2026-08-17
|
|
@@ -27,9 +27,9 @@ this list reflects the current state.
|
|
|
27
27
|
the box; side-aware narrowing is not implemented.
|
|
28
28
|
|
|
29
29
|
### CJK
|
|
30
|
-
- **Mojikumi**:
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
- **Mojikumi**: script spacing (TODO 125), line-end compression
|
|
31
|
+
(TODO 132), and class-based pair compression (2026-08-26) are
|
|
32
|
+
applied; named mojikumi sets (詳細 etc.) are not modeled.
|
|
33
33
|
- **Vertical mode**: Latin rotation is per glyph (not run-grouped);
|
|
34
34
|
ruby placement is beside-column; keep options do not apply.
|
|
35
35
|
|
data/TODO.pdf/README.md
CHANGED
|
@@ -12,7 +12,10 @@ 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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
All entries are DONE except: TODO 117 (endnote TEXT rendering —
|
|
16
|
+
gated on a real fixture to resolve the EndnoteTextRange reference
|
|
17
|
+
chain) and TODO 13's final stretch goal (full class-based mojikumi
|
|
18
|
+
tables; script spacing and line-end compression are in). Vertical
|
|
19
|
+
writing, kinsoku, ruby, and tate-chu-yoko all landed (TODOs
|
|
20
|
+
122-125). TODO 61 is the live known-limitations index. Each file
|
|
21
|
+
carries a `## Status:` header with a brief implementation summary.
|
data/idml.gemspec
CHANGED
|
@@ -12,7 +12,14 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.authors = ["Ribose"]
|
|
13
13
|
spec.email = ["open.source@ribose.com"]
|
|
14
14
|
|
|
15
|
-
spec.summary = "Parse,
|
|
15
|
+
spec.summary = "Parse, validate, compose, and render Adobe IDML files to PDF."
|
|
16
|
+
spec.description = "Pure-Ruby toolkit for Adobe InDesign IDML: " \
|
|
17
|
+
"schema-faithful models with byte-faithful " \
|
|
18
|
+
"round-trip, RelaxNG validation, composition " \
|
|
19
|
+
"operations, and a full " \
|
|
20
|
+
"IDML-to-PDF rendering pipeline (text layout, " \
|
|
21
|
+
"tables, CJK " \
|
|
22
|
+
"vertical writing, text wrap, footnotes, PDF/A)."
|
|
16
23
|
spec.homepage = "https://github.com/metanorma/idml"
|
|
17
24
|
spec.license = "BSD-2-Clause"
|
|
18
25
|
spec.required_ruby_version = Gem::Requirement.new(">= 3.3.0")
|
|
@@ -77,6 +77,7 @@ module Idml
|
|
|
77
77
|
style_lookup: context.style_lookup,
|
|
78
78
|
footnote_option: Footnote.option(context.package)
|
|
79
79
|
)
|
|
80
|
+
paragraphs.concat(endnote_paragraphs(story, context))
|
|
80
81
|
tables = tables_in_story(story).map { |table| [table, 0] }
|
|
81
82
|
return nil if paragraphs.empty? && tables.empty?
|
|
82
83
|
|
|
@@ -114,6 +115,55 @@ module Idml
|
|
|
114
115
|
end
|
|
115
116
|
private_class_method :render_inline_tables
|
|
116
117
|
|
|
118
|
+
# End-of-story endnote text (TODO 117): when the main
|
|
119
|
+
# story carries endnote markers, the package's endnote
|
|
120
|
+
# stories (IsEndnoteStory="true", in designmap StoryList
|
|
121
|
+
# order) append after the main flow, each prefixed with
|
|
122
|
+
# its ordinal so it reads as numbered. Multi-story linkage
|
|
123
|
+
# (which endnotes belong to which main story) is not
|
|
124
|
+
# modeled — all endnote stories follow the marker-bearing
|
|
125
|
+
# story, which matches single-article documents.
|
|
126
|
+
def self.endnote_paragraphs(story, context)
|
|
127
|
+
return [] unless endnotes_marked?(story, context)
|
|
128
|
+
|
|
129
|
+
number = 0
|
|
130
|
+
context.package.stories.filter_map do |endnote_story|
|
|
131
|
+
next unless endnote_story.inner&.is_endnote_story
|
|
132
|
+
|
|
133
|
+
number += 1
|
|
134
|
+
paragraphs = StyleResolver.extract_paragraphs(
|
|
135
|
+
endnote_story,
|
|
136
|
+
condition_filter: context.condition_filter,
|
|
137
|
+
style_lookup: context.style_lookup,
|
|
138
|
+
footnote_option: Footnote.option(context.package),
|
|
139
|
+
)
|
|
140
|
+
prefix_endnote_number(paragraphs, number)
|
|
141
|
+
paragraphs
|
|
142
|
+
end.flatten
|
|
143
|
+
end
|
|
144
|
+
private_class_method :endnote_paragraphs
|
|
145
|
+
|
|
146
|
+
def self.endnotes_marked?(story, context)
|
|
147
|
+
return false unless context.package
|
|
148
|
+
return false unless story_csrs(story).any? do |csr|
|
|
149
|
+
!csr.endnote_range.empty?
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
context.package.stories.any? do |candidate|
|
|
153
|
+
candidate.inner&.is_endnote_story
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
private_class_method :endnotes_marked?
|
|
157
|
+
|
|
158
|
+
def self.prefix_endnote_number(paragraphs, number)
|
|
159
|
+
return if paragraphs.empty?
|
|
160
|
+
return if paragraphs.first.runs.empty?
|
|
161
|
+
|
|
162
|
+
first_run = paragraphs.first.runs.first
|
|
163
|
+
first_run.text = "#{number} #{first_run.text}"
|
|
164
|
+
end
|
|
165
|
+
private_class_method :prefix_endnote_number
|
|
166
|
+
|
|
117
167
|
def self.tables_in_story(story)
|
|
118
168
|
story_csrs(story).flat_map(&:table).compact
|
|
119
169
|
end
|
data/lib/idml/render.rb
CHANGED
|
@@ -50,14 +50,13 @@ module Idml
|
|
|
50
50
|
|
|
51
51
|
# Render an IDML package to a PDF file. All options except
|
|
52
52
|
# `package:` and `to:` are keyword-only and optional.
|
|
53
|
-
# rubocop:disable Metrics/ParameterLists
|
|
53
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
54
54
|
def self.render(package:, to:, font_search_paths: nil, compliance: nil,
|
|
55
55
|
tagged: false, subset_fonts: true, compress: false)
|
|
56
56
|
Pipeline.new(package, to, font_search_paths,
|
|
57
57
|
compliance: compliance, tagged: tagged,
|
|
58
58
|
subset_fonts: subset_fonts, compress: compress).call
|
|
59
59
|
end
|
|
60
|
-
# rubocop:enable Metrics/ParameterLists
|
|
61
60
|
end
|
|
62
61
|
end
|
|
63
62
|
|
|
@@ -74,6 +74,68 @@ module Idml
|
|
|
74
74
|
TRAILING_COMPRESSION_CODEPOINTS.include?(codepoint)
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# Class-based mojikumi pair compression: a full-width
|
|
78
|
+
# closing/middle glyph followed by another full-width
|
|
79
|
+
# punctuation glyph compresses to half advance within the
|
|
80
|
+
# line (。」、ー) … pairs), matching InDesign's default
|
|
81
|
+
# mojikumi behavior for consecutive 約物.
|
|
82
|
+
OPENING_CODEPOINTS = [
|
|
83
|
+
0x300C, # 「
|
|
84
|
+
0x300E, # 『
|
|
85
|
+
0x3008, # 〈
|
|
86
|
+
0x300A, # 《
|
|
87
|
+
0x3010, # 【
|
|
88
|
+
0x3014, # 〔
|
|
89
|
+
0xFF08, # (fullwidth left paren
|
|
90
|
+
0xFF3B, # [
|
|
91
|
+
0xFF5B, # {
|
|
92
|
+
].freeze
|
|
93
|
+
|
|
94
|
+
MIDDLE_CODEPOINTS = [
|
|
95
|
+
0x30FB, # ・ fullwidth middle dot
|
|
96
|
+
0x30FC, # ー prolonged sound mark
|
|
97
|
+
0x2025, # ‥ two-dot leader
|
|
98
|
+
0x2026, # … ellipsis
|
|
99
|
+
].freeze
|
|
100
|
+
|
|
101
|
+
def apply_pair_compression(lines)
|
|
102
|
+
lines.each do |line|
|
|
103
|
+
glyphs = line.glyphs
|
|
104
|
+
(0...(glyphs.length - 1)).each do |index|
|
|
105
|
+
leading = glyphs[index]
|
|
106
|
+
following = glyphs[index + 1]
|
|
107
|
+
next unless pair_compressible?(leading, following)
|
|
108
|
+
|
|
109
|
+
leading.width /= 2.0
|
|
110
|
+
end
|
|
111
|
+
line.width = glyphs.sum(&:width)
|
|
112
|
+
end
|
|
113
|
+
lines
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Closing/middle glyph followed by full-width punctuation
|
|
117
|
+
# compresses the leading glyph to half advance.
|
|
118
|
+
def pair_compressible?(leading, following)
|
|
119
|
+
return false unless fullwidth_punct?(following.codepoint)
|
|
120
|
+
|
|
121
|
+
closing?(leading.codepoint) ||
|
|
122
|
+
(middle?(leading.codepoint) &&
|
|
123
|
+
!OPENING_CODEPOINTS.include?(following.codepoint))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def closing?(codepoint)
|
|
127
|
+
TRAILING_COMPRESSION_CODEPOINTS.include?(codepoint)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def middle?(codepoint)
|
|
131
|
+
MIDDLE_CODEPOINTS.include?(codepoint)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def fullwidth_punct?(codepoint)
|
|
135
|
+
closing?(codepoint) || middle?(codepoint) ||
|
|
136
|
+
OPENING_CODEPOINTS.include?(codepoint)
|
|
137
|
+
end
|
|
138
|
+
|
|
77
139
|
CJK_RANGES = [
|
|
78
140
|
0x3000..0x303F, # CJK Symbols and Punctuation
|
|
79
141
|
0x3040..0x309F, # Hiragana
|
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.5
|
|
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-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bigdecimal
|
|
@@ -94,7 +94,10 @@ dependencies:
|
|
|
94
94
|
- - ">="
|
|
95
95
|
- !ruby/object:Gem::Version
|
|
96
96
|
version: '0'
|
|
97
|
-
description:
|
|
97
|
+
description: 'Pure-Ruby toolkit for Adobe InDesign IDML: schema-faithful models with
|
|
98
|
+
byte-faithful round-trip, RelaxNG validation, composition operations, and a full
|
|
99
|
+
IDML-to-PDF rendering pipeline (text layout, tables, CJK vertical writing, text
|
|
100
|
+
wrap, footnotes, PDF/A).'
|
|
98
101
|
email:
|
|
99
102
|
- open.source@ribose.com
|
|
100
103
|
executables:
|
|
@@ -1149,5 +1152,5 @@ requirements: []
|
|
|
1149
1152
|
rubygems_version: 3.5.22
|
|
1150
1153
|
signing_key:
|
|
1151
1154
|
specification_version: 4
|
|
1152
|
-
summary: Parse,
|
|
1155
|
+
summary: Parse, validate, compose, and render Adobe IDML files to PDF.
|
|
1153
1156
|
test_files: []
|