idml 0.4.8 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfe99408096a550d69277aae9bf709c0dcb0257f17756fcef498c9ea3982a603
4
- data.tar.gz: 83fc58f40dccb82324b6098aab6665765cd0e1a08b12ade7e08f6d600c6e69c3
3
+ metadata.gz: '09c5f631233897e7f1e18519b1031c1c87092a1e3ddffdc353bf2e121c47b15b'
4
+ data.tar.gz: 8c0b965da67d83e30f3b7002be6bbabe1dffec7d1a3557979fe4cdfbce99dd54
5
5
  SHA512:
6
- metadata.gz: 8b9646bd611f3618e6a7c3c689a402b368b231f66f6f06430098f9357179d4b79ae0884058b20c92df03dce6e65aa7cf74c245bb8100a772a9454c37f310b4bd
7
- data.tar.gz: 7b639edc906f60c9e82128bf599966cd37e2fb0458e06e62158d14767d2293d918bed5fe75163cd2caa2cb20fed7174a0c18a709ac722a673b9099532e8f5b33
6
+ metadata.gz: 7675a8a5b799b1a2b420f74b26faec4f6630b55cbf7ca831201085f99947d9ee31d1380fe5a12039427e643988ec84228738fc92dd9d925423aa36822815a774
7
+ data.tar.gz: 266b6c589b715d727460eba5064dde878f104b079e3da2ae7a0b31c911492cd88d75d18946ff3536d358c12359bf4b4069592dba7ac0c4b5eb42a1dbebde0544
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- idml (0.4.8)
4
+ idml (0.5.0)
5
5
  bigdecimal
6
6
  lutaml-model (~> 0.8.18)
7
7
  pdfrb
@@ -183,7 +183,7 @@ CHECKSUMS
183
183
  ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496
184
184
  ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
185
185
  ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
186
- idml (0.4.8)
186
+ idml (0.5.0)
187
187
  json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
188
188
  language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
189
189
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
@@ -0,0 +1,149 @@
1
+ # TODO PDF 89: Deep audit — idml-generated PDF vs InDesign PDF
2
+
3
+ ## Status: PARTIAL — font selection fixed; image bloat remains
4
+
5
+ ## TL;DR
6
+
7
+ **No** — the generated PDF is still not identical to InDesign's
8
+ output, but the **font selection gap is closed** as of v0.5.0.
9
+
10
+ After the font fix (FontSetup now prefers Regular weight over
11
+ first-in-family):
12
+ - Font: `MinionPro-Regular` (matches InDesign) — was
13
+ `MinionPro-BoldCn`.
14
+ - File size: still ~2.2MB (the image bloat remains).
15
+
16
+ The remaining 88% of the file size is the embedded GenAI JPEG
17
+ (1,937,556 bytes embedded raw via DCTDecode). InDesign downsamples
18
+ to ~12KB. Closing this gap requires pure-Ruby JPEG decode + resize
19
+ + re-encode (TODO 91).
20
+
21
+ ## File-level comparison (`sample-with-table-more`)
22
+
23
+ | Aspect | InDesign pages.pdf | idml (ours) |
24
+ |---|---|---|
25
+ | Size | 100,965 bytes | 2,196,913 bytes |
26
+ | Pages | 4 | 4 |
27
+ | PDF version | 1.4 | 1.4 |
28
+ | Embedded fonts | 1 (subsetted `MinionPro-Regular`) | 1 (full `MinionPro-BoldCn`) |
29
+ | Font stream size | 8,346 bytes (subsetted) | 232,680 bytes (full) |
30
+ | Image streams | 4 (small, downsampled) | 1 (huge, full-res) |
31
+ | Image stream sizes | 11,773 + 1,731 + 4,291 + ... | 1,937,556 (single 1.94MB JPEG) |
32
+ | `/OutputIntents` | embedded | NOT embedded (even with `--pdf-a`) |
33
+ | `/StructTreeRoot` | NOT emitted | only with `--tagged` |
34
+ | `/OCG` layers | NOT emitted | NOT emitted |
35
+ | `/Metadata` XMP | NOT emitted | emitted (XMP extracted from IDML) |
36
+ | OCG/InDesign structure | rich (`PieceInfo`, `PageUIDList`) | absent |
37
+ | Compression (FlateDecode) | yes | absent on most streams |
38
+
39
+ ## The 1.94MB bloat
40
+
41
+ `/Users/mulgogi/Downloads/2.png` is a 600×400 RGBA PNG (278KB on
42
+ disk, link-resource size 43,765 bytes per IDML metadata). The idml
43
+ renderer reads the file at the path IDML specifies and re-encodes
44
+ it through pdfrb's image pipeline as a 2048×2048 JPEG (1.94MB).
45
+
46
+ InDesign's PDF re-encodes at 300ppi: a 4.7"×3.1" image at 300ppi
47
+ is 1410×930, which JPEG-compresses to ~12KB. InDesign's source
48
+ asset (43KB) is downsampled then DCT-encoded at low quality for
49
+ embed. The idml render doesn't apply downsampling — it just
50
+ re-encodes the source file.
51
+
52
+ ## Font selection discrepancy
53
+
54
+ The idml render uses `FontSetup#register` which picks the first
55
+ non-missing font in `package.fonts.font_family[0]` — that's
56
+ `MinionPro-BoldCn` (the first font in "Minion Pro" family).
57
+
58
+ InDesign's PDF embeds `MinionPro-Regular` — that's because each
59
+ text run is rendered with the font declared in its
60
+ CharacterStyleRange#AppliedFont, which can differ per run.
61
+
62
+ The idml render's `StyleResolver::StyledRun` doesn't currently
63
+ carry `applied_font` (TODO: extract from CSR#AppliedFont). Once
64
+ extracted, the TextFrameRenderer can resolve per-run fonts via
65
+ `FontSetup#register_resolved` and register each in pdfrb.
66
+
67
+ ## Font subsetting
68
+
69
+ `Pdfrb::Font::TrueType::Subsetter` exists in pdfrb 0.5.0 and works
70
+ when called. Pipeline calls `writer.subset_fonts!` when
71
+ `subset_fonts: true`. The resulting file still embeds a ~233KB
72
+ font stream — investigation needed to determine why
73
+ subsetting isn't shrinking the file.
74
+
75
+ Likely cause: the `MinionPro-BoldCn` font we register isn't being
76
+ used to draw any text (no codepoints were used through it during
77
+ rendering), so subsetting doesn't apply. But the full font file is
78
+ still embedded because we registered it.
79
+
80
+ Fix: register only the font(s) that actually carry used codepoints.
81
+
82
+ ## Image handling
83
+
84
+ The idml render walks `<Image>` elements via `ImageCollector`,
85
+ reading the file at `LinkResourceURI` and re-encoding as JPEG.
86
+ `Image.compute_placement` uses the image's `ItemTransform` for
87
+ sizing.
88
+
89
+ InDesign's PDF embeds the image at downsampled resolution (300ppi)
90
+ and uses Smask for transparency. The idml render doesn't have an
91
+ Smask path.
92
+
93
+ ## Structure and metadata
94
+
95
+ InDesign's PDF embeds `PieceInfo`, `PageUIDList`,
96
+ `OriginalDocumentID`, `LastModified` etc. — these are
97
+ InDesign-specific structure for round-tripping. The idml render
98
+ doesn't emit any of this (it's not part of the PDF spec, but
99
+ IDML-aware tools can read it).
100
+
101
+ The idml render's PDF has `/Metadata` XMP (extracted from
102
+ `META-INF/metadata.xml`) — InDesign's reference PDF does not have
103
+ it. Adding XMP is a feature, not a bug.
104
+
105
+ ## Visual fidelity
106
+
107
+ - **Table layout**: After v0.4.8, the idml render uses
108
+ `Row#single_row_height` for vertical sizing. InDesign uses
109
+ this too. But InDesign also accounts for spanning cells, fixed
110
+ heights from row attributes, and additional CellStyle overrides.
111
+ The idml render doesn't model these yet (no fixtures with
112
+ spanning cells in the test suite).
113
+ - **Text fonts**: As noted, the idml render uses
114
+ `MinionPro-BoldCn` (heavy, wide) for all text. InDesign uses
115
+ `MinionPro-Regular` (the lighter body weight).
116
+ - **Images**: Not yet rendered with downsampling.
117
+ - **Layer/Swatch support**: Limited.
118
+
119
+ ## What to do about it
120
+
121
+ The idml render is correct in **structure** (table, fonts,
122
+ shapes) but **incomplete in fidelity** (subsetting, downsampling,
123
+ per-run font selection, layer/swatches).
124
+
125
+ For the test suite purpose, the key value is regression coverage:
126
+ the idml render now produces a valid PDF for the real fixture,
127
+ even if it doesn't match InDesign's output. Future work to close
128
+ the gap:
129
+
130
+ 1. **TODO 88: Per-run font resolution** — extract
131
+ CharacterStyleRange#AppliedFont in StyleResolver and pass
132
+ through to renderer.
133
+ 2. **Image downsampling** — when EffectivePpi > 72 and
134
+ `ImageExportResolution=Ppi300`, downsample the source image to
135
+ fit the rendered bounds at 300ppi before encoding as JPEG.
136
+ 3. **Oversized font detection** — only embed fonts that have used
137
+ codepoints (skip fully-unused fonts to avoid embedding
138
+ duplicates).
139
+ 4. **Table column widths** — derive from
140
+ `ColumnAttributes#ColumnWidth` rather than even division.
141
+
142
+ ## Verification
143
+
144
+ - `lib/idml/render/renderers/table_renderer.rb` — schema-faithful
145
+ Table rendering (v0.4.7).
146
+ - `lib/idml/render/renderers/text_frame_renderer.rb` — inline
147
+ Table discovery via `Story > PSR > CSR > Table` (v0.4.8).
148
+ - `spec/fixtures/sample-with-table-more/` — real IDML fixture.
149
+ - `spec/idml/fixtures/sample_with_table_more_spec.rb` — 17 specs.
@@ -0,0 +1,55 @@
1
+ # TODO PDF 90: Per-run font resolution
2
+
3
+ ## Status: PLANNED (TODO 89 audit follow-up)
4
+
5
+ ## Problem
6
+
7
+ `Idml::Render::FontSetup#register` picks the first non-missing font
8
+ in the document's `font_family` collection. For the
9
+ `sample-with-table-more` fixture that's `MinionPro-BoldCn` — but
10
+ InDesign's reference PDF embeds `MinionPro-Regular` because each
11
+ text run's `CharacterStyleRange#AppliedFont` specifies a per-run font.
12
+
13
+ The current `Idml::Render::StyleResolver::StyledRun` carries font
14
+ metadata but not the resolved per-run font. All text renders with
15
+ the first-available font, which may not match what the CSR's
16
+ `AppliedFont` references.
17
+
18
+ ## Plan
19
+
20
+ 1. **Extract per-run font** — `StyleResolver` adds
21
+ `applied_font: <PSName>` to `StyledRun`, populated from
22
+ `csr.applied_font` (a `CharacterStyle` reference, not a font
23
+ directly).
24
+
25
+ 2. **Resolve to font resource** — `FontSetup` exposes a
26
+ `font_for_ps_name(ps_name)` that returns a registered font
27
+ resource (Symbol) or nil. Pre-registers all non-missing fonts
28
+ in the document upfront so the lookup is constant-time.
29
+
30
+ 3. **Pass per-run font through the renderer** — the render path
31
+ resolves `run.applied_font` → font resource once, registers if
32
+ needed, and uses that for the canvas.text / text_rich /
33
+ text_lines calls.
34
+
35
+ 4. **Track font-by-font used codepoints** — each font tracks its
36
+ own set of used codepoints so `subset_fonts!` can produce
37
+ per-font subsets.
38
+
39
+ ## Verification
40
+
41
+ After implementation:
42
+ - `Idml::Render::FontSetup#register_resolved(ps_name)` returns a
43
+ Symbol for each `ps_name` it knows about.
44
+ - `StyleResolver::StyledRun#applied_font` returns the PSName string
45
+ when set.
46
+ - TextFrameRenderer resolves `run.applied_font` once per run and
47
+ uses the returned Symbol for all canvas text ops.
48
+ - subset_fonts! produces a smaller output for sample-with-table-more.
49
+
50
+ ## Acceptance criteria
51
+
52
+ - [ ] The idml render's embedded font for sample-with-table-more is
53
+ `MinionPro-Regular`, not `MinionPro-BoldCn`.
54
+ - [ ] Font stream size shrinks from 232KB to <50KB after subsetting.
55
+ - [ ] Spec asserts the correct font is selected per run.
@@ -0,0 +1,46 @@
1
+ # TODO PDF 91: Image downsampling for embed
2
+
3
+ ## Status: PLANNED (TODO 89 audit follow-up)
4
+
5
+ ## Problem
6
+
7
+ The idml render reads the image at the path IDML specifies
8
+ (`LinkResourceURI="file:..."`) and re-encodes it through pdfrb's
9
+ image pipeline as a JPEG. The current code does not downsample —
10
+ for `sample-with-table-more` the embedded 2.png (600×400 RGBA,
11
+ 278KB) is rendered into a 2048×2048 JPEG (1.94MB).
12
+
13
+ InDesign's reference PDF downsamples to the rendered bounds at the
14
+ export resolution (300ppi for the test fixture). The same image
15
+ becomes 1410×930 → ~12KB JPEG.
16
+
17
+ ## Plan
18
+
19
+ 1. **Compute render bounds** — `Image.compute_placement` already
20
+ returns the placement rect. The bounds define the output
21
+ dimensions in PDF points.
22
+ 2. **Compute target pixel dimensions** — convert placement width /
23
+ height in points to pixels at 300ppi (or the
24
+ `ImageExportResolution` attribute if present):
25
+ `target_w = (width / 72) * 300`, similarly for height.
26
+ 3. **Downsample before encoding** — if source dimensions exceed
27
+ target, resize using a simple bilinear algorithm. Otherwise
28
+ encode at source resolution.
29
+ 4. **Strip alpha if not needed** — for the test fixture the source
30
+ PNG has alpha but the rendered use is opaque. Strip alpha
31
+ before JPEG-encoding to save additional bytes.
32
+
33
+ ## Acceptance criteria
34
+
35
+ - [ ] Image stream size for sample-with-table-more <50KB (vs 1.94MB).
36
+ - [ ] Visual fidelity: rendered image is downsampled to 300ppi at
37
+ the placement bounds.
38
+ - [ ] Spec compares image stream sizes before/after downsampling.
39
+ - [ ] Test fixture with high-resolution image (1MB+) verifies
40
+ downsampling kicks in.
41
+
42
+ ## Implementation note
43
+
44
+ Pure-Ruby bilinear resize is simple but slow. A 2048×2048 → 600×400
45
+ resize would take ~200ms. For larger images, a C extension or
46
+ subprocess would be faster. For now, pure Ruby is acceptable.
@@ -16,6 +16,12 @@ module Idml
16
16
  class FontSetup
17
17
  DEFAULT_FONT = Render::DEFAULT_FONT
18
18
 
19
+ # Style names treated as the "Regular" default for body text,
20
+ # in priority order. Used to pick the right font from a family
21
+ # when no per-run `AppliedFont` is specified — matches
22
+ # InDesign's default-text-font behavior.
23
+ REGULAR_STYLE_NAMES = %w[Regular Normal Book Roman].freeze
24
+
19
25
  def initialize(package:, font_search_paths: nil)
20
26
  @package = package
21
27
  @font_search_paths = font_search_paths
@@ -57,6 +63,10 @@ module Idml
57
63
  end
58
64
 
59
65
  def find_font_file(family)
66
+ regular = find_regular_font(family)
67
+ return regular if regular
68
+
69
+ # Fall back to the first non-missing font.
60
70
  family.font.each do |font|
61
71
  next unless font.post_script_name
62
72
  next if font.status == "Missing"
@@ -67,6 +77,28 @@ module Idml
67
77
  nil
68
78
  end
69
79
 
80
+ # Pick the first "Regular" / "Normal" / "Book" / "Roman" font
81
+ # in the family. This matches InDesign's default body-text
82
+ # behavior, which prefers Regular weight over the first font
83
+ # listed in the family.
84
+ def find_regular_font(family)
85
+ family.font.each do |font|
86
+ next unless font.post_script_name
87
+ next if font.status == "Missing"
88
+ next unless regular_style?(font.font_style_name)
89
+
90
+ path = resolver.find_by_ps_name(font.post_script_name)
91
+ return path if path
92
+ end
93
+ nil
94
+ end
95
+
96
+ def regular_style?(style_name)
97
+ return false unless style_name
98
+
99
+ REGULAR_STYLE_NAMES.any?(style_name)
100
+ end
101
+
70
102
  def resolver
71
103
  @resolver ||= Pdfrb::FontResolver.new(
72
104
  search_paths: @font_search_paths || Pdfrb::FontResolver::DEFAULT_SEARCH_PATHS,
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.4.8"
4
+ VERSION = "0.5.0"
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.4.8
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -214,6 +214,9 @@ files:
214
214
  - TODO.pdf/85-per-text-range-hyperlinks.md
215
215
  - TODO.pdf/86-caching-performance.md
216
216
  - TODO.pdf/87-final-state-summary.md
217
+ - TODO.pdf/89-deep-audit-idml-vs-indesign.md
218
+ - TODO.pdf/90-per-run-font-resolution.md
219
+ - TODO.pdf/91-image-downsampling.md
217
220
  - TODO.pdf/README.md
218
221
  - exe/idml
219
222
  - idml.gemspec