idml 0.10.1 → 0.10.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7360c896ca2b95a5fa92dd030c71e3170b0f26039f5b48938f3ba038405bc46
4
- data.tar.gz: 1ce5fccd66f88f3dc5a25289e844cdfd99b391279f8bf8f659d74de9488c866e
3
+ metadata.gz: 40262a683342f0a9308e33ab2cce4a8bcf6b803288d6a0bc3ece3d2b376c6761
4
+ data.tar.gz: 78cb5495d1d9b72d5e850c6d25c8cd594622d6484476ea61a1ff239df41e9e99
5
5
  SHA512:
6
- metadata.gz: e9aaaa8864f60931ebf50be726503f2b9db6313135262aa6d740d6fda4e06a57b03b8970aaaa87eddcd29a82e562cf0bd0ee56df227c2729782f276bca447e06
7
- data.tar.gz: b4a07f57002002cc027fb1459b9524107f1fc309a1d34cbb23575416054488589e355b4bc0a548ee59f6d69e50225dc2a8a021f4848007d9f04820b3925af778
6
+ metadata.gz: b10228f8870d90e1a66f8fb9b621a26951db4a71e155a3f28165c22a3e9c44b53e7dbc17916ec4b5db0f49987403e4e645e5f0872a79cb0fe591881ff6f9caca
7
+ data.tar.gz: d4904d3ef2a6d0b1c4dfcd497597042fcedee1b5c3c1643ff892ac867e7b4c3f0881419a906527e5f1705a64292c8c5c5e06ae8af774b8ce725f81c8bcf9ff92
data/README.adoc CHANGED
@@ -1,4 +1,64 @@
1
- = Ruby parser for Adobe IDML
1
+ = idml
2
2
 
3
- IDML is the InDesign Markup Language from Adobe used by InDesign.
3
+ A pure-Ruby toolkit for Adobe InDesign IDML files: parse, validate,
4
+ round-trip, compose, and render to PDF.
4
5
 
6
+ == What it does
7
+
8
+ * **Parse & round-trip** — every IDML part is a typed
9
+ `Lutaml::Model` class generated from Adobe's RelaxNG schemas
10
+ (`reference-docs/schemas/`): byte-faithful re-serialization with
11
+ `ZIP_STORED` mimetype ordering preserved.
12
+ * **Validate** — RelaxNG validation against the committed InDesign
13
+ 2026 (DOMVersion 21.5) schemas.
14
+ * **Compose** — `insert_idml`, `add_page_from_idml`, `prefix`,
15
+ `import_xml`/`export_xml` (ported from SimpleIDML).
16
+ * **Render to PDF** — a full pure-Ruby layout + paint pipeline
17
+ (pdfrb): multi-frame threaded stories, justification with
18
+ word/letter/glyph limits, footnotes (bottom-of-frame with
19
+ separator rules), anchored objects, tables (row/column banding,
20
+ per-side cell strokes, merged cells), Bézier shape contours,
21
+ gradients, text wrap (bounding-box, contour, and inverse modes),
22
+ paragraph shading/borders/rules, drop caps, bullets & numbering,
23
+ CJK layout (kinsoku shori, ruby, vertical writing with
24
+ tate-chu-yoko and rotated Latin, script spacing, line-end
25
+ punctuation compression), tagged PDF, hyperlinks, bookmarks,
26
+ PDF/A, and font subsetting.
27
+
28
+ == Install
29
+
30
+ ....
31
+ gem install idml
32
+ ....
33
+
34
+ `.indd` files are a proprietary binary format — export them to
35
+ `.idml` from InDesign first (File → Export), or use InDesign Server
36
+ for headless automation.
37
+
38
+ == Quick start
39
+
40
+ ....
41
+ require "idml"
42
+
43
+ # Round-trip a package
44
+ pkg = Idml::Package.new("doc.idml")
45
+ spec = pkg.spreads.first
46
+
47
+ # Render to PDF
48
+ Idml.render(package: pkg, to: "doc.pdf")
49
+ ....
50
+
51
+ == Requirements
52
+
53
+ * Ruby >= 3.3 (tested through 4.0 on macOS/Linux/Windows)
54
+
55
+ == Reference material
56
+
57
+ The `reference-docs/` tree carries Adobe's IDML specification, the
58
+ Plug-in SDK's idmltools (behavioral ground truth), and the
59
+ generated RelaxNG schemas — the attribute authority for every
60
+ model class.
61
+
62
+ == License
63
+
64
+ BSD-2-Clause.
@@ -24,7 +24,8 @@ kinsoku shori, ruby annotations).
24
24
  and vertical ruby (TODO 122), CJK/Latin automatic script spacing
25
25
  (TODO 125, a mojikumi subset wired into Shaper). The remaining
26
26
  stretch goal is full class-based mojikumi (per-pair punctuation
27
- compression tables). See `lib/idml/text_engine/cjk_layout.rb` and
27
+ compression tables); line-end punctuation compression landed
28
+ 2026-08-23 as TODO 132. See `lib/idml/text_engine/cjk_layout.rb` and
28
29
  `lib/idml/text_engine/vertical_text_layout.rb`.
29
30
 
30
31
  ## Progress 2026-08-17
@@ -0,0 +1,19 @@
1
+ # TODO PDF 131: Inverse text wrap
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-23
4
+
5
+ ## What was done
6
+
7
+ `TextWrapPreference Inverse="true"` flips the wrap region: text
8
+ may only flow INSIDE the wrap shape (the avoid-region becomes
9
+ everything outside it within the frame). Works for both the
10
+ bounding-box contour and the Contour-mode polygon shape (TODO
11
+ 130); the inversion lives in one place — TextWrapResolver's
12
+ overlap dispatch — and reduces by (frame width − inside width),
13
+ so bands outside the shape block text entirely.
14
+
15
+ ## Known limitations
16
+
17
+ - Side-aware narrowing is not implemented: the reduction shrinks
18
+ the wrap width from the right regardless of which side the
19
+ shape occupies.
@@ -0,0 +1,22 @@
1
+ # TODO PDF 132: Line-end punctuation compression (mojikumi subset 2)
2
+
3
+ ## Status: COMPLETE — implemented 2026-08-23
4
+
5
+ ## What was done
6
+
7
+ `CjkLayout.apply_line_end_compression`: a CJK line whose final
8
+ glyph is full-width closing punctuation (。、」』)]}ー etc.) renders
9
+ that glyph at half advance (行末約物半角詰め), tightening the line
10
+ as InDesign's default mojikumi does. Applied in LineBreaker's CJK
11
+ post-pass after kinsoku shori, so horizontal and vertical layout
12
+ both benefit.
13
+
14
+ Also fixed a latent bug the specs caught: kinsoku's return value
15
+ (new dup'd line array) was discarded when composing the post-pass
16
+ chain — kinsoku adjustments silently vanished once a second
17
+ post-pass followed it.
18
+
19
+ ## Remaining from TODO 13
20
+
21
+ Per-pair punctuation compression tables (full class-based
22
+ mojikumi).
@@ -22,13 +22,14 @@ this list reflects the current state.
22
22
  (TODO 128); CapHeight / XHeight approximate as leading.
23
23
  - **StartParagraph** breaks act at frame/column granularity — no
24
24
  odd/even page parity.
25
- - **Text wrap**: BoundingBoxTextWrap and Contour modes render
26
- (TODO 130); JumpObject/NextColumn approximate as the box;
27
- Inverse mode is not implemented.
25
+ - **Text wrap**: BoundingBoxTextWrap, Contour, and Inverse modes
26
+ render (TODOs 130-131); JumpObject/NextColumn approximate as
27
+ the box; side-aware narrowing is not implemented.
28
28
 
29
29
  ### CJK
30
- - **Mojikumi**: CJK/Latin auto script spacing is applied (TODO
31
- 125); full class-based punctuation compression tables are not.
30
+ - **Mojikumi**: CJK/Latin auto script spacing (TODO 125) and
31
+ line-end punctuation compression (TODO 132) are applied; full
32
+ class-based compression tables are not.
32
33
  - **Vertical mode**: Latin rotation is per glyph (not run-grouped);
33
34
  ruby placement is beside-column; keep options do not apply.
34
35
 
@@ -14,7 +14,7 @@ module Idml
14
14
  class TextWrapResolver
15
15
  # A wrap contour: a PDF-coordinate rectangle the text avoids.
16
16
  Contour = Struct.new(
17
- :x, :y, :width, :height,
17
+ :x, :y, :width, :height, :inverse,
18
18
  keyword_init: true
19
19
  )
20
20
 
@@ -40,16 +40,22 @@ module Idml
40
40
  # Returns the total horizontal overlap of all contours with a
41
41
  # text line at the given y range within the given x range.
42
42
  # Sum, not max — multiple overlapping contours compound.
43
- # Returns 0.0 when there's no overlap.
43
+ # Returns 0.0 when there's no overlap. Inverse contours
44
+ # reduce by the frame width minus their own width (text may
45
+ # only flow inside).
44
46
  def overlap_width(line_y, line_height, frame_x, frame_right)
45
47
  @contours.sum do |contour|
46
- if contour.is_a?(WrapContour::Shape)
47
- WrapContour.overlap_width(contour, line_y, line_height,
48
- frame_x, frame_right)
49
- else
50
- line_overlap(contour, line_y, line_height,
51
- frame_x, frame_right)[:width]
52
- end
48
+ width = if contour.is_a?(WrapContour::Shape)
49
+ WrapContour.overlap_width(contour, line_y,
50
+ line_height, frame_x,
51
+ frame_right)
52
+ else
53
+ line_overlap(contour, line_y, line_height,
54
+ frame_x, frame_right)[:width]
55
+ end
56
+ next width unless contour.inverse
57
+
58
+ (frame_right - frame_x) - width
53
59
  end
54
60
  end
55
61
 
@@ -12,8 +12,10 @@ module Idml
12
12
  FLATTEN_STEPS = 8
13
13
 
14
14
  # A wrap shape: flattened polygons (one per subpath) plus the
15
- # TextWrapOffset bounds.
16
- Shape = Struct.new(:polygons, :offset, keyword_init: true)
15
+ # TextWrapOffset bounds. `inverse` flips the wrap region:
16
+ # text may only flow INSIDE the shape.
17
+ Shape = Struct.new(:polygons, :offset, :inverse,
18
+ keyword_init: true)
17
19
 
18
20
  # Builds the wrap shape for an item's PathGeometry and wrap
19
21
  # preference. Returns nil when the item has no geometry.
@@ -21,7 +23,8 @@ module Idml
21
23
  polygons = polygons_for(item, page_height)
22
24
  return nil if polygons.empty?
23
25
 
24
- Shape.new(polygons: polygons, offset: wrap_offset(pref))
26
+ Shape.new(polygons: polygons, offset: wrap_offset(pref),
27
+ inverse: pref.inverse)
25
28
  end
26
29
 
27
30
  def self.polygons_for(item, page_height)
@@ -118,6 +121,7 @@ module Idml
118
121
 
119
122
  # Total horizontal overlap of the text line's band with the
120
123
  # shape, expanded by the offsets and clipped to the frame.
124
+ # The caller (TextWrapResolver) owns inverse flipping.
121
125
  def self.overlap_width(shape, line_y, line_height, frame_x,
122
126
  frame_right)
123
127
  mid_y = line_y + (line_height / 2.0)
@@ -39,6 +39,41 @@ module Idml
39
39
  (0x61..0x7A).cover?(codepoint)
40
40
  end
41
41
 
42
+ # Mojikumi subset 2: line-end punctuation compression —
43
+ # full-width closing punctuation occupying a line's final
44
+ # position renders at half advance (行末約物半角詰め),
45
+ # tightening the line as InDesign's default mojikumi does.
46
+ TRAILING_COMPRESSION_CODEPOINTS = [
47
+ 0x3001, # 、 ideographic comma
48
+ 0x3002, # 。 ideographic full stop
49
+ 0x3009, # 〉
50
+ 0x300B, # 》
51
+ 0x300D, # 」
52
+ 0x300F, # 』
53
+ 0x3011, # 】
54
+ 0x3015, # 〕
55
+ 0xFF09, # )fullwidth right paren
56
+ 0xFF3D, # ]
57
+ 0xFF5D, # }
58
+ 0x30FC, # ー prolonged sound mark
59
+ ].freeze
60
+
61
+ def apply_line_end_compression(lines)
62
+ lines.each do |line|
63
+ last = line.glyphs.last
64
+ next unless last
65
+ next unless compressible?(last.codepoint)
66
+
67
+ last.width /= 2.0
68
+ line.width = line.glyphs.sum(&:width)
69
+ end
70
+ lines
71
+ end
72
+
73
+ def compressible?(codepoint)
74
+ TRAILING_COMPRESSION_CODEPOINTS.include?(codepoint)
75
+ end
76
+
42
77
  CJK_RANGES = [
43
78
  0x3000..0x303F, # CJK Symbols and Punctuation
44
79
  0x3040..0x309F, # Hiragana
@@ -14,7 +14,8 @@ module Idml
14
14
  lines = new(frame_width).break(glyphs)
15
15
  return lines unless cjk_run?(glyphs)
16
16
 
17
- CjkLayout.apply_kinsoku(lines)
17
+ lines = CjkLayout.apply_kinsoku(lines)
18
+ CjkLayout.apply_line_end_compression(lines)
18
19
  end
19
20
 
20
21
  def self.cjk_run?(glyphs)
data/lib/idml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Idml
4
- VERSION = "0.10.1"
4
+ VERSION = "0.10.2"
5
5
  end
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.1
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -190,6 +190,8 @@ files:
190
190
  - TODO.pdf/129-justification-glyph-scaling.md
191
191
  - TODO.pdf/13-cjk-text-layout.md
192
192
  - TODO.pdf/130-contour-text-wrap.md
193
+ - TODO.pdf/131-inverse-wrap.md
194
+ - TODO.pdf/132-line-end-compression.md
193
195
  - TODO.pdf/14-page-item-element-models.md
194
196
  - TODO.pdf/15-wire-spread-children.md
195
197
  - TODO.pdf/16-typed-model-pipeline.md