ucode 0.2.1 → 0.2.3
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/Gemfile +10 -2
- data/README.md +66 -20
- data/Rakefile +18 -7
- data/config/unicode17_universal_glyph_set.yml +1 -1
- data/lib/ucode/audit/reference_factory.rb +1 -1
- data/lib/ucode/cli.rb +10 -42
- data/lib/ucode/code_chart/extractor.rb +3 -5
- data/lib/ucode/code_chart/provenance.rb +6 -5
- data/lib/ucode/code_chart/sidecar.rb +2 -2
- data/lib/ucode/code_chart/writer.rb +1 -1
- data/lib/ucode/code_chart.rb +1 -1
- data/lib/ucode/commands/build.rb +3 -26
- data/lib/ucode/commands/canonical_build.rb +1 -4
- data/lib/ucode/commands/fetch.rb +1 -1
- data/lib/ucode/commands/lookup.rb +1 -1
- data/lib/ucode/commands/parse.rb +1 -1
- data/lib/ucode/commands.rb +0 -1
- data/lib/ucode/coordinator/indices.rb +2 -2
- data/lib/ucode/error.rb +0 -8
- data/lib/ucode/fetch/code_charts.rb +2 -3
- data/lib/ucode/fetch/http.rb +12 -14
- data/lib/ucode/glyphs/embedded_fonts/catalog.rb +81 -4
- data/lib/ucode/glyphs/embedded_fonts/trace_correlator.rb +230 -0
- data/lib/ucode/glyphs/embedded_fonts/trace_glyph.rb +27 -0
- data/lib/ucode/glyphs/embedded_fonts/trace_parser.rb +50 -0
- data/lib/ucode/glyphs/embedded_fonts/trace_runner.rb +53 -0
- data/lib/ucode/glyphs/embedded_fonts.rb +4 -0
- data/lib/ucode/glyphs/pdf_fetcher.rb +7 -50
- data/lib/ucode/glyphs.rb +4 -14
- data/lib/ucode/repo/aggregate_writer.rb +1 -1
- data/lib/ucode/repo/writers/blocks_writer.rb +13 -13
- data/lib/ucode/repo/writers/enums_writer.rb +2 -2
- data/lib/ucode/repo/writers/indexes_writer.rb +4 -4
- data/lib/ucode/repo/writers/manifest_writer.rb +4 -4
- data/lib/ucode/repo/writers/named_sequences_writer.rb +1 -1
- data/lib/ucode/repo/writers/planes_writer.rb +17 -17
- data/lib/ucode/repo/writers/relationships_writer.rb +1 -1
- data/lib/ucode/repo/writers/scripts_writer.rb +6 -6
- data/lib/ucode/repo/writers.rb +1 -1
- data/lib/ucode/version.rb +1 -1
- data/lib/ucode.rb +0 -2
- data/ucode.gemspec +6 -1
- metadata +10 -19
- data/lib/ucode/commands/glyphs.rb +0 -94
- data/lib/ucode/glyphs/cell_extractor.rb +0 -130
- data/lib/ucode/glyphs/dvisvgm_renderer.rb +0 -29
- data/lib/ucode/glyphs/grid.rb +0 -30
- data/lib/ucode/glyphs/grid_detector.rb +0 -165
- data/lib/ucode/glyphs/monolith_page_map.rb +0 -181
- data/lib/ucode/glyphs/mutool_renderer.rb +0 -28
- data/lib/ucode/glyphs/page_renderer.rb +0 -221
- data/lib/ucode/glyphs/path_bbox.rb +0 -62
- data/lib/ucode/glyphs/pdf2svg_renderer.rb +0 -26
- data/lib/ucode/glyphs/pdftocairo_renderer.rb +0 -32
- data/lib/ucode/glyphs/pipeline.rb +0 -106
- data/lib/ucode/glyphs/writer.rb +0 -250
|
@@ -34,11 +34,11 @@ module Ucode
|
|
|
34
34
|
def write
|
|
35
35
|
count = 0
|
|
36
36
|
count += 1 if write_atomic(Paths.names_index_path(@output_root),
|
|
37
|
-
|
|
37
|
+
to_pretty_json(@names))
|
|
38
38
|
count += 1 if write_atomic(Paths.labels_index_path(@output_root),
|
|
39
|
-
|
|
39
|
+
to_pretty_json(@labels))
|
|
40
40
|
count += 1 if write_atomic(codepoint_to_block_path,
|
|
41
|
-
|
|
41
|
+
to_pretty_json(@cp_to_block))
|
|
42
42
|
count
|
|
43
43
|
end
|
|
44
44
|
|
|
@@ -50,4 +50,4 @@ module Ucode
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
|
-
end
|
|
53
|
+
end
|
|
@@ -45,10 +45,10 @@ module Ucode
|
|
|
45
45
|
def write
|
|
46
46
|
path = Paths.manifest_path(@output_root)
|
|
47
47
|
content = {
|
|
48
|
-
"ucd_version"
|
|
48
|
+
"ucd_version" => @ucd_version,
|
|
49
49
|
"codepoint_count" => @codepoint_count,
|
|
50
|
-
"glyph_count"
|
|
51
|
-
"schema_version"
|
|
50
|
+
"glyph_count" => @glyph_count,
|
|
51
|
+
"schema_version" => SCHEMA_VERSION,
|
|
52
52
|
}
|
|
53
53
|
ts = preserved_or_new_timestamp(path, content)
|
|
54
54
|
payload = content.merge("generated_at" => ts)
|
|
@@ -75,4 +75,4 @@ module Ucode
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
|
-
end
|
|
78
|
+
end
|
|
@@ -16,16 +16,16 @@ module Ucode
|
|
|
16
16
|
# Static metadata for the 17 Unicode planes. Planes 4–13 are
|
|
17
17
|
# unassigned in Unicode 17; their entries use placeholder names.
|
|
18
18
|
PLANE_TABLE = {
|
|
19
|
-
0
|
|
20
|
-
1
|
|
21
|
-
2
|
|
22
|
-
3
|
|
23
|
-
4
|
|
24
|
-
5
|
|
25
|
-
6
|
|
26
|
-
7
|
|
27
|
-
8
|
|
28
|
-
9
|
|
19
|
+
0 => ["Basic Multilingual Plane", "BMP"],
|
|
20
|
+
1 => ["Supplementary Multilingual Plane", "SMP"],
|
|
21
|
+
2 => ["Supplementary Ideographic Plane", "SIP"],
|
|
22
|
+
3 => ["Tertiary Ideographic Plane", "TIP"],
|
|
23
|
+
4 => ["Unassigned Plane 4", "—"],
|
|
24
|
+
5 => ["Unassigned Plane 5", "—"],
|
|
25
|
+
6 => ["Unassigned Plane 6", "—"],
|
|
26
|
+
7 => ["Unassigned Plane 7", "—"],
|
|
27
|
+
8 => ["Unassigned Plane 8", "—"],
|
|
28
|
+
9 => ["Unassigned Plane 9", "—"],
|
|
29
29
|
10 => ["Unassigned Plane 10", "—"],
|
|
30
30
|
11 => ["Unassigned Plane 11", "—"],
|
|
31
31
|
12 => ["Unassigned Plane 12", "—"],
|
|
@@ -68,15 +68,15 @@ module Ucode
|
|
|
68
68
|
range_first = plane_number * 0x10000
|
|
69
69
|
range_last = range_first + 0xFFFF
|
|
70
70
|
to_pretty_json(
|
|
71
|
-
"number"
|
|
72
|
-
"name"
|
|
73
|
-
"abbrev"
|
|
74
|
-
"range_first"
|
|
75
|
-
"range_last"
|
|
76
|
-
"block_ids"
|
|
71
|
+
"number" => plane_number,
|
|
72
|
+
"name" => name,
|
|
73
|
+
"abbrev" => abbrev,
|
|
74
|
+
"range_first" => range_first,
|
|
75
|
+
"range_last" => range_last,
|
|
76
|
+
"block_ids" => block_ids,
|
|
77
77
|
)
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
|
-
end
|
|
82
|
+
end
|
|
@@ -41,14 +41,14 @@ module Ucode
|
|
|
41
41
|
|
|
42
42
|
def script_payload(code, ranges)
|
|
43
43
|
to_pretty_json(
|
|
44
|
-
"code"
|
|
45
|
-
"name"
|
|
46
|
-
"range_first"
|
|
47
|
-
"range_last"
|
|
48
|
-
"codepoint_ids"
|
|
44
|
+
"code" => code,
|
|
45
|
+
"name" => ranges.first&.name,
|
|
46
|
+
"range_first" => ranges.map(&:range_first).min,
|
|
47
|
+
"range_last" => ranges.map(&:range_last).max,
|
|
48
|
+
"codepoint_ids" => @script_codepoint_ids[code] || [],
|
|
49
49
|
)
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
|
-
end
|
|
54
|
+
end
|
data/lib/ucode/repo/writers.rb
CHANGED
data/lib/ucode/version.rb
CHANGED
data/lib/ucode.rb
CHANGED
|
@@ -32,8 +32,6 @@ module Ucode
|
|
|
32
32
|
autoload :UnknownVersionError, "ucode/error"
|
|
33
33
|
autoload :UnknownBlockError, "ucode/error"
|
|
34
34
|
autoload :GlyphError, "ucode/error"
|
|
35
|
-
autoload :PdfRenderError, "ucode/error"
|
|
36
|
-
autoload :GridDetectionError, "ucode/error"
|
|
37
35
|
autoload :LastResortMissingError, "ucode/error"
|
|
38
36
|
autoload :EmbeddedFontsMissingError, "ucode/error"
|
|
39
37
|
autoload :CodeChartNotFoundError, "ucode/error"
|
data/ucode.gemspec
CHANGED
|
@@ -45,7 +45,12 @@ Gem::Specification.new do |spec|
|
|
|
45
45
|
spec.require_paths = ["lib"]
|
|
46
46
|
|
|
47
47
|
spec.add_dependency "base64"
|
|
48
|
-
|
|
48
|
+
# Pin fontisan to 0.2.22 — 0.2.23+ removed
|
|
49
|
+
# `Fontisan::Commands::AuditCommand` (used by RealFonts::CoverageAuditor)
|
|
50
|
+
# and 0.4.x removed the entire Audit subsystem. Until
|
|
51
|
+
# CoverageAuditor is rewritten against the new fontisan API, this
|
|
52
|
+
# pin keeps the existing audit path working.
|
|
53
|
+
spec.add_dependency "fontisan", "= 0.2.22"
|
|
49
54
|
spec.add_dependency "fontist", "~> 3.0"
|
|
50
55
|
spec.add_dependency "logger"
|
|
51
56
|
spec.add_dependency "lutaml-model", "~> 0.8"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ucode
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -28,16 +28,16 @@ dependencies:
|
|
|
28
28
|
name: fontisan
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
|
-
- -
|
|
31
|
+
- - '='
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version:
|
|
33
|
+
version: 0.2.22
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
|
-
- -
|
|
38
|
+
- - '='
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version:
|
|
40
|
+
version: 0.2.22
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: fontist
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -321,7 +321,6 @@ files:
|
|
|
321
321
|
- lib/ucode/commands/canonical_build.rb
|
|
322
322
|
- lib/ucode/commands/fetch.rb
|
|
323
323
|
- lib/ucode/commands/font_coverage.rb
|
|
324
|
-
- lib/ucode/commands/glyphs.rb
|
|
325
324
|
- lib/ucode/commands/lookup.rb
|
|
326
325
|
- lib/ucode/commands/parse.rb
|
|
327
326
|
- lib/ucode/commands/release.rb
|
|
@@ -342,8 +341,6 @@ files:
|
|
|
342
341
|
- lib/ucode/fetch/ucd_zip.rb
|
|
343
342
|
- lib/ucode/fetch/unihan_zip.rb
|
|
344
343
|
- lib/ucode/glyphs.rb
|
|
345
|
-
- lib/ucode/glyphs/cell_extractor.rb
|
|
346
|
-
- lib/ucode/glyphs/dvisvgm_renderer.rb
|
|
347
344
|
- lib/ucode/glyphs/embedded_fonts.rb
|
|
348
345
|
- lib/ucode/glyphs/embedded_fonts/catalog.rb
|
|
349
346
|
- lib/ucode/glyphs/embedded_fonts/content_stream_correlator.rb
|
|
@@ -352,9 +349,11 @@ files:
|
|
|
352
349
|
- lib/ucode/glyphs/embedded_fonts/source.rb
|
|
353
350
|
- lib/ucode/glyphs/embedded_fonts/svg.rb
|
|
354
351
|
- lib/ucode/glyphs/embedded_fonts/tounicode.rb
|
|
352
|
+
- lib/ucode/glyphs/embedded_fonts/trace_correlator.rb
|
|
353
|
+
- lib/ucode/glyphs/embedded_fonts/trace_glyph.rb
|
|
354
|
+
- lib/ucode/glyphs/embedded_fonts/trace_parser.rb
|
|
355
|
+
- lib/ucode/glyphs/embedded_fonts/trace_runner.rb
|
|
355
356
|
- lib/ucode/glyphs/embedded_fonts/writer.rb
|
|
356
|
-
- lib/ucode/glyphs/grid.rb
|
|
357
|
-
- lib/ucode/glyphs/grid_detector.rb
|
|
358
357
|
- lib/ucode/glyphs/last_resort.rb
|
|
359
358
|
- lib/ucode/glyphs/last_resort/cmap_index.rb
|
|
360
359
|
- lib/ucode/glyphs/last_resort/contents.rb
|
|
@@ -363,14 +362,7 @@ files:
|
|
|
363
362
|
- lib/ucode/glyphs/last_resort/source.rb
|
|
364
363
|
- lib/ucode/glyphs/last_resort/svg.rb
|
|
365
364
|
- lib/ucode/glyphs/last_resort/writer.rb
|
|
366
|
-
- lib/ucode/glyphs/monolith_page_map.rb
|
|
367
|
-
- lib/ucode/glyphs/mutool_renderer.rb
|
|
368
|
-
- lib/ucode/glyphs/page_renderer.rb
|
|
369
|
-
- lib/ucode/glyphs/path_bbox.rb
|
|
370
|
-
- lib/ucode/glyphs/pdf2svg_renderer.rb
|
|
371
365
|
- lib/ucode/glyphs/pdf_fetcher.rb
|
|
372
|
-
- lib/ucode/glyphs/pdftocairo_renderer.rb
|
|
373
|
-
- lib/ucode/glyphs/pipeline.rb
|
|
374
366
|
- lib/ucode/glyphs/real_fonts.rb
|
|
375
367
|
- lib/ucode/glyphs/real_fonts/block_coverage.rb
|
|
376
368
|
- lib/ucode/glyphs/real_fonts/cmap_cache.rb
|
|
@@ -397,7 +389,6 @@ files:
|
|
|
397
389
|
- lib/ucode/glyphs/universal_set/manifest_writer.rb
|
|
398
390
|
- lib/ucode/glyphs/universal_set/pre_build_check.rb
|
|
399
391
|
- lib/ucode/glyphs/universal_set/validator.rb
|
|
400
|
-
- lib/ucode/glyphs/writer.rb
|
|
401
392
|
- lib/ucode/index.rb
|
|
402
393
|
- lib/ucode/index_builder.rb
|
|
403
394
|
- lib/ucode/models.rb
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "pathname"
|
|
4
|
-
|
|
5
|
-
require "ucode/glyphs"
|
|
6
|
-
|
|
7
|
-
module Ucode
|
|
8
|
-
module Commands
|
|
9
|
-
# `ucode glyphs` — extract per-codepoint SVGs from Code Charts PDFs.
|
|
10
|
-
# Thin Thor-facing wrapper around {Ucode::Glyphs::Pipeline}:
|
|
11
|
-
# opt-in gate + experimental warning live here; the pipeline
|
|
12
|
-
# assembly (block loading, fetcher, per-block specs) lives in
|
|
13
|
-
# {Ucode::Glyphs::Pipeline}.
|
|
14
|
-
#
|
|
15
|
-
# **Status (v0.1): EXPERIMENTAL.** The cell-extraction pipeline
|
|
16
|
-
# currently includes cell-border decorations alongside the actual
|
|
17
|
-
# character outline because the Code Charts PDFs composite the two
|
|
18
|
-
# into a single glyph definition. The output is therefore not yet
|
|
19
|
-
# suitable for end-user display. The command is retained so the
|
|
20
|
-
# pipeline can be iterated on without churning the CLI surface, but
|
|
21
|
-
# callers MUST opt in via `include_glyphs: true` (CLI: `--include-glyphs`)
|
|
22
|
-
# and will receive a printed warning. Tracked for v0.2.
|
|
23
|
-
#
|
|
24
|
-
# Takes a resolved version string; CLI callers resolve via
|
|
25
|
-
# {VersionResolver.resolve} once and thread it through. See
|
|
26
|
-
# Candidate 4 of the 2026-06-29 architecture review.
|
|
27
|
-
class GlyphsCommand
|
|
28
|
-
ExperimentalWarning = "ucode glyphs is experimental in v0.1: " \
|
|
29
|
-
"extracted SVGs include cell-border decorations " \
|
|
30
|
-
"alongside the character outline."
|
|
31
|
-
private_constant :ExperimentalWarning
|
|
32
|
-
|
|
33
|
-
class << self
|
|
34
|
-
# @return [String] the experimental-status banner. Exposed so the
|
|
35
|
-
# CLI and BuildCommand surface the same message verbatim.
|
|
36
|
-
def experimental_warning
|
|
37
|
-
ExperimentalWarning
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# @param version [String] resolved UCD version
|
|
42
|
-
# @param output_root [String, Pathname]
|
|
43
|
-
# @param block_filter [Array<String>, nil] block ids to limit to;
|
|
44
|
-
# nil = every block
|
|
45
|
-
# @param force [Boolean] re-fetch PDFs even when cached
|
|
46
|
-
# @param monolith_path [String, Pathname, nil] path to CodeCharts.pdf
|
|
47
|
-
# for fallback slicing; defaults to ./CodeCharts.pdf
|
|
48
|
-
# @param include_glyphs [Boolean] opt-in for the experimental v0.1
|
|
49
|
-
# pipeline. When false (default), the command returns a `skipped`
|
|
50
|
-
# payload without touching disk.
|
|
51
|
-
# @param warn [IO, nil] when provided, the experimental warning is
|
|
52
|
-
# written here exactly once before work begins.
|
|
53
|
-
# @return [Hash] aggregated Writer tally + version, or a `skipped`
|
|
54
|
-
# payload when opt-in is false.
|
|
55
|
-
def call(version, output_root:,
|
|
56
|
-
block_filter: nil, force: false,
|
|
57
|
-
monolith_path: Glyphs::Pipeline::DEFAULT_MONOLITH_PATH,
|
|
58
|
-
include_glyphs: false, warn: nil)
|
|
59
|
-
return skipped(version) unless include_glyphs
|
|
60
|
-
|
|
61
|
-
warn&.puts(ExperimentalWarning)
|
|
62
|
-
|
|
63
|
-
pipeline = Glyphs::Pipeline.new(
|
|
64
|
-
version: version,
|
|
65
|
-
block_filter: block_filter,
|
|
66
|
-
monolith_path: monolith_path,
|
|
67
|
-
)
|
|
68
|
-
specs = pipeline.build_specs(force: force)
|
|
69
|
-
|
|
70
|
-
writer = Glyphs::Writer.new(
|
|
71
|
-
output_root: Pathname.new(output_root),
|
|
72
|
-
parallel_workers: workers,
|
|
73
|
-
)
|
|
74
|
-
tally = writer.write_all(specs)
|
|
75
|
-
tally.merge(version: version, block_count: specs.size)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
private
|
|
79
|
-
|
|
80
|
-
def workers
|
|
81
|
-
Ucode.configuration.parallel_workers
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def skipped(version)
|
|
85
|
-
{
|
|
86
|
-
version: version,
|
|
87
|
-
skipped: true,
|
|
88
|
-
reason: :experimental_v0_1,
|
|
89
|
-
warning: ExperimentalWarning,
|
|
90
|
-
}
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
end
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "nokogiri"
|
|
4
|
-
|
|
5
|
-
require "ucode/glyphs/path_bbox"
|
|
6
|
-
|
|
7
|
-
module Ucode
|
|
8
|
-
module Glyphs
|
|
9
|
-
# Extracts a single character cell from a Code Charts SVG page and
|
|
10
|
-
# returns a normalized standalone SVG containing only that cell's
|
|
11
|
-
# vector paths.
|
|
12
|
-
#
|
|
13
|
-
# The cell is identified by codepoint. The extractor asks the Grid
|
|
14
|
-
# for the cell's anchor position, finds the `<use>` element placed
|
|
15
|
-
# at that position, resolves its glyph definition from `<defs>`,
|
|
16
|
-
# and emits a fresh `<svg>` whose viewBox is `0 0 1000 1000` and
|
|
17
|
-
# whose body is the glyph's `<path>` data translated and scaled to
|
|
18
|
-
# fit that viewBox with a small margin.
|
|
19
|
-
#
|
|
20
|
-
# Vector-only. Never rasterizes, never OCRs. If the cell is empty
|
|
21
|
-
# (no character glyph placed there, e.g. unassigned codepoint or
|
|
22
|
-
# control character), the extractor returns nil.
|
|
23
|
-
class CellExtractor
|
|
24
|
-
ViewBoxSize = 1000.0
|
|
25
|
-
MarginRatio = 0.1
|
|
26
|
-
private_constant :ViewBoxSize, :MarginRatio
|
|
27
|
-
|
|
28
|
-
# @param doc [Nokogiri::XML::Document] the rendered Code Charts page
|
|
29
|
-
def initialize(doc)
|
|
30
|
-
@doc = doc
|
|
31
|
-
@glyph_cache = {}
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# @param grid [Ucode::Glyphs::Grid]
|
|
35
|
-
# @param codepoint [Integer]
|
|
36
|
-
# @return [Nokogiri::XML::Document, nil] a standalone `<svg>` doc
|
|
37
|
-
# with viewBox `0 0 1000 1000`, or nil if the cell is empty.
|
|
38
|
-
def extract(grid, codepoint)
|
|
39
|
-
anchor = grid.cell_position(codepoint)
|
|
40
|
-
return nil unless anchor
|
|
41
|
-
|
|
42
|
-
use_node = find_use_at(anchor, grid)
|
|
43
|
-
return nil unless use_node
|
|
44
|
-
|
|
45
|
-
path_data = collect_paths(use_node["xlink:href"] || use_node["href"])
|
|
46
|
-
return nil if path_data.empty?
|
|
47
|
-
|
|
48
|
-
bbox = PathBbox.estimate(path_data.join(" "))
|
|
49
|
-
return nil if bbox.empty?
|
|
50
|
-
|
|
51
|
-
build_svg(path_data, bbox, use_node["x"].to_f, use_node["y"].to_f)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
def find_use_at(anchor, grid)
|
|
57
|
-
tolerance_x = grid.column_pitch / 2
|
|
58
|
-
tolerance_y = grid.row_pitch / 2
|
|
59
|
-
|
|
60
|
-
candidates = @doc.css("use").select do |node|
|
|
61
|
-
href = node["xlink:href"] || node["href"] || ""
|
|
62
|
-
href.start_with?("#glyph-") &&
|
|
63
|
-
(node["x"].to_f - anchor[0]).abs <= tolerance_x &&
|
|
64
|
-
(node["y"].to_f - anchor[1]).abs <= tolerance_y
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
candidates.min_by do |node|
|
|
68
|
-
dx = node["x"].to_f - anchor[0]
|
|
69
|
-
dy = node["y"].to_f - anchor[1]
|
|
70
|
-
(dx * dx) + (dy * dy)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def collect_paths(href)
|
|
75
|
-
return [] unless href
|
|
76
|
-
|
|
77
|
-
glyph_id = href.sub(/\A#/, "")
|
|
78
|
-
node = glyph_definition(glyph_id)
|
|
79
|
-
return [] unless node
|
|
80
|
-
|
|
81
|
-
node.css("path").map { |p| p["d"] }.compact
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def glyph_definition(glyph_id)
|
|
85
|
-
return @glyph_cache[glyph_id] if @glyph_cache.key?(glyph_id)
|
|
86
|
-
|
|
87
|
-
@glyph_cache[glyph_id] = @doc.at_css("defs ##{glyph_id}")
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def build_svg(path_data, glyph_bbox, place_x, place_y)
|
|
91
|
-
placed = PathBbox::Result.new(
|
|
92
|
-
min_x: place_x + glyph_bbox.min_x,
|
|
93
|
-
min_y: place_y + glyph_bbox.min_y,
|
|
94
|
-
max_x: place_x + glyph_bbox.max_x,
|
|
95
|
-
max_y: place_y + glyph_bbox.max_y,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
width = placed.width
|
|
99
|
-
height = placed.height
|
|
100
|
-
return nil if width <= 0 || height <= 0
|
|
101
|
-
|
|
102
|
-
content_size = ViewBoxSize * (1.0 - (2.0 * MarginRatio))
|
|
103
|
-
scale = [content_size / width, content_size / height].min
|
|
104
|
-
offset_x = (ViewBoxSize - (width * scale)) / 2.0
|
|
105
|
-
offset_y = (ViewBoxSize - (height * scale)) / 2.0
|
|
106
|
-
translate_x = offset_x - (placed.min_x * scale)
|
|
107
|
-
translate_y = offset_y - (placed.min_y * scale)
|
|
108
|
-
|
|
109
|
-
builder = Nokogiri::XML::Document.new
|
|
110
|
-
root = builder.create_element(
|
|
111
|
-
"svg",
|
|
112
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
113
|
-
viewBox: "0 0 #{ViewBoxSize.to_i} #{ViewBoxSize.to_i}",
|
|
114
|
-
width: ViewBoxSize.to_i,
|
|
115
|
-
height: ViewBoxSize.to_i,
|
|
116
|
-
)
|
|
117
|
-
group = builder.create_element(
|
|
118
|
-
"g",
|
|
119
|
-
transform: "scale(#{format('%.6f', scale)}) translate(#{format('%.6f', translate_x)}, #{format('%.6f', translate_y)})",
|
|
120
|
-
)
|
|
121
|
-
path_data.each do |d|
|
|
122
|
-
group.add_child(builder.create_element("path", d: d, fill: "black"))
|
|
123
|
-
end
|
|
124
|
-
root.add_child(group)
|
|
125
|
-
builder.add_child(root)
|
|
126
|
-
builder
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "ucode/glyphs/page_renderer"
|
|
4
|
-
|
|
5
|
-
module Ucode
|
|
6
|
-
module Glyphs
|
|
7
|
-
# `dvisvgm` — originally a DVI-to-SVG converter, also handles PDF.
|
|
8
|
-
# The `--no-fonts` flag forces outline-only output (no font subsetting
|
|
9
|
-
# artifacts), which is what we want for vector glyph extraction.
|
|
10
|
-
#
|
|
11
|
-
# Command: `dvisvgm --pdf --no-fonts --page=<n> <in.pdf> -o <out.svg>`
|
|
12
|
-
class DvisvgmRenderer < PageRenderer
|
|
13
|
-
class << self
|
|
14
|
-
def renderer_name
|
|
15
|
-
:dvisvgm
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def binary_name
|
|
19
|
-
:dvisvgm
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def build_command(pdf_path, page_num, out_path)
|
|
23
|
-
["dvisvgm", "--pdf", "--no-fonts", "--page=#{page_num}",
|
|
24
|
-
pdf_path.to_s, "-o", out_path.to_s]
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
data/lib/ucode/glyphs/grid.rb
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ucode
|
|
4
|
-
module Glyphs
|
|
5
|
-
Grid = Struct.new(
|
|
6
|
-
:origin_x, :origin_y,
|
|
7
|
-
:column_pitch, :row_pitch,
|
|
8
|
-
:columns, :rows,
|
|
9
|
-
:block_first_cp,
|
|
10
|
-
keyword_init: true,
|
|
11
|
-
) do
|
|
12
|
-
def cell_position(codepoint)
|
|
13
|
-
offset = codepoint - block_first_cp
|
|
14
|
-
return nil if offset.negative?
|
|
15
|
-
|
|
16
|
-
row, col = offset.divmod(columns)
|
|
17
|
-
return nil if row >= rows
|
|
18
|
-
|
|
19
|
-
[origin_x + (col * column_pitch), origin_y + (row * row_pitch)]
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def codepoint_at(row, col)
|
|
23
|
-
return nil if row.negative? || row >= rows
|
|
24
|
-
return nil if col.negative? || col >= columns
|
|
25
|
-
|
|
26
|
-
block_first_cp + (row * columns) + col
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|