ucode 0.5.1 → 0.5.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 +4 -4
- data/config/unicode17_universal_glyph_set.yml +1 -1
- data/lib/ucode/glyphs/embedded_fonts/codepoint_mapper.rb +35 -4
- data/lib/ucode/glyphs/embedded_fonts/page_trace_cache.rb +32 -3
- data/lib/ucode/glyphs/embedded_fonts/trace_correlator.rb +2 -2
- data/lib/ucode/glyphs/embedded_fonts/trace_glyph.rb +35 -1
- data/lib/ucode/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d93808cc35d75099bb2028677c0ece5bcac1985cf93b20dbb3ba83d6161bd7a
|
|
4
|
+
data.tar.gz: 230980b63347f44f11b0cf5238836c0328aa0d46711baca05fec78830e5f67f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1e56059471ba2fe204f018c41811f19f20f1cf1569ac9da91004031561f8826c64faba4d7ecd07963d273ca51631ec71589c74860e54521e6e1d30b4ec321f7
|
|
7
|
+
data.tar.gz: 2f30cd92e0ac72619250363c4b71321241042b3ade8a7eaaf20c8bfffa2c3d79be42414e1b872d1f0375c470a4c9b82825e8a518ec328022d6c6a8096b8211a6
|
|
@@ -56,11 +56,17 @@ module Ucode
|
|
|
56
56
|
# about block scope).
|
|
57
57
|
# @param force_positional_for_font_ids [Set<Integer>] Type0
|
|
58
58
|
# font object IDs that always trigger positional attribution
|
|
59
|
+
# @param trace_cache [PageTraceCache, nil] consulted by
|
|
60
|
+
# {#needs_positional?} for the partial-ToUnicode-coverage
|
|
61
|
+
# case (the font has glyphs the CMap doesn't list). nil =
|
|
62
|
+
# skip that check (legacy behavior).
|
|
59
63
|
def initialize(strategies:, block_range: nil,
|
|
60
|
-
force_positional_for_font_ids: Set.new
|
|
64
|
+
force_positional_for_font_ids: Set.new,
|
|
65
|
+
trace_cache: nil)
|
|
61
66
|
@strategies = strategies
|
|
62
67
|
@block_range = block_range
|
|
63
68
|
@force_positional_for_font_ids = force_positional_for_font_ids
|
|
69
|
+
@trace_cache = trace_cache
|
|
64
70
|
end
|
|
65
71
|
|
|
66
72
|
# Convenience builder — wires up the default 3-strategy chain
|
|
@@ -71,7 +77,9 @@ module Ucode
|
|
|
71
77
|
# @param trace_cache [PageTraceCache, nil] when provided, the
|
|
72
78
|
# TraceStrategy shares this cache (lets the caller reuse the
|
|
73
79
|
# traced pages for downstream concerns like Catalog's
|
|
74
|
-
# location lookup)
|
|
80
|
+
# location lookup) AND the orchestrator consults it for
|
|
81
|
+
# gap detection (ToUnicode partial-coverage case). nil =
|
|
82
|
+
# construct internally.
|
|
75
83
|
# @return [CodepointMapper]
|
|
76
84
|
def self.build(source:, correlator_configs:, indexer:,
|
|
77
85
|
block_range: nil, force_positional_for_font_ids: Set.new,
|
|
@@ -93,7 +101,8 @@ module Ucode
|
|
|
93
101
|
]
|
|
94
102
|
new(strategies: strategies,
|
|
95
103
|
block_range: block_range,
|
|
96
|
-
force_positional_for_font_ids: force_positional_for_font_ids
|
|
104
|
+
force_positional_for_font_ids: force_positional_for_font_ids,
|
|
105
|
+
trace_cache: trace_cache)
|
|
97
106
|
end
|
|
98
107
|
|
|
99
108
|
# @param descriptor [RawFontDescriptor]
|
|
@@ -136,7 +145,7 @@ module Ucode
|
|
|
136
145
|
{}
|
|
137
146
|
end
|
|
138
147
|
|
|
139
|
-
# Positional strategies are gated behind
|
|
148
|
+
# Positional strategies are gated behind four conditions, any
|
|
140
149
|
# of which triggers them:
|
|
141
150
|
#
|
|
142
151
|
# 1. Caller explicitly listed this font in
|
|
@@ -146,10 +155,17 @@ module Ucode
|
|
|
146
155
|
# 3. The intrinsic result fell entirely outside the
|
|
147
156
|
# caller's block scope — the font's CMap encoded the
|
|
148
157
|
# wrong codepoints (Option 1 auto-detect, e.g. U1F200).
|
|
158
|
+
# 4. The intrinsic result covered fewer glyphs than the
|
|
159
|
+
# trace cache observes on the page — the font's CMap is
|
|
160
|
+
# partial (Unicode Consortium ships orphan glyphs whose
|
|
161
|
+
# CIDs the ToUnicode stream forgot to map, e.g. the
|
|
162
|
+
# last glyph of Mongolian Supplement U+1166C and Myanmar
|
|
163
|
+
# Extended-A U+116DA in the v17.0 charts).
|
|
149
164
|
def needs_positional?(descriptor, intrinsic_result)
|
|
150
165
|
return true if @force_positional_for_font_ids.include?(descriptor.font_obj_id)
|
|
151
166
|
return true if intrinsic_result.empty?
|
|
152
167
|
return true if intrinsic_out_of_scope?(intrinsic_result)
|
|
168
|
+
return true if intrinsic_has_uncovered_gids?(descriptor, intrinsic_result)
|
|
153
169
|
|
|
154
170
|
false
|
|
155
171
|
end
|
|
@@ -162,6 +178,21 @@ module Ucode
|
|
|
162
178
|
intrinsic_result.keys.all? { |cp| !@block_range.include?(cp) }
|
|
163
179
|
end
|
|
164
180
|
|
|
181
|
+
# Detects the partial-ToUnicode-coverage case: the font's
|
|
182
|
+
# CMap names N codepoints, but the page actually renders >N
|
|
183
|
+
# glyphs from this font (orphan glyphs whose CIDs the CMap
|
|
184
|
+
# forgot). Positional attribution via the chart's hex labels
|
|
185
|
+
# is the only way to recover them.
|
|
186
|
+
def intrinsic_has_uncovered_gids?(descriptor, intrinsic_result)
|
|
187
|
+
return false unless @trace_cache
|
|
188
|
+
return false if intrinsic_result.empty?
|
|
189
|
+
|
|
190
|
+
covered_gids = intrinsic_result.values.to_set
|
|
191
|
+
@trace_cache.distinct_gids_for(descriptor.base_font).any? do |gid|
|
|
192
|
+
!covered_gids.include?(gid)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
165
196
|
# Positional chain: union of all positional strategies' results.
|
|
166
197
|
# Within positional, earlier strategies win on conflict (chain
|
|
167
198
|
# order expresses caller preference — CorrelatorStrategy
|
|
@@ -4,6 +4,7 @@ require "pathname"
|
|
|
4
4
|
|
|
5
5
|
require "ucode/glyphs/embedded_fonts/mutool"
|
|
6
6
|
require "ucode/glyphs/embedded_fonts/trace_parser"
|
|
7
|
+
require "ucode/glyphs/embedded_fonts/trace_glyph"
|
|
7
8
|
|
|
8
9
|
module Ucode
|
|
9
10
|
module Glyphs
|
|
@@ -24,6 +25,15 @@ module Ucode
|
|
|
24
25
|
# Lazy: glyphs are only fetched when a strategy first asks for
|
|
25
26
|
# them. PDFs where every font has /ToUnicode never trigger the
|
|
26
27
|
# trace at all.
|
|
28
|
+
#
|
|
29
|
+
# ## mutool font-name truncation
|
|
30
|
+
#
|
|
31
|
+
# mutool trace emits font names truncated to 31 chars (PDF
|
|
32
|
+
# base-font-name limit). The BaseFont dict may carry the full
|
|
33
|
+
# original name (e.g. `HBBJCP+Uni11660Mongoliansupplement`).
|
|
34
|
+
# All name comparisons in this class go through
|
|
35
|
+
# {TraceGlyph.name_match?} so the truncation doesn't silently
|
|
36
|
+
# break lookups for long-named fonts.
|
|
27
37
|
class PageTraceCache
|
|
28
38
|
# @param pdf [Pathname, String]
|
|
29
39
|
# @param page_count [Integer] total pages in the PDF
|
|
@@ -55,7 +65,7 @@ module Ucode
|
|
|
55
65
|
glyphs_by_page.each_with_index do |glyphs, idx|
|
|
56
66
|
next if idx.zero?
|
|
57
67
|
|
|
58
|
-
present = glyphs.any? { |g| g.font_name
|
|
68
|
+
present = glyphs.any? { |g| TraceGlyph.name_match?(g.font_name, base_font) }
|
|
59
69
|
next unless present
|
|
60
70
|
|
|
61
71
|
present_in_any = true
|
|
@@ -68,8 +78,27 @@ module Ucode
|
|
|
68
78
|
# @return [Boolean] true if any page references this font
|
|
69
79
|
def references_font?(base_font)
|
|
70
80
|
glyphs_by_page.any? do |page_glyphs|
|
|
71
|
-
page_glyphs.any? { |g| g.font_name
|
|
81
|
+
page_glyphs.any? { |g| TraceGlyph.name_match?(g.font_name, base_font) }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Returns the set of distinct GIDs rendered on any page for
|
|
86
|
+
# the given font. Used by {CodepointMapper#needs_positional?}
|
|
87
|
+
# to detect the partial-ToUnicode-coverage case (font ships
|
|
88
|
+
# more glyphs than its CMap admits).
|
|
89
|
+
#
|
|
90
|
+
# @param base_font [String]
|
|
91
|
+
# @return [Set<Integer>]
|
|
92
|
+
def distinct_gids_for(base_font)
|
|
93
|
+
gids = Set.new
|
|
94
|
+
glyphs_by_page.each do |page_glyphs|
|
|
95
|
+
page_glyphs.each do |g|
|
|
96
|
+
next unless TraceGlyph.name_match?(g.font_name, base_font)
|
|
97
|
+
|
|
98
|
+
gids << g.gid if g.gid
|
|
99
|
+
end
|
|
72
100
|
end
|
|
101
|
+
gids
|
|
73
102
|
end
|
|
74
103
|
|
|
75
104
|
# Locate the first occurrence of a specific (font, gid) pair
|
|
@@ -87,7 +116,7 @@ module Ucode
|
|
|
87
116
|
next if idx.zero?
|
|
88
117
|
|
|
89
118
|
match = page_glyphs.find do |g|
|
|
90
|
-
g.font_name
|
|
119
|
+
TraceGlyph.name_match?(g.font_name, base_font) && g.gid == gid
|
|
91
120
|
end
|
|
92
121
|
return { page: idx, x: match.x, y: match.y } if match
|
|
93
122
|
end
|
|
@@ -48,7 +48,7 @@ module Ucode
|
|
|
48
48
|
private
|
|
49
49
|
|
|
50
50
|
def select_specimens(trace_glyphs)
|
|
51
|
-
trace_glyphs.select { |g| g.font_name
|
|
51
|
+
trace_glyphs.select { |g| TraceGlyph.name_match?(g.font_name, @specimen_font_name) }
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
def select_labels(trace_glyphs)
|
|
@@ -93,7 +93,7 @@ module Ucode
|
|
|
93
93
|
|
|
94
94
|
def select_hex_candidates(trace_glyphs)
|
|
95
95
|
trace_glyphs.select do |g|
|
|
96
|
-
g.font_name
|
|
96
|
+
!TraceGlyph.name_match?(g.font_name, @specimen_font_name) &&
|
|
97
97
|
g.unicode&.match?(/\A[0-9A-Fa-f]\z/)
|
|
98
98
|
end
|
|
99
99
|
end
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
module Ucode
|
|
4
4
|
module Glyphs
|
|
5
5
|
module EmbeddedFonts
|
|
6
|
+
# mutool trace emits font names truncated to 31 characters
|
|
7
|
+
# (PDF base-font-name limit, see PDF 32000-1 §7.9.6). The
|
|
8
|
+
# BaseFont dict, however, may carry the full original name.
|
|
9
|
+
# `HBBJCP+Uni11660Mongoliansupplement` (34 chars) is emitted
|
|
10
|
+
# by mutool trace as `HBBJCP+Uni11660Mongoliansupplem`. The
|
|
11
|
+
# helpers below let callers compare trace-side names against
|
|
12
|
+
# catalog-side names without tripping on the truncation.
|
|
13
|
+
TRACE_NAME_LIMIT = 31
|
|
14
|
+
private_constant :TRACE_NAME_LIMIT
|
|
15
|
+
|
|
6
16
|
# Value object for one glyph emitted by `mutool trace`.
|
|
7
17
|
#
|
|
8
18
|
# Each `<g>` element in the trace XML maps to one TraceGlyph:
|
|
@@ -21,7 +31,31 @@ module Ucode
|
|
|
21
31
|
:y,
|
|
22
32
|
:unicode,
|
|
23
33
|
keyword_init: true,
|
|
24
|
-
)
|
|
34
|
+
) do
|
|
35
|
+
class << self
|
|
36
|
+
# @param name [String, nil] a BaseFont name from `mutool info`
|
|
37
|
+
# or a trace-emitted font name
|
|
38
|
+
# @return [String, nil] the name truncated to the trace limit
|
|
39
|
+
def normalize_name(name)
|
|
40
|
+
return nil if name.nil?
|
|
41
|
+
|
|
42
|
+
name.length <= TRACE_NAME_LIMIT ? name : name[0, TRACE_NAME_LIMIT]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# True when `a` and `b` resolve to the same normalized
|
|
46
|
+
# name. Treats `nil` as never matching (avoids accidental
|
|
47
|
+
# collision on missing names).
|
|
48
|
+
#
|
|
49
|
+
# @param a [String, nil]
|
|
50
|
+
# @param b [String, nil]
|
|
51
|
+
# @return [Boolean]
|
|
52
|
+
def name_match?(a, b)
|
|
53
|
+
return false if a.nil? || b.nil?
|
|
54
|
+
|
|
55
|
+
normalize_name(a) == normalize_name(b)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
25
59
|
end
|
|
26
60
|
end
|
|
27
61
|
end
|
data/lib/ucode/version.rb
CHANGED