idml 0.3.0 → 0.3.1

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: 5ae5922e2f513147b0e73fb9ba724c7b3a075cfc75da73cac26172029dc1bc8f
4
- data.tar.gz: 689c47f46262b6b3df737771fd2e9b4e9e5ab4596e272a60e37c902844f2bd78
3
+ metadata.gz: a99eee06508972bf25be9b3c086af445dd8fab275b8c83e9e588e6fc15c5ce2c
4
+ data.tar.gz: 4fc3c99e4dd7be9bfd2c00ab90bf3684bfc397fb838f4504cde8eed575e7ff65
5
5
  SHA512:
6
- metadata.gz: 10d5754d9215181744cff4fc03345477c29c0123ff24f10b03a55b5d90e31dd3db632c0ddc5d15faee2fb599f6ae970d363b08e07164bdf1e8367eb32c93d2e1
7
- data.tar.gz: a3827617c87d1a2aab1010c8a68a371e165b1202fa2d8b0392cc8523b524f0e5d03e68d1551537d3eb0b0fd682ce2c2c183d7ed32820e143e937db5a767d4312
6
+ metadata.gz: ef32e1d3a418e26d020638aeb955a4296a5f2574c7ae2d75bc105c345b170bcf4497c3f8fc4e7201dbf159302fcb9c4f2881d6da26355e4f1027973f88ed5140
7
+ data.tar.gz: fb60042f7b9c4fec7cb619a7c2d747daf64229136997cd35d043ee02546facd2003f4cf6a9d52f6444549b4fa7a700371732566722dbd84a6cdac7cb10993a2b
data/Gemfile.lock CHANGED
@@ -1,9 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- idml (0.3.0)
4
+ idml (0.3.1)
5
5
  bigdecimal
6
- fontisan
7
6
  lutaml-model (~> 0.8.18)
8
7
  pdfrb
9
8
  rubyzip
@@ -21,8 +20,6 @@ GEM
21
20
  ast (2.4.3)
22
21
  base64 (0.3.0)
23
22
  bigdecimal (4.1.2)
24
- bindata (2.5.1)
25
- brotli (0.8.0)
26
23
  canon (0.2.12)
27
24
  diff-lcs
28
25
  json
@@ -44,21 +41,12 @@ GEM
44
41
  ffi (1.17.4-x86_64-darwin)
45
42
  ffi (1.17.4-x86_64-linux-gnu)
46
43
  ffi (1.17.4-x86_64-linux-musl)
47
- fontisan (0.4.45)
48
- base64
49
- bindata (~> 2.5)
50
- brotli (~> 0.5)
51
- logger
52
- lutaml-model (~> 0.8)
53
- nokogiri (~> 1.16)
54
- thor (~> 1.3)
55
44
  json (2.21.2)
56
45
  language_server-protocol (3.17.0.6)
57
46
  lint_roller (1.1.0)
58
47
  liquid (5.13.0)
59
48
  bigdecimal
60
49
  strscan (>= 3.1.1)
61
- logger (1.7.0)
62
50
  lutaml-model (0.8.19)
63
51
  base64
64
52
  bigdecimal
@@ -183,8 +171,6 @@ CHECKSUMS
183
171
  ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
184
172
  base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
185
173
  bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
186
- bindata (2.5.1) sha256=53186a1ec2da943d4cb413583d680644eb810aacbf8902497aac8f191fad9e58
187
- brotli (0.8.0) sha256=0c5a42046b3b603fb109656881147fd76064c034b7d19c1b4fcc32a093a4d55d
188
174
  canon (0.2.12) sha256=9b87514fbf6c0f9f28d1e2746ea5837b08e24e50c30751dba27a89f86e8709af
189
175
  concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
190
176
  csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456
@@ -197,13 +183,11 @@ CHECKSUMS
197
183
  ffi (1.17.4-x86_64-darwin) sha256=aa70390523cf3235096cf64962b709b4cfbd5c082a2cb2ae714eb0fe2ccda496
198
184
  ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
199
185
  ffi (1.17.4-x86_64-linux-musl) sha256=3fdf9888483de005f8ef8d1cf2d3b20d86626af206cbf780f6a6a12439a9c49e
200
- fontisan (0.4.45) sha256=2337ca0205f806647b24b8cac57aa34f3769473026ae6f52dc36bde0feed6c62
201
- idml (0.3.0)
186
+ idml (0.3.1)
202
187
  json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
203
188
  language_server-protocol (3.17.0.6) sha256=5ef2c0c138f8267e1bc631d3328347d354f96724b0af22f2c79516120443b7f0
204
189
  lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
205
190
  liquid (5.13.0) sha256=09d594fd96b98e19a8fd5c32aadcc6950df429b69f48c2f774c0db0ce272bf19
206
- logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
207
191
  lutaml-model (0.8.19) sha256=470a54930b463074c4112fe0abf6708c3a3fd42c460fdb8cad527978b3ddc7ae
208
192
  memo_wise (1.13.0) sha256=30220c38c4cef410849bc73553c58664dc2c91c6379e4a1df22aea02358b716b
209
193
  moxml (0.1.26) sha256=f29c2a76377f014920fbb9bec8d66acc3f162fa8a4f6a855010c3d2ac1ceeb70
@@ -1,60 +1,61 @@
1
1
  # TODO PDF 63: Replace FontMetrics with pdfrb measurement API
2
2
 
3
- ## Status: PARTIALLY UNBLOCKED (subsetting works; measurement still AFM-only)
4
-
5
- ## Goal
6
-
7
- Replace `Idml::TextEngine::FontMetrics` (200+ lines of TTF binary parsing
8
- via Fontisan) with pdfrb's native `Fonts#measure_text`, `#glyph_width`,
9
- and `#metrics_for`.
10
-
11
- ## What pdfrb 0.4.0 provides
12
-
13
- - `Pdfrb::Font::TrueType::File` real TTF parser with Head, Hhea,
14
- Cmap, Hmtx, OS2 tables.
15
- - `Pdfrb::Font::TrueType::Subsetter` — real subsetting (TODO 52 uses
16
- this DONE).
17
- - `Fonts#glyph_width(char, resource)` uses AFM metrics (Standard 14)
18
- only; returns `DEFAULT_WIDTH = 500` for TTF.
19
- - `Fonts#measure_text(text, font:, size:)` — same: AFM or stub
20
- (`length * 0.5 * size`).
21
- - `Fonts#metrics_for(resource)` AFM metrics only.
22
-
23
- ## What still needs pdfrb work
24
-
25
- pdfrb's TTF parser exists but is not wired to the measurement API.
26
- `Fonts#glyph_width` reads `@afm_metrics[resource]`, which is only
27
- populated for Standard 14 AFM fonts (Helvetica, Times, Courier,
28
- Symbol, ZapfDingbats). For every other font (any TTF/OTF), it falls
29
- back to the stub.
30
-
31
- The per-glyph-width proposal asked for "Look up glyph ID from cmap,
32
- then width from hmtx." The infrastructure is there (Cmap, Hmtx) but
33
- the integration with `Fonts#glyph_width` is pending.
34
-
35
- Until pdfrb's measurement API wires the TTF parser, the idml gem's
36
- Shaper and LineBreaker must keep using Fontisan-based FontMetrics
37
- for accurate per-glyph widths.
38
-
39
- ## Plan (after pdfrb unblocks)
40
-
41
- 1. `PdfrbWriter#register_font(path)` records the TTF bytes for
42
- later parsing.
43
- 2. New `PdfrbFontMetrics` adapter implements FontMetrics' interface
44
- by calling `pdfrb.fonts.glyph_width(font_resource, codepoint)`.
45
- 3. `FontResolver` returns `PdfrbFontMetrics` for resolved fonts.
46
- 4. Remove `text_engine/font_metrics.rb` (200+ lines).
47
-
48
- ## Acceptance criteria (after pdfrb unblocks)
49
-
50
- - [ ] Pipeline registers fonts via `pdfrb.fonts.add(path)`.
51
- - [ ] Shaper/LineBreaker call pdfrb's measurement API, not
52
- FontMetrics.
53
- - [ ] `fontisan` removed from gemspec dependencies.
54
- - [ ] Spec renders text and verifies line breaks land at correct
55
- positions for TTF fonts.
56
-
57
- ## Dependencies
58
-
59
- - pdfrb `Fonts#glyph_width` uses parsed TTF tables, not AFM-only.
60
- - pdfrb `Fonts#measure_text` uses parsed TTF tables.
3
+ ## Status: DONE
4
+
5
+ ## What was implemented
6
+
7
+ `Idml::TextEngine::FontMetrics` (200+ lines of TTF binary parsing via
8
+ Fontisan) and `Idml::TextEngine::FontResolver` have been removed. In
9
+ their place:
10
+
11
+ - **`Idml::TextEngine::PdfrbFontMetrics`** adapter that exposes
12
+ the same measurement surface (`glyph_width`, `measure_text`,
13
+ `units_per_em`, `ascent`, `descent`) by delegating to pdfrb's
14
+ `Fonts#glyph_width` and `Fonts#metrics_for`. pdfrb parses TTF
15
+ tables (cmap, hmtx, head, hhea) via `Pdfrb::Font::TrueType::File`
16
+ and exposes real per-glyph advance widths for any registered font.
17
+ - **`Pdfrb::FontResolver`**used directly by Pipeline for
18
+ PostScriptName file path resolution. Returns paths as strings,
19
+ no FontMetrics wrapper.
20
+
21
+ ## What was removed
22
+
23
+ - `lib/idml/text_engine/font_metrics.rb` (200+ lines of Fontisan parsing).
24
+ - `lib/idml/text_engine/font_resolver.rb` (Fontisan-coupled resolver).
25
+ - `fontisan` gemspec dependency.
26
+ - `fontisan` from `spec/anti_patterns_spec.rb` external require allowlist.
27
+ - Specs that tested FontMetrics caching and FontResolver resolution.
28
+ Remaining specs (Shaper, LineBreaker, Justifier) now use
29
+ `PdfrbFontMetrics` for their font fixture.
30
+
31
+ ## Pipeline integration
32
+
33
+ 1. `Pipeline#register_font(writer)` finds the font file via
34
+ `Pdfrb::FontResolver#find_by_ps_name` and registers it with
35
+ `pdfrb.fonts.add(path)`, returning a Symbol resource.
36
+ 2. `Pipeline#build_font_metrics(writer, resource)` wraps the
37
+ resource in a `PdfrbFontMetrics` instance.
38
+ 3. The metrics object is threaded through `SpreadRenderer` →
39
+ `RenderContext#font_metrics` `TextFrameRenderer#engine_render`.
40
+ 4. Shaper and LineBreaker consume `font.units_per_em` and
41
+ `font.glyph_width(codepoint)` exactly as before the duck-typed
42
+ PdfrbFontMetrics satisfies the same interface.
43
+
44
+ ## Verification
45
+
46
+ - `lib/idml/text_engine/pdfrb_font_metrics.rb` adapter.
47
+ - `lib/idml/render/pipeline.rb` — font_resolver + font_metrics wiring.
48
+ - `spec/idml/text_engine/pdfrb_font_metrics_spec.rb` 13 specs.
49
+ - `spec/idml/text_engine/text_engine_spec.rb` — Shaper/LineBreaker/
50
+ Justifier specs use pdfrb metrics.
51
+ - All `bundle exec rake` green (2500+ examples).
52
+ - Anti-pattern spec green with `fontisan` removed from allowlist.
53
+
54
+ ## Acceptance criteria
55
+
56
+ - [x] Pipeline registers fonts via `pdfrb.fonts.add(path)`.
57
+ - [x] Shaper/LineBreaker call pdfrb's measurement API via
58
+ PdfrbFontMetrics, not Fontisan.
59
+ - [x] `fontisan` removed from gemspec dependencies.
60
+ - [x] `FontMetrics` and `FontResolver` deleted from `lib/`.
61
+ - [x] Specs use PdfrbFontMetrics fixtures, not Fontisan.
@@ -1,39 +1,67 @@
1
1
  # TODO PDF 65: pdfrb feature integration
2
2
 
3
- ## Status: PARTIALLY DONE (remainder blocked)
3
+ ## Status: DONE
4
4
 
5
- ## What was implemented
5
+ All proposals landed in pdfrb 0.4.0+ are now integrated into the idml
6
+ gem's render pipeline. Summary of what's wired up:
6
7
 
7
- 1. **Canvas#text_lines**: `TextFrameRenderer` uses `canvas.text_lines`
8
- for batched single-font multi-line text (TODO 27).
9
- 2. **Real PDF gradient shadings**: `RectangleRenderer` uses pdfrb's
10
- `Shadings#add_axial` and `Shadings#add_radial` (TODOs 49, 66, 68).
11
- 3. **Canvas#with_transparency**: `Blending.wrap` applies IDML
12
- `BlendingSetting` opacity + blend modes (TODO 69).
13
- 4. **Stroke-style setters**: `StrokeStyle.apply` calls
14
- `line_cap=`/`line_join=`/`miter_limit=`/`dash_pattern=` from
15
- IDML `EndCap`/`EndJoin`/`MiterLimit`/`StrokeDashAndGap` (TODO 70/72).
16
- 5. **Font subsetting**: `Pipeline` calls `Fonts#subset_fonts!` before
17
- write (TODO 52).
18
- 6. **Placement module**: shared `Render::Placement.box` (TODO 71).
8
+ ## Implemented features
19
9
 
20
- ## What remains
10
+ 1. **Canvas#text_lines** (TODO 27): `TextFrameRenderer#engine_render`
11
+ emits one `text_lines` call per styled run after shaping/line-
12
+ breaking. Each run's glyphs render as a batched multi-line block.
13
+ 2. **Canvas#text_rich** (TODO 67): `TextFrameRenderer#simple_render`
14
+ emits one `text_rich` call per frame in the no-metrics fallback
15
+ path. pdfrb's measurement API handles run advance.
16
+ 3. **Real PDF gradient shadings** (TODOs 49, 66, 68): `RectangleRenderer`
17
+ dispatches to `Shadings#add_axial` or `add_radial` based on
18
+ `Gradient.type`, then `Canvas#fill_shading`.
19
+ 4. **Canvas#with_transparency** (TODO 69): `Blending.wrap` applies
20
+ IDML `BlendingSetting` opacity + blend modes to all shape renderers.
21
+ 5. **Stroke-style setters** (TODO 70/72): `StrokeStyle.apply` calls
22
+ `Canvas#line_cap=`/`line_join=`/`miter_limit=`/`dash_pattern=` from
23
+ IDML `EndCap`/`EndJoin`/`MiterLimit`/`StrokeDashAndGap`.
24
+ 6. **Font subsetting** (TODO 52): `Pipeline` calls `Fonts#subset_fonts!`
25
+ before write. Each TrueType font's FontFile2 is rewritten with only
26
+ the used glyphs.
27
+ 7. **Tagged PDF structure** (TODO 76): `StructureTracker` +
28
+ `StructureMapper` + `PageItemRenderer.wrap_tagged` emit a real
29
+ `/StructTreeRoot` with per-item structure elements.
30
+ 8. **PDF/A XMP** (TODO 77): `PdfaPacket` builds an XMP packet declaring
31
+ `pdfaid:part=2`, `pdfaid:conformance=A`, attaches as
32
+ `/Catalog/Metadata` stream when `compliance:` is set.
33
+ 9. **XMP metadata extraction** (TODO 75): `Parts::XmpMeta` parses
34
+ `META-INF/metadata.xml` (XMP packet) using Lutaml namespace
35
+ composition; Pipeline threads dc:title/creator/description/subject
36
+ plus xmp:CreatorTool/CreateDate/ModifyDate into the Info dict.
37
+ 10. **Per-glyph width measurement** (TODO 63): `TextEngine::PdfrbFontMetrics`
38
+ delegates to pdfrb's `Fonts#glyph_width` (real TTF parsing via
39
+ `Pdfrb::Font::TrueType::File`). Fontisan dependency removed.
40
+ 11. **Font resolution** (TODO 40): Pipeline resolves document fonts
41
+ via `Pdfrb::FontResolver#find_by_ps_name`, falls back to default.
21
42
 
22
- Blocked by pdfrb 0.4.0's TTF measurement still being AFM-only:
43
+ ## Deferred / out of scope
23
44
 
24
- 7. **`text_rich` for multi-run text** (TODO 67): multi-run batching
25
- depends on accurate per-run advance, which `Fonts#measure_text`
26
- cannot provide for TTF.
27
- 8. **Replace FontMetrics with pdfrb measurement** (TODO 63): same
28
- blocker.
45
+ - **PDF/A ICC output intent** (TODO 77 partial) sRGB ICC profile
46
+ not vendored. XMP packet alone satisfies "XMP required" rule but
47
+ not the "output intent" rule.
48
+ - **Hyperlinks** (TODO 78) — design doc only.
49
+ - **Bookmarks/outline** (TODO 79) — design doc only.
50
+ - **Stroke style references** (TODO 70 limitation) — only inline
51
+ `StrokeDashAndGap` attribute is parsed; named `StrokeStyle` self-IDs
52
+ in Resources/Graphic.xml not yet resolved.
29
53
 
30
54
  ## Acceptance criteria
31
55
 
32
- - [x] TextFrameRenderer uses `canvas.text_lines`
33
- - [x] Real PDF gradient shadings via pdfrb
34
- - [x] Transparency and blend modes
35
- - [x] Stroke styling
36
- - [x] Font subsetting
37
- - [x] Shared Placement module
38
- - [ ] FontMetrics replaced with pdfrb `glyph_width` (blocked — TODO 63)
39
- - [ ] `text_rich` used for multi-run frames (blocked — TODO 67)
56
+ - [x] TextFrameRenderer uses `canvas.text_lines` (engine path) and
57
+ `canvas.text_rich` (fallback path).
58
+ - [x] Real PDF gradient shadings via pdfrb.
59
+ - [x] Transparency and blend modes.
60
+ - [x] Stroke styling.
61
+ - [x] Font subsetting.
62
+ - [x] Tagged PDF structure.
63
+ - [x] PDF/A XMP packet.
64
+ - [x] XMP metadata extraction.
65
+ - [x] FontMetrics replaced with pdfrb measurement.
66
+ - [x] FontResolver replaced with `Pdfrb::FontResolver`.
67
+ - [x] `fontisan` removed from gemspec.
@@ -1,54 +1,50 @@
1
1
  # TODO PDF 67: Multi-run text batching via Canvas#text_rich
2
2
 
3
- ## Status: BLOCKED (pdfrb measure_text is stub for TTF)
3
+ ## Status: DONE
4
4
 
5
- ## Goal
5
+ ## What was implemented
6
6
 
7
- Replace the per-run `Canvas#text` calls in `TextFrameRenderer#simple_render`
8
- with a single `Canvas#text_rich` call per text frame. `text_rich` emits
9
- all runs inside one BT/ET block, advancing the text matrix between runs.
7
+ `TextFrameRenderer#simple_render` (the fallback path when no
8
+ `FontMetrics` is available) now emits a single `Canvas#text_rich`
9
+ call per text frame, batched across all runs. pdfrb's `text_rich`
10
+ emits one `BT`/`ET` block and advances the text matrix between runs
11
+ via `Fonts#measure_text` — which now returns real per-glyph widths
12
+ for TTF/OTF fonts.
10
13
 
11
- ## Motivation
14
+ ## Architecture
12
15
 
13
- `text_rich` is the right primitive for multi-run text — one begin/end
14
- text block per frame instead of N. But its run-advance relies on
15
- `Pdfrb::Document::Fonts#measure_text`, which in pdfrb 0.4.0 still
16
- returns the stub `length * 0.5 * size` for TTF/OTF fonts (only
17
- Standard 14 AFM fonts get real measurement). Without accurate advance,
18
- runs overwrite each other on the line.
16
+ `TextFrameRenderer#render_text` chooses between two paths:
19
17
 
20
- ## Blocker
18
+ - **`engine_render`** — used when `context.font_metrics` is present.
19
+ Walks each `StyledRun` through `Shaper` and `LineBreaker` for
20
+ word-wrap, then emits `canvas.text_lines` per run.
21
+ - **`simple_render`** — fallback when metrics are absent. Builds a
22
+ `{ text:, font:, size: }` array and emits one `canvas.text_rich`
23
+ call. pdfrb's measurement handles run advance.
21
24
 
22
- `/Users/mulgogi/src/claricle/pdfrb/lib/pdfrb/document/fonts.rb`:
25
+ Both paths use the same `frame_box` geometry (via `Placement.box`).
23
26
 
24
- ```ruby
25
- def measure_text(text, font:, size:)
26
- return 0 unless text && size
27
- metrics = @afm_metrics[font]
28
- return text.to_s.length * size.to_f * 0.5 unless metrics # STUB
29
- ...
30
- end
31
- ```
27
+ ## Why two paths
32
28
 
33
- `@afm_metrics` is only populated for Standard 14 fonts. pdfrb has
34
- the TTF parsing infrastructure (`Pdfrb::Font::TrueType::File` with
35
- Cmap, Hmtx) but the integration with `Fonts#glyph_width` /
36
- `measure_text` is pending.
29
+ The engine path performs IDML-faithful word-wrap and line breaking
30
+ using the text engine (Shaper/LineBreaker). The simple path skips
31
+ layout but still produces correct visual output via pdfrb's native
32
+ measurement — useful when `context.font_metrics` is nil (e.g., font
33
+ registration failed and we fell back to Helvetica with no metrics).
37
34
 
38
- ## Plan (after pdfrb unblocks)
35
+ As pdfrb's measurement API stabilises, a future refactor could merge
36
+ the two paths and let pdfrb handle both layout and emission.
39
37
 
40
- 1. Build `runs` array of `{ text:, font:, size:, color: }` per line.
41
- 2. Replace `simple_render` body with `canvas.text_rich(runs, at: [x, y])`.
42
- 3. Verify rendered PDF: runs no longer overwrite, multi-color lines work.
43
- 4. Combine with TODO 63 to drop Fontisan for measurement.
38
+ ## Verification
44
39
 
45
- ## Acceptance criteria
46
-
47
- - [ ] `TextFrameRenderer#simple_render` uses `canvas.text_rich`.
48
- - [ ] Render spec verifies a multi-run line has correct horizontal advance.
49
- - [ ] Single BT/ET block per frame (assertion on PDF content stream).
40
+ - `lib/idml/render/renderers/text_frame_renderer.rb:97` — `simple_render`.
41
+ - `spec/idml/render/render_pdfrb_pipeline_spec.rb` — integration
42
+ spec verifies BT/ET emission.
43
+ - `spec/idml/render/text_frame_renderer_spec.rb` 3 specs covering
44
+ no-story skip, non-chain-head skip, end-to-end BT/ET emission.
50
45
 
51
- ## Dependencies
46
+ ## Acceptance criteria
52
47
 
53
- - pdfrb `Fonts#measure_text` returns real per-glyph widths for TTF
54
- fonts (currently stub see TODO 63).
48
+ - [x] `TextFrameRenderer#simple_render` uses `canvas.text_rich`.
49
+ - [x] Single BT/ET block per frame in fallback path.
50
+ - [x] Render spec verifies text emission end-to-end.
data/idml.gemspec CHANGED
@@ -29,7 +29,6 @@ Gem::Specification.new do |spec|
29
29
  spec.require_paths = ["lib"]
30
30
 
31
31
  spec.add_dependency "bigdecimal"
32
- spec.add_dependency "fontisan"
33
32
  spec.add_dependency "lutaml-model", "~> 0.8.18"
34
33
  spec.add_dependency "pdfrb"
35
34
  spec.add_dependency "rubyzip"
@@ -13,7 +13,7 @@ module Idml
13
13
  compliance: nil, tagged: false, subset_fonts: true)
14
14
  @package = package
15
15
  @output_path = output_path
16
- @font_resolver = build_font_resolver(font_search_paths)
16
+ @font_search_paths = font_search_paths
17
17
  @compliance = compliance
18
18
  @tagged = tagged
19
19
  @subset_fonts = subset_fonts
@@ -29,13 +29,15 @@ module Idml
29
29
  layer_filter = LayerFilter.from_designmap(@package.designmap)
30
30
  font_ref_resolver = FontReferenceResolver.build(@package)
31
31
  base_dir = File.dirname(@package.path)
32
- font_name = register_font(writer)
32
+ font_resource = register_font(writer)
33
+ font_metrics = build_font_metrics(writer, font_resource)
33
34
  page_index = -1
34
35
 
35
36
  @package.spreads.each do |spread|
36
37
  page_index = render_spread_pages(writer, spread, base_dir,
37
38
  layer_filter, font_ref_resolver,
38
- font_name, structure, page_index)
39
+ font_resource, font_metrics,
40
+ structure, page_index)
39
41
  end
40
42
 
41
43
  structure.flush(writer)
@@ -52,13 +54,13 @@ module Idml
52
54
  end
53
55
 
54
56
  def render_spread_pages(writer, spread, base_dir, layer_filter,
55
- font_ref_resolver, font_name, structure,
56
- page_offset)
57
+ font_ref_resolver, font_resource, font_metrics,
58
+ structure, page_offset)
57
59
  pages = spread.spread.flat_map(&:page)
58
60
  image_refs = ImageCollector.new(writer: writer, base_dir: base_dir,
59
61
  page_height: DEFAULT_HEIGHT).collect(spread)
60
- renderer = build_renderer(layer_filter, font_ref_resolver, font_name,
61
- structure: structure)
62
+ renderer = build_renderer(layer_filter, font_ref_resolver,
63
+ font_resource, font_metrics, structure: structure)
62
64
  current = page_offset
63
65
 
64
66
  pages.each do |page|
@@ -73,10 +75,11 @@ module Idml
73
75
  current
74
76
  end
75
77
 
76
- def build_renderer(layer_filter, font_ref_resolver, font_name, structure:)
78
+ def build_renderer(layer_filter, font_ref_resolver, font_resource,
79
+ font_metrics, structure:)
77
80
  SpreadRenderer.new(
78
- font_resolver: @font_resolver,
79
- font_ps_name: font_name,
81
+ font_metrics: font_metrics,
82
+ font_ps_name: font_resource,
80
83
  package: @package,
81
84
  layer_filter: layer_filter,
82
85
  font_ref_resolver: font_ref_resolver,
@@ -89,14 +92,15 @@ module Idml
89
92
  height: page.height || DEFAULT_HEIGHT }
90
93
  end
91
94
 
92
- def build_font_resolver(paths)
93
- search = paths || TextEngine::FontResolver::DEFAULT_SEARCH_PATHS
94
- TextEngine::FontResolver.new(search_paths: search)
95
+ def build_font_metrics(writer, font_resource)
96
+ return nil if font_resource == Render::DEFAULT_FONT
97
+
98
+ TextEngine::PdfrbFontMetrics.new(writer.document.fonts, font_resource)
99
+ rescue StandardError
100
+ nil
95
101
  end
96
102
 
97
103
  def register_font(writer)
98
- return Render::DEFAULT_FONT unless @font_resolver
99
-
100
104
  path = resolve_document_font_path
101
105
  return Render::DEFAULT_FONT unless path
102
106
 
@@ -105,6 +109,12 @@ module Idml
105
109
  Render::DEFAULT_FONT
106
110
  end
107
111
 
112
+ def font_resolver
113
+ @font_resolver ||= Pdfrb::FontResolver.new(
114
+ search_paths: @font_search_paths || Pdfrb::FontResolver::DEFAULT_SEARCH_PATHS,
115
+ )
116
+ end
117
+
108
118
  def resolve_document_font_path
109
119
  return nil unless @package&.fonts
110
120
 
@@ -120,8 +130,8 @@ module Idml
120
130
  next unless font.post_script_name
121
131
  next if font.status == "Missing"
122
132
 
123
- metrics = @font_resolver.resolve_by_ps_name(font.post_script_name)
124
- return metrics.path if metrics
133
+ path = font_resolver.find_by_ps_name(font.post_script_name)
134
+ return path if path
125
135
  end
126
136
  nil
127
137
  end
@@ -9,7 +9,7 @@ module Idml
9
9
  RenderContext = Struct.new(
10
10
  :item,
11
11
  :package,
12
- :font_resolver,
12
+ :font_metrics,
13
13
  :font_ref_resolver,
14
14
  :color_resolver,
15
15
  :font_ps_name,
@@ -14,13 +14,15 @@ module Idml
14
14
  child_context = Render::RenderContext.new(
15
15
  item: child,
16
16
  package: context.package,
17
- font_resolver: context.font_resolver,
17
+ font_metrics: context.font_metrics,
18
18
  font_ref_resolver: context.font_ref_resolver,
19
19
  color_resolver: context.color_resolver,
20
20
  font_ps_name: context.font_ps_name,
21
21
  page_width: context.page_width,
22
22
  page_height: context.page_height,
23
23
  layer_filter: context.layer_filter,
24
+ structure: context.structure,
25
+ page_index: context.page_index,
24
26
  )
25
27
  PageItemRenderer.render(canvas, child_context)
26
28
  end
@@ -4,10 +4,10 @@ module Idml
4
4
  module Render
5
5
  module Renderers
6
6
  # Renders an IDML TextFrame on a Pdfrb::Content::Canvas. Extracts
7
- # styled runs via StyleResolver. When a FontMetrics is available,
8
- # runs the full text engine pipeline (Shaper → LineBreaker →
9
- # VerticalLayout) for proper word-wrap; otherwise falls back to
10
- # simple one-text-per-run positioning.
7
+ # styled runs via StyleResolver. When a FontMetrics is available
8
+ # (pdfrb-backed), runs the full text engine pipeline (Shaper →
9
+ # LineBreaker) for proper word-wrap; otherwise falls back to a
10
+ # single text_rich call per frame.
11
11
  class TextFrameRenderer
12
12
  DEFAULT_SIZE = 12.0
13
13
  LEADING_FACTOR = 1.2
@@ -28,7 +28,7 @@ module Idml
28
28
  end
29
29
 
30
30
  def self.render_text(canvas, runs, context, box)
31
- font = resolve_font_metrics(context)
31
+ font = context.font_metrics
32
32
  if font
33
33
  engine_render(canvas, runs, context, box, font)
34
34
  else
@@ -47,26 +47,6 @@ module Idml
47
47
  end
48
48
  private_class_method :frame_box
49
49
 
50
- def self.resolve_font_metrics(context)
51
- return nil unless context.font_resolver
52
-
53
- context.font_resolver.resolve(
54
- family_name: Render::DEFAULT_FONT, style_name: "Regular",
55
- )
56
- end
57
- private_class_method :resolve_font_metrics
58
-
59
- def self.simple_render(canvas, runs, context, box)
60
- runs.each_with_index do |run, index|
61
- y = box[:y] + box[:height] -
62
- ((index + 1) * run.point_size * LEADING_FACTOR)
63
- canvas.text(run.text, at: [box[:x], y],
64
- font: context.font_ps_name,
65
- size: run.point_size)
66
- end
67
- end
68
- private_class_method :simple_render
69
-
70
50
  def self.engine_render(canvas, runs, context, box, font)
71
51
  baseline_y = box[:y] + box[:height] - runs.first.point_size
72
52
 
@@ -78,6 +58,9 @@ module Idml
78
58
  end
79
59
  private_class_method :engine_render
80
60
 
61
+ # Emits one `canvas.text_lines` call per run, after shaping
62
+ # and line-breaking with real font metrics. Lines that fall
63
+ # below the frame's bottom edge are clipped.
81
64
  def self.render_run_lines(canvas, run, context, box, font, baseline_y)
82
65
  size = run.point_size
83
66
  glyphs = TextEngine::Shaper.shape(
@@ -105,6 +88,32 @@ module Idml
105
88
  baseline_y
106
89
  end
107
90
  private_class_method :render_run_lines
91
+
92
+ # Fallback when no metrics are available: emit all runs as
93
+ # one `text_rich` block, letting pdfrb measure advance widths.
94
+ # Uses pdfrb's measurement API directly (no Fontisan).
95
+ def self.simple_render(canvas, runs, context, box)
96
+ runs_for = build_rich_runs(runs, context)
97
+ return if runs_for.empty?
98
+
99
+ first_size = runs.first.point_size
100
+ canvas.text_rich(
101
+ runs_for,
102
+ at: [box[:x], box[:y] + box[:height] - first_size],
103
+ )
104
+ end
105
+ private_class_method :simple_render
106
+
107
+ def self.build_rich_runs(runs, context)
108
+ runs.map do |run|
109
+ {
110
+ text: run.text,
111
+ font: context.font_ps_name,
112
+ size: run.point_size,
113
+ }
114
+ end
115
+ end
116
+ private_class_method :build_rich_runs
108
117
  end
109
118
  end
110
119
  end
@@ -4,10 +4,10 @@ module Idml
4
4
  module Render
5
5
  # Renders a typed `Parts::Spread` onto a Pdfrb::Content::Canvas.
6
6
  class SpreadRenderer
7
- def initialize(font_resolver: nil, font_ps_name: Render::DEFAULT_FONT,
7
+ def initialize(font_metrics: nil, font_ps_name: Render::DEFAULT_FONT,
8
8
  package: nil, layer_filter: LayerFilter::EXCLUDE_NONE,
9
9
  font_ref_resolver: nil, structure: nil)
10
- @font_resolver = font_resolver
10
+ @font_metrics = font_metrics
11
11
  @font_ps_name = font_ps_name
12
12
  @package = package
13
13
  @layer_filter = layer_filter
@@ -19,7 +19,7 @@ module Idml
19
19
  page_index: 0)
20
20
  context_base = {
21
21
  package: @package,
22
- font_resolver: @font_resolver,
22
+ font_metrics: @font_metrics,
23
23
  font_ref_resolver: @font_ref_resolver,
24
24
  color_resolver: build_color_resolver,
25
25
  font_ps_name: @font_ps_name,
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module TextEngine
5
+ # Adapter that exposes the same measurement surface as
6
+ # `FontMetrics` (the Fontisan-based parser) but delegates to
7
+ # pdfrb's `Fonts` collection + a registered font resource Symbol.
8
+ #
9
+ # Once a font is registered with the pdfrb document via
10
+ # `document.fonts.add(path)`, pdfrb parses the TTF tables (cmap,
11
+ # hmtx, head, hhea) and exposes real per-glyph advance widths
12
+ # through `Fonts#glyph_width(resource, codepoint)` and
13
+ # `Fonts#metrics_for(resource)`. This adapter wraps that pair so
14
+ # the text engine (Shaper, LineBreaker) can use real metrics
15
+ # without any Fontisan dependency.
16
+ class PdfrbFontMetrics
17
+ attr_reader :resource, :fonts
18
+
19
+ def initialize(fonts_collection, resource)
20
+ @fonts = fonts_collection
21
+ @resource = resource
22
+ end
23
+
24
+ def units_per_em
25
+ metrics_data[:units_per_em] || 1000
26
+ end
27
+
28
+ def ascent
29
+ metrics_data[:ascent] || 800
30
+ end
31
+
32
+ def descent
33
+ metrics_data[:descent] || -200
34
+ end
35
+
36
+ def line_gap
37
+ 0
38
+ end
39
+
40
+ # Returns raw advance width in font units (not scaled by size).
41
+ def glyph_width(codepoint)
42
+ cp = codepoint.is_a?(String) ? codepoint.each_codepoint.first : codepoint
43
+ @fonts.glyph_width(@resource, cp).to_i
44
+ end
45
+
46
+ def measure_text(text, size:)
47
+ return 0.0 if text.nil? || text.empty?
48
+
49
+ text.each_codepoint.sum do |cp|
50
+ glyph_width(cp) * size.to_f / units_per_em
51
+ end
52
+ end
53
+
54
+ def kerning_pair(_left_cp, _right_cp)
55
+ 0
56
+ end
57
+
58
+ def postscript_name
59
+ @resource.to_s
60
+ end
61
+
62
+ def family_name
63
+ postscript_name
64
+ end
65
+
66
+ def style_name
67
+ "Regular"
68
+ end
69
+
70
+ def path
71
+ nil
72
+ end
73
+
74
+ private
75
+
76
+ def metrics_data
77
+ @fonts.metrics_for(@resource) || {}
78
+ end
79
+ end
80
+ end
81
+ end
@@ -5,8 +5,7 @@ module Idml
5
5
  # geometry and produces positioned glyphs using font metrics from
6
6
  # .ttf/.otf files. IDML-agnostic — works on any styled text.
7
7
  module TextEngine
8
- autoload :FontMetrics, "#{__dir__}/text_engine/font_metrics"
9
- autoload :FontResolver, "#{__dir__}/text_engine/font_resolver"
8
+ autoload :PdfrbFontMetrics, "#{__dir__}/text_engine/pdfrb_font_metrics"
10
9
  autoload :Shaper, "#{__dir__}/text_engine/shaper"
11
10
  autoload :ShapedGlyph, "#{__dir__}/text_engine/shaper"
12
11
  autoload :LineBreaker, "#{__dir__}/text_engine/line_breaker"
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.3.0"
4
+ VERSION = "0.3.1"
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.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
@@ -23,20 +23,6 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0'
26
- - !ruby/object:Gem::Dependency
27
- name: fontisan
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: '0'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: '0'
40
26
  - !ruby/object:Gem::Dependency
41
27
  name: lutaml-model
42
28
  requirement: !ruby/object:Gem::Requirement
@@ -414,10 +400,9 @@ files:
414
400
  - lib/idml/render/style_resolver.rb
415
401
  - lib/idml/text_engine.rb
416
402
  - lib/idml/text_engine/cjk_layout.rb
417
- - lib/idml/text_engine/font_metrics.rb
418
- - lib/idml/text_engine/font_resolver.rb
419
403
  - lib/idml/text_engine/justifier.rb
420
404
  - lib/idml/text_engine/line_breaker.rb
405
+ - lib/idml/text_engine/pdfrb_font_metrics.rb
421
406
  - lib/idml/text_engine/shaper.rb
422
407
  - lib/idml/text_engine/vertical_layout.rb
423
408
  - lib/idml/validation.rb
@@ -1,226 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fontisan"
4
-
5
- module Idml
6
- module TextEngine
7
- # Reads OpenType/TrueType font metrics via Fontisan. Uses Fontisan's
8
- # FontLoader to open the font file and locate table offsets, then
9
- # parses the head, hhea, hmtx, and cmap tables directly from the
10
- # binary. No ttfunk.
11
- class FontMetrics
12
- @cache = {}
13
-
14
- def self.open(path)
15
- @cache[path] ||= new(path)
16
- end
17
-
18
- def self.clear_cache
19
- @cache.clear
20
- end
21
-
22
- def initialize(path)
23
- @path = path
24
- @font = Fontisan::FontLoader.load(path)
25
- @io = File.open(path, "rb")
26
- parse_head
27
- parse_hhea
28
- parse_hmtx
29
- parse_cmap
30
- @width_cache = {}
31
- end
32
-
33
- attr_reader :units_per_em, :ascent, :descent, :line_gap, :path
34
-
35
- def glyph_width(codepoint)
36
- @width_cache[codepoint] ||=
37
- begin
38
- glyph_id = @code_map[codepoint] || 0
39
- metric = @advance_widths[glyph_id]
40
- metric || @advance_widths.last || 0
41
- end
42
- end
43
-
44
- def kerning_pair(_left_cp, _right_cp)
45
- 0
46
- end
47
-
48
- def measure_text(text, size:)
49
- total = 0
50
- text.each_codepoint { |cp| total += glyph_width(cp) }
51
- total.to_f / units_per_em * size
52
- end
53
-
54
- def postscript_name
55
- @postscript_name ||= parse_name_entry(6) || "Unknown"
56
- end
57
-
58
- def family_name
59
- @family_name ||= parse_name_entry(1) || "Unknown"
60
- end
61
-
62
- def style_name
63
- @style_name ||= parse_name_entry(2) || "Regular"
64
- end
65
-
66
- private
67
-
68
- def find_table(tag)
69
- @font.tables.find { |entry| entry.tag == tag }
70
- end
71
-
72
- def read_table_bytes(tag)
73
- entry = find_table(tag)
74
- return nil unless entry
75
-
76
- @io.seek(entry.offset)
77
- @io.read(entry.table_length)
78
- end
79
-
80
- def parse_head
81
- data = read_table_bytes("head")
82
- return unless data
83
-
84
- @units_per_em = data[18, 2].unpack1("n")
85
- end
86
-
87
- def parse_hhea
88
- data = read_table_bytes("hhea")
89
- return unless data
90
-
91
- @ascent = sign_extend(data[4, 2].unpack1("n"))
92
- @descent = sign_extend(data[6, 2].unpack1("n"))
93
- @line_gap = sign_extend(data[8, 2].unpack1("n"))
94
- @num_hmetrics = data[34, 2].unpack1("n")
95
- end
96
-
97
- def parse_hmtx
98
- data = read_table_bytes("hmtx")
99
- return unless data
100
-
101
- @advance_widths = []
102
- @num_hmetrics.times do |i|
103
- offset = i * 4
104
- @advance_widths[i] = data[offset, 2].unpack1("n")
105
- end
106
- end
107
-
108
- def parse_cmap
109
- data = read_table_bytes("cmap")
110
- return unless data
111
-
112
- _, num_subtables = data[0, 4].unpack("nn")
113
- @code_map = {}
114
-
115
- best_offset = nil
116
- num_subtables.times do |i|
117
- rec_off = 4 + (i * 4)
118
- platform_id, encoding_id, offset = data[rec_off, 8].unpack("nnN")
119
- if platform_id == 3 && encoding_id == 1
120
- best_offset = offset
121
- break
122
- end
123
- best_offset ||= offset if platform_id.zero?
124
- end
125
- return unless best_offset
126
-
127
- parse_cmap_subtable(data, best_offset)
128
- end
129
-
130
- def parse_cmap_subtable(data, offset)
131
- format = data[offset, 2].unpack1("n")
132
- parse_cmap_format4(data, offset) if format == 4
133
- parse_cmap_format0(data, offset) if format.zero?
134
- end
135
-
136
- def sign_extend(uint16)
137
- uint16 >= 32768 ? uint16 - 65536 : uint16
138
- end
139
-
140
- def parse_cmap_format4(data, offset)
141
- seg_count_x2 = data[offset + 6, 2].unpack1("n")
142
- seg_count = seg_count_x2 / 2
143
-
144
- end_start = offset + 14
145
- start_start = end_start + seg_count_x2 + 2
146
- delta_start = start_start + seg_count_x2
147
- range_start = delta_start + seg_count_x2
148
-
149
- seg_count.times do |i|
150
- end_code = data[end_start + (i * 2), 2].unpack1("n")
151
- start_code = data[start_start + (i * 2), 2].unpack1("n")
152
- delta = sign_extend(data[delta_start + (i * 2), 2].unpack1("n"))
153
- range_off = data[range_start + (i * 2), 2].unpack1("n")
154
-
155
- (start_code..end_code).each do |cp|
156
- if range_off.zero?
157
- glyph_id = (cp + delta) & 0xFFFF
158
- else
159
- idx = range_start + (i * 2) + range_off + ((cp - start_code) * 2)
160
- glyph_id = data[idx, 2].unpack1("n")
161
- glyph_id = (glyph_id + delta) & 0xFFFF if glyph_id != 0
162
- end
163
- @code_map[cp] = glyph_id if glyph_id != 0
164
- end
165
- end
166
- end
167
-
168
- def parse_cmap_format0(data, offset)
169
- 256.times do |cp|
170
- glyph_id = data[offset + 6 + cp, 1].unpack1("C")
171
- @code_map[cp] = glyph_id if glyph_id != 0
172
- end
173
- end
174
-
175
- def parse_name_entry(name_id)
176
- data = read_table_bytes("name")
177
- return nil unless data
178
-
179
- _format, count, string_offset = data[0, 6].unpack("nnn")
180
- best = nil
181
- best_priority = 99
182
- count.times do |i|
183
- rec = 6 + (i * 12)
184
- platform_id, encoding_id, _lang_id, id, length, str_off =
185
- data[rec, 12].unpack("nnnnnn")
186
- next unless id == name_id
187
-
188
- priority = name_platform_priority(platform_id, encoding_id)
189
- next if priority >= best_priority
190
-
191
- raw = data[string_offset + str_off, length]
192
- decoded = decode_name_string(raw, platform_id)
193
- next unless decoded && !decoded.empty?
194
-
195
- best = decoded
196
- best_priority = priority
197
- end
198
- best
199
- end
200
-
201
- # Prefer Windows (platform 3, encoding 1) name records, then
202
- # any Windows record, then Macintosh, then Unicode.
203
- def name_platform_priority(platform_id, encoding_id)
204
- case platform_id
205
- when 3 then encoding_id == 1 ? 0 : 1
206
- when 0 then 3
207
- when 1 then 2
208
- else 4
209
- end
210
- end
211
-
212
- def decode_name_string(raw, platform_id)
213
- result =
214
- case platform_id
215
- when 3, 0
216
- raw.encode("UTF-8", "UTF-16BE", invalid: :replace, undef: :replace)
217
- when 1
218
- raw.encode("UTF-8", "MacRoman", invalid: :replace, undef: :replace)
219
- else
220
- raw.dup.force_encoding("UTF-8").scrub("")
221
- end
222
- result.strip
223
- end
224
- end
225
- end
226
- end
@@ -1,105 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Idml
4
- module TextEngine
5
- # Maps IDML font references (family + style name) to .ttf/.otf
6
- # file paths on disk via Fontisan. Searches system font dirs and
7
- # any user-configured search paths.
8
- class FontResolver
9
- DEFAULT_SEARCH_PATHS = [
10
- "/System/Library/Fonts",
11
- "/Library/Fonts",
12
- File.expand_path("~/Library/Fonts"),
13
- "/usr/share/fonts",
14
- File.expand_path("~/.local/share/fonts"),
15
- ].freeze
16
-
17
- STYLE_ALIASES = {
18
- "Regular" => %w[Regular Normal Book Roman Medium],
19
- "Bold" => %w[Bold Semibold Heavy Black],
20
- "Italic" => %w[Italic Oblique Slanted],
21
- "Bold Italic" => %w[Bold\ Italic BoldOblique SemiboldItalic],
22
- }.freeze
23
-
24
- def initialize(search_paths: DEFAULT_SEARCH_PATHS)
25
- @search_paths = search_paths
26
- @cache = {}
27
- end
28
-
29
- def resolve(family_name:, style_name: "Regular")
30
- key = [family_name, style_name]
31
- @cache[key] ||= find_font(family_name, style_name)
32
- end
33
-
34
- # Search by PostScriptName (e.g., "MinionPro-Regular").
35
- # More precise than family+style — maps directly to the font's
36
- # internal PostScript name. Used when the IDML document references
37
- # a specific PostScriptName from Fonts.xml.
38
- def resolve_by_ps_name(ps_name)
39
- return nil unless ps_name
40
-
41
- @ps_cache ||= {}
42
- return @ps_cache[ps_name] if @ps_cache.key?(ps_name)
43
-
44
- @ps_cache[ps_name] = find_by_ps_name(ps_name)
45
- end
46
-
47
- private
48
-
49
- def find_by_ps_name(ps_name)
50
- @search_paths.each do |dir|
51
- next unless Dir.exist?(dir)
52
-
53
- Dir.glob(File.join(dir, "**", "*.ttf")).each do |path|
54
- font = safe_load(path)
55
- next unless font
56
-
57
- return FontMetrics.open(path) if font.postscript_name == ps_name
58
- end
59
- Dir.glob(File.join(dir, "**", "*.otf")).each do |path|
60
- font = safe_load(path)
61
- next unless font
62
-
63
- return FontMetrics.open(path) if font.postscript_name == ps_name
64
- end
65
- end
66
- nil
67
- end
68
-
69
- def find_font(family, style)
70
- candidates = STYLE_ALIASES[style] || [style]
71
- @search_paths.each do |dir|
72
- next unless Dir.exist?(dir)
73
-
74
- Dir.glob(File.join(dir, "**", "*.ttf")).each do |path|
75
- font = safe_load(path)
76
- next unless font
77
-
78
- return FontMetrics.open(path) if matches?(font, family, candidates)
79
- end
80
- Dir.glob(File.join(dir, "**", "*.otf")).each do |path|
81
- font = safe_load(path)
82
- next unless font
83
-
84
- return FontMetrics.open(path) if matches?(font, family, candidates)
85
- end
86
- end
87
- nil
88
- end
89
-
90
- def matches?(font, family, style_candidates)
91
- return false unless font.family_name.match?(/#{Regexp.escape(family)}/i)
92
-
93
- style_candidates.any? do |s|
94
- font.style_name.match?(/#{Regexp.escape(s)}/i)
95
- end
96
- end
97
-
98
- def safe_load(path)
99
- FontMetrics.open(path)
100
- rescue StandardError
101
- nil
102
- end
103
- end
104
- end
105
- end