ucode 0.3.3 → 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 +4 -4
- data/README.md +45 -6
- data/config/unicode17_universal_glyph_set.yml +1 -1
- data/lib/ucode/cli.rb +201 -32
- data/lib/ucode/code_chart/batch_runner.rb +184 -0
- data/lib/ucode/code_chart/block_index.rb +89 -0
- data/lib/ucode/code_chart/coverage_gap_index.rb +115 -0
- data/lib/ucode/code_chart/extractor.rb +65 -13
- data/lib/ucode/code_chart/fetcher.rb +112 -0
- data/lib/ucode/code_chart/gap_analyzer/block_gap.rb +35 -0
- data/lib/ucode/code_chart/gap_analyzer/essenfont_manifest.rb +60 -0
- data/lib/ucode/code_chart/gap_analyzer/manifest.rb +39 -0
- data/lib/ucode/code_chart/gap_analyzer.rb +83 -0
- data/lib/ucode/code_chart/provenance.rb +99 -58
- data/lib/ucode/code_chart/sidecar.rb +3 -4
- data/lib/ucode/code_chart/verifier/builder.rb +49 -0
- data/lib/ucode/code_chart/verifier/mutool_strategy.rb +80 -0
- data/lib/ucode/code_chart/verifier/page_render_cache.rb +63 -0
- data/lib/ucode/code_chart/verifier/result.rb +27 -0
- data/lib/ucode/code_chart/verifier/resvg_strategy.rb +71 -0
- data/lib/ucode/code_chart/verifier/strategy.rb +73 -0
- data/lib/ucode/code_chart/verifier.rb +152 -0
- data/lib/ucode/code_chart/writer.rb +21 -14
- data/lib/ucode/code_chart.rb +6 -0
- data/lib/ucode/error.rb +16 -1
- data/lib/ucode/fetch/http.rb +23 -3
- data/lib/ucode/glyphs/embedded_fonts/catalog.rb +66 -3
- data/lib/ucode/glyphs/embedded_fonts/codepoint_mapper/correlator_strategy.rb +46 -0
- data/lib/ucode/glyphs/embedded_fonts/codepoint_mapper/strategy.rb +54 -0
- data/lib/ucode/glyphs/embedded_fonts/codepoint_mapper/tounicode_strategy.rb +41 -0
- data/lib/ucode/glyphs/embedded_fonts/codepoint_mapper/trace_strategy.rb +64 -0
- data/lib/ucode/glyphs/embedded_fonts/codepoint_mapper.rb +150 -90
- data/lib/ucode/glyphs/embedded_fonts/mutool/draw.rb +26 -0
- data/lib/ucode/glyphs/embedded_fonts/mutool/info.rb +23 -0
- data/lib/ucode/glyphs/embedded_fonts/mutool/show.rb +36 -0
- data/lib/ucode/glyphs/embedded_fonts/mutool/trace.rb +26 -0
- data/lib/ucode/glyphs/embedded_fonts/mutool.rb +30 -0
- data/lib/ucode/glyphs/embedded_fonts/page_trace_cache.rb +112 -0
- data/lib/ucode/glyphs/embedded_fonts/pdf_indexer.rb +50 -56
- data/lib/ucode/glyphs/embedded_fonts/{pdf_location.rb → pdf_source.rb} +1 -1
- data/lib/ucode/glyphs/embedded_fonts/renderer.rb +19 -2
- data/lib/ucode/glyphs/embedded_fonts/trace_runner.rb +12 -28
- data/lib/ucode/glyphs/embedded_fonts.rb +3 -1
- data/lib/ucode/glyphs/real_fonts/coverage_auditor.rb +0 -4
- data/lib/ucode/glyphs/real_fonts/font_coverage_report.rb +0 -2
- data/lib/ucode/glyphs/real_fonts/writer.rb +0 -2
- data/lib/ucode/glyphs/source.rb +11 -1
- data/lib/ucode/glyphs/sources/pillar1_embedded_tounicode.rb +11 -6
- data/lib/ucode/version.rb +1 -1
- data/lib/ucode.rb +2 -0
- metadata +28 -74
- data/TODO.extract-code-chart/01-pdf-fetch-validation.md +0 -80
- data/TODO.extract-code-chart/02-block-name-resolver.md +0 -68
- data/TODO.extract-code-chart/03-codechart-namespace.md +0 -82
- data/TODO.extract-code-chart/04-codechart-extractor.md +0 -154
- data/TODO.extract-code-chart/05-provenance-and-sidecar.md +0 -147
- data/TODO.extract-code-chart/06-codechart-writer.md +0 -134
- data/TODO.extract-code-chart/07-codechart-cli.md +0 -135
- data/TODO.extract-code-chart/08-specs.md +0 -87
- data/TODO.full/00-README.md +0 -116
- data/TODO.full/01-panglyph-vision.md +0 -112
- data/TODO.full/02-panglyph-repo-bootstrap.md +0 -184
- data/TODO.full/03-panglyph-font-builder.md +0 -201
- data/TODO.full/04-panglyph-publish-pipeline.md +0 -126
- data/TODO.full/05-ucode-0-1-1-release.md +0 -139
- data/TODO.full/06-fontisan-remove-audit.md +0 -142
- data/TODO.full/07-fontisan-remove-ucd.md +0 -125
- data/TODO.full/08-archive-private-bin-build.md +0 -143
- data/TODO.full/09-archive-public-structure.md +0 -164
- data/TODO.full/10-fontist-org-woff-glyphs.md +0 -131
- data/TODO.full/11-fontist-org-audit-coverage.md +0 -140
- data/TODO.full/12-implementation-order.md +0 -216
- data/TODO.full/13-fontisan-font-writer-api.md +0 -189
- data/TODO.full/14-fontisan-table-writers.md +0 -66
- data/TODO.full/15-panglyph-builder-real.md +0 -82
- data/TODO.full/16-archive-public-sync-workflows.md +0 -167
- data/TODO.full/17-fontist-org-font-picker.md +0 -73
- data/TODO.full/18-comprehensive-spec-coverage.md +0 -64
- data/TODO.full/19-ucode-0-1-2-patch.md +0 -32
- data/TODO.full/20-fontisan-0-2-23-release.md +0 -52
- data/TODO.new/00-README.md +0 -96
- data/TODO.new/01-pillar-terminology-alignment.md +0 -69
- data/TODO.new/02-audit-schema-design.md +0 -255
- data/TODO.new/03-directory-output-spec.md +0 -203
- data/TODO.new/04-fontist-org-contract.md +0 -173
- data/TODO.new/05-baseline-unicode17-coverage-audit.md +0 -144
- data/TODO.new/06-audit-namespace-skeleton.md +0 -105
- data/TODO.new/07-audit-models-port.md +0 -132
- data/TODO.new/08-extractors-cheap-port.md +0 -113
- data/TODO.new/09-extractors-expensive-port.md +0 -99
- data/TODO.new/10-aggregations-ucd-rewrite.md +0 -168
- data/TODO.new/11-differ-and-library-auditor-port.md +0 -102
- data/TODO.new/12-formatters-port.md +0 -115
- data/TODO.new/13-directory-emitter.md +0 -147
- data/TODO.new/14-html-face-browser.md +0 -144
- data/TODO.new/15-html-library-browser.md +0 -102
- data/TODO.new/16-cli-audit-subcommands.md +0 -142
- data/TODO.new/17-fontisan-cleanup-audit.md +0 -147
- data/TODO.new/18-fontisan-cleanup-ucd.md +0 -156
- data/TODO.new/19-fontisan-docs-update.md +0 -155
- data/TODO.new/20-canonical-resolver-4-tier.md +0 -182
- data/TODO.new/21-canonical-unicode17-build.md +0 -148
- data/TODO.new/22-implementation-order.md +0 -176
- data/TODO.new/23-universal-glyph-set-source-map.md +0 -312
- data/TODO.new/24-universal-glyph-set-build.md +0 -189
- data/TODO.new/25-font-audit-against-universal-set.md +0 -195
- data/TODO.new/26-missing-glyph-reporter.md +0 -189
- data/TODO.new/27-fontist-org-consumer-integration.md +0 -200
- data/TODO.new/28-implementation-order-update.md +0 -187
- data/TODO.new/29-universal-set-curation-uc17.md +0 -312
- data/TODO.new/30-tier1-font-acquisition.md +0 -241
- data/TODO.new/31-universal-set-production-build.md +0 -205
- data/TODO.new/32-uc17-coverage-matrix.md +0 -165
- data/TODO.new/33-specialist-font-acquisition-refresh.md +0 -138
- data/TODO.new/34-pillar2-content-stream-correlator.md +0 -147
- data/TODO.new/35-universal-set-production-run.md +0 -160
- data/TODO.new/36-per-font-coverage-audit.md +0 -145
- data/TODO.new/37-coverage-highlight-reporter.md +0 -125
- data/TODO.new/38-fontist-org-glyph-consumer.md +0 -141
- data/TODO.new/39-implementation-order-update-32-38.md +0 -258
- data/TODO.new/40-archive-private-uses-ucode-audit.md +0 -124
- data/TODO.new/41-ucode-unicode-archive-bridge.md +0 -160
|
@@ -1,128 +1,188 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
require "
|
|
5
|
-
require "
|
|
3
|
+
require "ucode/glyphs/embedded_fonts/codepoint_mapper/strategy"
|
|
4
|
+
require "ucode/glyphs/embedded_fonts/codepoint_mapper/tounicode_strategy"
|
|
5
|
+
require "ucode/glyphs/embedded_fonts/codepoint_mapper/correlator_strategy"
|
|
6
|
+
require "ucode/glyphs/embedded_fonts/codepoint_mapper/trace_strategy"
|
|
7
|
+
require "ucode/glyphs/embedded_fonts/mutool"
|
|
8
|
+
require "ucode/glyphs/embedded_fonts/page_trace_cache"
|
|
9
|
+
require "ucode/error"
|
|
6
10
|
|
|
7
11
|
module Ucode
|
|
8
12
|
module Glyphs
|
|
9
13
|
module EmbeddedFonts
|
|
10
|
-
# Resolves codepoint → GID for one Type0 font via a
|
|
14
|
+
# Resolves codepoint → GID for one Type0 font via a chain of
|
|
15
|
+
# {Strategy} subclasses.
|
|
11
16
|
#
|
|
12
|
-
#
|
|
13
|
-
# for pillar 1). Parsed by {ToUnicode}.
|
|
14
|
-
# 2. **Caller-supplied correlator config** (pillar 2) — render the
|
|
15
|
-
# font's pages to SVG and run {ContentStreamCorrelator}.
|
|
16
|
-
# 3. **Auto-detect via mutool trace** (pillar 2b) — trace every
|
|
17
|
-
# page and run {TraceCorrelator} positionally.
|
|
17
|
+
# The chain is partitioned by {Strategy#positional?}:
|
|
18
18
|
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
19
|
+
# * Intrinsic strategies (ToUnicodeStrategy) read the font's
|
|
20
|
+
# own CMap. Tried in chain order; the first non-empty result
|
|
21
|
+
# wins.
|
|
22
|
+
# * Positional strategies (CorrelatorStrategy, TraceStrategy)
|
|
23
|
+
# attribute glyphs by chart-grid geometry. Expensive — they
|
|
24
|
+
# shell out to mutool per page — so they only run when an
|
|
25
|
+
# intrinsic strategy cannot cover the requested block scope.
|
|
21
26
|
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
27
|
+
# When positional strategies run, their results merge over the
|
|
28
|
+
# intrinsic result with positional precedence: chart geometry is
|
|
29
|
+
# authoritative for in-block specimens, while a font's CMap can
|
|
30
|
+
# be misleading (Enclosed Ideographic Supplement, where the
|
|
31
|
+
# embedded CJKSymbols font's CMap maps its CIDs to the *composing*
|
|
32
|
+
# ideographs rather than the squared characters themselves).
|
|
33
|
+
#
|
|
34
|
+
# Two escape hatches let callers control the gating:
|
|
35
|
+
#
|
|
36
|
+
# * `block_range:` — when set, an intrinsic result with zero
|
|
37
|
+
# in-block intersection is treated as "wrong scope" and
|
|
38
|
+
# dropped, so positional strategies can take over. Auto-detect
|
|
39
|
+
# for the U1F200 class of failure (Option 1).
|
|
40
|
+
# * `force_positional_for_font_ids:` — Type0 font object IDs
|
|
41
|
+
# that always go through positional attribution, regardless
|
|
42
|
+
# of whether the intrinsic strategy succeeded. Escape hatch
|
|
43
|
+
# for partial-overlap cases where the CMap covers some
|
|
44
|
+
# in-block codepoints but positional attribution is still
|
|
45
|
+
# desired for the rest (Option 2).
|
|
46
|
+
#
|
|
47
|
+
# Adding a new strategy = one Strategy subclass + (if positional)
|
|
48
|
+
# a `positional?` override + one entry in the chain. No edit to
|
|
49
|
+
# {#map} (Open/Closed Principle).
|
|
26
50
|
class CodepointMapper
|
|
27
|
-
# @param
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
51
|
+
# @param strategies [Array<Strategy>] chain; partitioned by
|
|
52
|
+
# Strategy#positional? at map time
|
|
53
|
+
# @param block_range [Range<Integer>, nil] codepoint scope the
|
|
54
|
+
# caller is extracting. nil = intrinsic result is always
|
|
55
|
+
# trusted (legacy behavior; the caller doesn't know or care
|
|
56
|
+
# about block scope).
|
|
57
|
+
# @param force_positional_for_font_ids [Set<Integer>] Type0
|
|
58
|
+
# font object IDs that always trigger positional attribution
|
|
59
|
+
def initialize(strategies:, block_range: nil,
|
|
60
|
+
force_positional_for_font_ids: Set.new)
|
|
61
|
+
@strategies = strategies
|
|
62
|
+
@block_range = block_range
|
|
63
|
+
@force_positional_for_font_ids = force_positional_for_font_ids
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Convenience builder — wires up the default 3-strategy chain
|
|
67
|
+
# with default Mutool wrappers. Callers that need to inject
|
|
68
|
+
# stubs for tests should construct strategies directly and pass
|
|
69
|
+
# them to +#initialize+.
|
|
70
|
+
#
|
|
71
|
+
# @param trace_cache [PageTraceCache, nil] when provided, the
|
|
72
|
+
# TraceStrategy shares this cache (lets the caller reuse the
|
|
73
|
+
# traced pages for downstream concerns like Catalog's
|
|
74
|
+
# location lookup). nil = construct internally.
|
|
75
|
+
# @return [CodepointMapper]
|
|
76
|
+
def self.build(source:, correlator_configs:, indexer:,
|
|
77
|
+
block_range: nil, force_positional_for_font_ids: Set.new,
|
|
78
|
+
mutool_show: Mutool::Show.new,
|
|
79
|
+
mutool_draw: Mutool::Draw.new,
|
|
80
|
+
mutool_trace: Mutool::Trace.new,
|
|
81
|
+
trace_cache: nil)
|
|
82
|
+
trace_cache ||= PageTraceCache.new(
|
|
83
|
+
pdf: source.pdf_path,
|
|
84
|
+
page_count: indexer.page_count,
|
|
85
|
+
mutool: mutool_trace,
|
|
86
|
+
)
|
|
87
|
+
strategies = [
|
|
88
|
+
ToUnicodeStrategy.new(source: source, mutool_show: mutool_show),
|
|
89
|
+
CorrelatorStrategy.new(source: source,
|
|
90
|
+
correlator_configs: correlator_configs,
|
|
91
|
+
mutool_draw: mutool_draw),
|
|
92
|
+
TraceStrategy.new(cache: trace_cache, indexer: indexer),
|
|
93
|
+
]
|
|
94
|
+
new(strategies: strategies,
|
|
95
|
+
block_range: block_range,
|
|
96
|
+
force_positional_for_font_ids: force_positional_for_font_ids)
|
|
35
97
|
end
|
|
36
98
|
|
|
37
99
|
# @param descriptor [RawFontDescriptor]
|
|
38
|
-
# @return [Hash{Integer=>Integer}] codepoint => gid; empty
|
|
39
|
-
# no strategy produces a
|
|
100
|
+
# @return [Hash{Integer=>Integer}] codepoint => gid; empty
|
|
101
|
+
# when no strategy produces a mapping
|
|
40
102
|
def map(descriptor)
|
|
41
103
|
return {} unless descriptor.cid_map_kind == :identity
|
|
42
104
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
105
|
+
positional, intrinsic = partition_strategies
|
|
106
|
+
intrinsic_result = run_intrinsic(descriptor, intrinsic)
|
|
107
|
+
return intrinsic_result if positional.empty?
|
|
108
|
+
return intrinsic_result unless needs_positional?(descriptor,
|
|
109
|
+
intrinsic_result)
|
|
48
110
|
|
|
49
|
-
|
|
111
|
+
positional_result = run_positional(descriptor, positional)
|
|
112
|
+
merge_with_positional_precedence(intrinsic_result, positional_result)
|
|
50
113
|
end
|
|
51
114
|
|
|
52
115
|
private
|
|
53
116
|
|
|
54
|
-
#
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
build_codepoint_map(cid_to_cp)
|
|
117
|
+
# `Enumerable#partition` returns `[truthy_group, falsy_group]`,
|
|
118
|
+
# so the first element is positional strategies and the second
|
|
119
|
+
# is intrinsic. Naming them explicitly here avoids the
|
|
120
|
+
# destructure-order footgun.
|
|
121
|
+
def partition_strategies
|
|
122
|
+
positional, intrinsic = @strategies.partition(&:positional?)
|
|
123
|
+
[positional, intrinsic]
|
|
62
124
|
end
|
|
63
125
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
126
|
+
# Intrinsic chain: first non-empty result wins. preserves the
|
|
127
|
+
# pre-partition semantics so the legacy ToUnicode-only flow
|
|
128
|
+
# behaves identically when no positional strategies exist.
|
|
129
|
+
def run_intrinsic(descriptor, strategies)
|
|
130
|
+
strategies.each do |s|
|
|
131
|
+
next unless s.supports?(descriptor)
|
|
69
132
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
tmp.close
|
|
73
|
-
ok = system("mutool", "show", "-o", tmp.path, "-b",
|
|
74
|
-
@source.pdf_to_s, obj_id.to_s,
|
|
75
|
-
out: File::NULL, err: File::NULL)
|
|
76
|
-
unless ok
|
|
77
|
-
raise Ucode::EmbeddedFontsMissingError,
|
|
78
|
-
"mutool show failed for ToUnicode obj=#{obj_id}"
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
File.binread(tmp.path).force_encoding("UTF-8")
|
|
133
|
+
result = s.map(descriptor)
|
|
134
|
+
return result unless result.empty?
|
|
82
135
|
end
|
|
136
|
+
{}
|
|
83
137
|
end
|
|
84
138
|
|
|
85
|
-
#
|
|
139
|
+
# Positional strategies are gated behind three conditions, any
|
|
140
|
+
# of which triggers them:
|
|
141
|
+
#
|
|
142
|
+
# 1. Caller explicitly listed this font in
|
|
143
|
+
# `force_positional_for_font_ids` (Option 2 escape hatch).
|
|
144
|
+
# 2. No intrinsic strategy produced a mapping (legacy
|
|
145
|
+
# fallback for fonts without ToUnicode).
|
|
146
|
+
# 3. The intrinsic result fell entirely outside the
|
|
147
|
+
# caller's block scope — the font's CMap encoded the
|
|
148
|
+
# wrong codepoints (Option 1 auto-detect, e.g. U1F200).
|
|
149
|
+
def needs_positional?(descriptor, intrinsic_result)
|
|
150
|
+
return true if @force_positional_for_font_ids.include?(descriptor.font_obj_id)
|
|
151
|
+
return true if intrinsic_result.empty?
|
|
152
|
+
return true if intrinsic_out_of_scope?(intrinsic_result)
|
|
153
|
+
|
|
154
|
+
false
|
|
155
|
+
end
|
|
86
156
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
return {} unless config
|
|
157
|
+
def intrinsic_out_of_scope?(intrinsic_result)
|
|
158
|
+
return false unless @block_range
|
|
90
159
|
|
|
91
|
-
|
|
92
|
-
|
|
160
|
+
# block_range is a Range, not an Array; Array#intersect? would
|
|
161
|
+
# force an eager .to_a conversion on potentially huge CJK ranges.
|
|
162
|
+
intrinsic_result.keys.all? { |cp| !@block_range.include?(cp) }
|
|
93
163
|
end
|
|
94
164
|
|
|
95
|
-
|
|
96
|
-
|
|
165
|
+
# Positional chain: union of all positional strategies' results.
|
|
166
|
+
# Within positional, earlier strategies win on conflict (chain
|
|
167
|
+
# order expresses caller preference — CorrelatorStrategy
|
|
168
|
+
# before TraceStrategy when both are configured).
|
|
169
|
+
def run_positional(descriptor, strategies)
|
|
170
|
+
merged = {}
|
|
171
|
+
strategies.each do |s|
|
|
172
|
+
next unless s.supports?(descriptor)
|
|
97
173
|
|
|
98
|
-
|
|
99
|
-
"mutool", "draw", "-F", "svg",
|
|
100
|
-
@source.pdf_to_s,
|
|
101
|
-
*page_numbers.map(&:to_s),
|
|
102
|
-
)
|
|
103
|
-
unless status.success?
|
|
104
|
-
raise Ucode::EmbeddedFontsMissingError,
|
|
105
|
-
"mutool draw failed: #{err.strip}"
|
|
174
|
+
s.map(descriptor).each { |cp, gid| merged[cp] ||= gid }
|
|
106
175
|
end
|
|
107
|
-
|
|
108
|
-
out
|
|
176
|
+
merged
|
|
109
177
|
end
|
|
110
178
|
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
(1..@indexer.page_count).each_with_object({}) do |page, mapping|
|
|
120
|
-
glyphs = runner.trace([page])
|
|
121
|
-
page_mapping = correlator.correlate(glyphs)
|
|
122
|
-
page_mapping.each do |cp, gid|
|
|
123
|
-
mapping[cp] ||= gid
|
|
124
|
-
end
|
|
125
|
-
end
|
|
179
|
+
# Merge intrinsic and positional results. Positional wins on
|
|
180
|
+
# conflict (chart geometry is authoritative for in-block
|
|
181
|
+
# specimens).
|
|
182
|
+
def merge_with_positional_precedence(intrinsic, positional)
|
|
183
|
+
merged = intrinsic.dup
|
|
184
|
+
positional.each { |cp, gid| merged[cp] = gid }
|
|
185
|
+
merged
|
|
126
186
|
end
|
|
127
187
|
end
|
|
128
188
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require "ucode/glyphs/embedded_fonts/mutool"
|
|
6
|
+
|
|
7
|
+
module Ucode
|
|
8
|
+
module Glyphs
|
|
9
|
+
module EmbeddedFonts
|
|
10
|
+
module Mutool
|
|
11
|
+
class Draw
|
|
12
|
+
def initialize(runner: SystemRunner.new)
|
|
13
|
+
@runner = runner
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def svg(pdf, *pages)
|
|
17
|
+
return "" if pages.empty?
|
|
18
|
+
|
|
19
|
+
@runner.run("mutool", "draw", "-F", "svg", pdf.to_s,
|
|
20
|
+
*pages.map(&:to_s))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require "ucode/glyphs/embedded_fonts/mutool"
|
|
6
|
+
|
|
7
|
+
module Ucode
|
|
8
|
+
module Glyphs
|
|
9
|
+
module EmbeddedFonts
|
|
10
|
+
module Mutool
|
|
11
|
+
class Info
|
|
12
|
+
def initialize(runner: SystemRunner.new)
|
|
13
|
+
@runner = runner
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(pdf)
|
|
17
|
+
@runner.run("mutool", "info", pdf.to_s)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
require "ucode/glyphs/embedded_fonts/mutool"
|
|
7
|
+
|
|
8
|
+
module Ucode
|
|
9
|
+
module Glyphs
|
|
10
|
+
module EmbeddedFonts
|
|
11
|
+
module Mutool
|
|
12
|
+
class Show
|
|
13
|
+
def initialize(runner: SystemRunner.new)
|
|
14
|
+
@runner = runner
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def grep(pdf, *obj_ids)
|
|
18
|
+
return "" if obj_ids.empty?
|
|
19
|
+
|
|
20
|
+
@runner.run("mutool", "show", "-g", pdf.to_s,
|
|
21
|
+
*obj_ids.map(&:to_s))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def stream(pdf, obj_id)
|
|
25
|
+
Tempfile.create("mutool-stream") do |tmp|
|
|
26
|
+
tmp.close
|
|
27
|
+
@runner.run("mutool", "show", "-o", tmp.path, "-b",
|
|
28
|
+
pdf.to_s, obj_id.to_s)
|
|
29
|
+
File.binread(tmp.path).force_encoding("UTF-8")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require "ucode/glyphs/embedded_fonts/mutool"
|
|
6
|
+
|
|
7
|
+
module Ucode
|
|
8
|
+
module Glyphs
|
|
9
|
+
module EmbeddedFonts
|
|
10
|
+
module Mutool
|
|
11
|
+
class Trace
|
|
12
|
+
def initialize(runner: SystemRunner.new)
|
|
13
|
+
@runner = runner
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(pdf, *pages)
|
|
17
|
+
return "" if pages.empty?
|
|
18
|
+
|
|
19
|
+
@runner.run("mutool", "trace", pdf.to_s,
|
|
20
|
+
*pages.map(&:to_s))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
require "ucode/error"
|
|
6
|
+
|
|
7
|
+
module Ucode
|
|
8
|
+
module Glyphs
|
|
9
|
+
module EmbeddedFonts
|
|
10
|
+
module Mutool
|
|
11
|
+
autoload :Info, "ucode/glyphs/embedded_fonts/mutool/info"
|
|
12
|
+
autoload :Show, "ucode/glyphs/embedded_fonts/mutool/show"
|
|
13
|
+
autoload :Draw, "ucode/glyphs/embedded_fonts/mutool/draw"
|
|
14
|
+
autoload :Trace, "ucode/glyphs/embedded_fonts/mutool/trace"
|
|
15
|
+
|
|
16
|
+
class SystemRunner
|
|
17
|
+
def run(*argv)
|
|
18
|
+
out, err, status = Open3.capture3(*argv)
|
|
19
|
+
return out + err if status.success?
|
|
20
|
+
|
|
21
|
+
raise Ucode::MutoolError.new(
|
|
22
|
+
"mutool failed (exit #{status.exitstatus}): #{err.strip}",
|
|
23
|
+
context: { argv: argv },
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
require "ucode/glyphs/embedded_fonts/mutool"
|
|
6
|
+
require "ucode/glyphs/embedded_fonts/trace_parser"
|
|
7
|
+
|
|
8
|
+
module Ucode
|
|
9
|
+
module Glyphs
|
|
10
|
+
module EmbeddedFonts
|
|
11
|
+
# Per-PDF trace cache. Runs `mutool trace` once per page
|
|
12
|
+
# (1..page_count), parses each result, and stores glyphs
|
|
13
|
+
# grouped by page so each TraceStrategy can correlate
|
|
14
|
+
# positionally without re-tracing.
|
|
15
|
+
#
|
|
16
|
+
# Replaces the O(F × P) explosion in the pre-cache TraceStrategy,
|
|
17
|
+
# which spawned mutool trace once per page per CID font. For
|
|
18
|
+
# Code Charts PDFs with F trace-needing fonts and P pages, that
|
|
19
|
+
# was F×P subprocess invocations; this cache makes it exactly P.
|
|
20
|
+
#
|
|
21
|
+
# Why per-page grouping: TraceCorrelator's algorithm clusters
|
|
22
|
+
# label glyphs by Y bucket, and Y positions are page-local.
|
|
23
|
+
#
|
|
24
|
+
# Lazy: glyphs are only fetched when a strategy first asks for
|
|
25
|
+
# them. PDFs where every font has /ToUnicode never trigger the
|
|
26
|
+
# trace at all.
|
|
27
|
+
class PageTraceCache
|
|
28
|
+
# @param pdf [Pathname, String]
|
|
29
|
+
# @param page_count [Integer] total pages in the PDF
|
|
30
|
+
# @param mutool [Mutool::Trace]
|
|
31
|
+
def initialize(pdf:, page_count:, mutool: Mutool::Trace.new)
|
|
32
|
+
@pdf = Pathname.new(pdf)
|
|
33
|
+
@page_count = page_count
|
|
34
|
+
@mutool = mutool
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Array<Array<TraceGlyph>>] one Array per page,
|
|
38
|
+
# 1-indexed (index 0 is unused). Each inner array holds
|
|
39
|
+
# every glyph emitted by `mutool trace` on that page.
|
|
40
|
+
def glyphs_by_page
|
|
41
|
+
@glyphs_by_page ||= fetch_glyphs_by_page
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @param base_font [String] specimen font BaseFont name
|
|
45
|
+
# @yieldparam page [Integer] 1-based page number
|
|
46
|
+
# @yieldparam glyphs [Array<TraceGlyph>] every glyph on that
|
|
47
|
+
# page (all fonts — TraceCorrelator filters internally)
|
|
48
|
+
# @return [Boolean] true if at least one page references the
|
|
49
|
+
# font; false otherwise
|
|
50
|
+
# @return [Enumerator] if no block given
|
|
51
|
+
def each_page_for(base_font)
|
|
52
|
+
return enum_for(:each_page_for, base_font) unless block_given?
|
|
53
|
+
|
|
54
|
+
present_in_any = false
|
|
55
|
+
glyphs_by_page.each_with_index do |glyphs, idx|
|
|
56
|
+
next if idx.zero?
|
|
57
|
+
|
|
58
|
+
present = glyphs.any? { |g| g.font_name == base_font }
|
|
59
|
+
next unless present
|
|
60
|
+
|
|
61
|
+
present_in_any = true
|
|
62
|
+
yield idx, glyphs
|
|
63
|
+
end
|
|
64
|
+
present_in_any
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @param base_font [String]
|
|
68
|
+
# @return [Boolean] true if any page references this font
|
|
69
|
+
def references_font?(base_font)
|
|
70
|
+
glyphs_by_page.any? do |page_glyphs|
|
|
71
|
+
page_glyphs.any? { |g| g.font_name == base_font }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Locate the first occurrence of a specific (font, gid) pair
|
|
76
|
+
# across all traced pages. Returns nil when no match. Used by
|
|
77
|
+
# {Catalog#location_for} to attribute a codepoint's source
|
|
78
|
+
# page + (x, y) without exposing the cache's internal layout
|
|
79
|
+
# to callers.
|
|
80
|
+
#
|
|
81
|
+
# @param base_font [String] specimen font BaseFont name
|
|
82
|
+
# @param gid [Integer] glyph id inside that font
|
|
83
|
+
# @return [Hash{Symbol=>Integer, Float}, nil]
|
|
84
|
+
# `{ page: Integer, x: Float, y: Float }` or nil
|
|
85
|
+
def find_glyph(base_font:, gid:)
|
|
86
|
+
glyphs_by_page.each_with_index do |page_glyphs, idx|
|
|
87
|
+
next if idx.zero?
|
|
88
|
+
|
|
89
|
+
match = page_glyphs.find do |g|
|
|
90
|
+
g.font_name == base_font && g.gid == gid
|
|
91
|
+
end
|
|
92
|
+
return { page: idx, x: match.x, y: match.y } if match
|
|
93
|
+
end
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def fetch_glyphs_by_page
|
|
100
|
+
result = [[]] # index 0 unused (1-based pages)
|
|
101
|
+
return result unless @page_count.positive?
|
|
102
|
+
|
|
103
|
+
(1..@page_count).each do |page|
|
|
104
|
+
xml = @mutool.call(@pdf, page)
|
|
105
|
+
result << TraceParser.parse(xml)
|
|
106
|
+
end
|
|
107
|
+
result
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|