pixel_font_trie_ocr 0.1.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 +7 -0
- data/AGENTS.md +72 -0
- data/CHANGELOG.md +6 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +151 -0
- data/Rakefile +13 -0
- data/lib/pixel_font_trie_ocr/font_metadata.rb +110 -0
- data/lib/pixel_font_trie_ocr/fonts/hex-synergy_font.ttf +0 -0
- data/lib/pixel_font_trie_ocr/image_column_extractor.rb +49 -0
- data/lib/pixel_font_trie_ocr/image_utils.rb +63 -0
- data/lib/pixel_font_trie_ocr/methods.rb +13 -0
- data/lib/pixel_font_trie_ocr/parsing.rb +34 -0
- data/lib/pixel_font_trie_ocr/trie/node.rb +27 -0
- data/lib/pixel_font_trie_ocr/trie.rb +50 -0
- data/lib/pixel_font_trie_ocr/version.rb +5 -0
- data/lib/pixel_font_trie_ocr.rb +18 -0
- data/lib/tasks/font_map.rake +17 -0
- data/sig/pixel_font_trie_ocr.rbs +4 -0
- metadata +92 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3ef0ba96a8d1f2d871122d57bff74f315c07e75367ee1d823462ae534c7018ee
|
|
4
|
+
data.tar.gz: a146081f4bc5feb0b930eec43287a2dd7ded02ee62d52c5dac2f7242753a6b08
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7b76df9489ccf4e0ce2a89e10a574d08fc37e76f8f07f820553439f9467c3086a456298f66ec2cc7b6ab5169ddd7cf746549a57242e54e3eab493ecc68a2ea6d
|
|
7
|
+
data.tar.gz: 443df30b953382a1cef4af8abf3d4e592b30583bc7b852d9a307c7dcf741c57dfbc98908de9e20541c02b9e067bb156f3675e340db1b2a9b32a67ce167f4cae8
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# AGENTS.md – PixelFontTrieOCR
|
|
2
|
+
|
|
3
|
+
**IMPORTANT: Think and Communicate with the user ONLY in American English. Do NOT use any oriental glyphs or non-English characters.**
|
|
4
|
+
You are pair programming with the user.
|
|
5
|
+
If you make changes, the user will review.
|
|
6
|
+
If the user makes changes, they will ask you to review.
|
|
7
|
+
|
|
8
|
+
## Project Overview
|
|
9
|
+
Deterministic OCR for tiny (5–8px) crystal-clear pixel fonts using a multi-way Trie keyed by 8-bit column bitmasks (black=1, white=0). Replaces noisy general OCR (Tesseract) with perfect accuracy, early pruning, and microsecond performance on exact fonts.
|
|
10
|
+
|
|
11
|
+
**Gem name:** pixel_font_trie_ocr
|
|
12
|
+
**Module:** `PixelFontTrieOCR`
|
|
13
|
+
**Core tech:** RMagick for image handling, RSpec + RuboCop for testing/linting.
|
|
14
|
+
|
|
15
|
+
## Code Style & Conventions
|
|
16
|
+
- **No inline comments after code** in source files. Comments before methods, classes or modules following rdoc conventions.
|
|
17
|
+
- Heavy memoization: `@var ||= ...` for `img`, `width`, `image`, `draw`, etc.
|
|
18
|
+
- Short, single-responsibility methods with public accessors (`width`, `image`, `extract`).
|
|
19
|
+
- Seperation of concerns, is important. Don't many any one method, module or class do too much.. Break it down.
|
|
20
|
+
- Mimic existing patterns from neighboring files (see `lib/pixel_font_trie_ocr/*.rb`).
|
|
21
|
+
- Use in-memory `Magick::Image` objects wherever possible (avoid temp files in tests).
|
|
22
|
+
- Follow RubyGem structure; update gemspec on name/module changes.
|
|
23
|
+
- **NEVER** introduce secrets, assume libraries only if already in Gemfile/gemspec.
|
|
24
|
+
- If a gem library would be useful for a task, ask the user about adding the gem.
|
|
25
|
+
|
|
26
|
+
## Important Commands
|
|
27
|
+
- Full test + lint: `bundle exec rake` (default task: spec + rubocop)
|
|
28
|
+
- Run specs only: `bundle exec rspec`
|
|
29
|
+
- Console: `bin/console`
|
|
30
|
+
- Build gem: `bundle exec rake build`
|
|
31
|
+
|
|
32
|
+
**Always run `bundle exec rake` after changes. If there are errors, go into plan mode, and ask the user before making changes.**
|
|
33
|
+
If the specs pass, then have the user review the changes before making a commit.
|
|
34
|
+
Make rubocop fixes either automatic or manual in a seperate commit.
|
|
35
|
+
**never make changes on the master branch, Ask the user to create a working branch before making changes**
|
|
36
|
+
use descriptive commit messages to describe the changes made.
|
|
37
|
+
If there are changes that you don't recognize, they may be from the user, ask if they should be included.
|
|
38
|
+
Preferentially write specs before writing code, use a red green refactor process. red for code that fails the spec, green for getting the code to pass the spec, refactor for improving the code after it passes.
|
|
39
|
+
|
|
40
|
+
## Key Components
|
|
41
|
+
- `ImageColumnExtractor`: Path or `Magick::Image` → array of column masks (8-bit ints). Supports `height_limit`, `threshold`.
|
|
42
|
+
- `ColumnImageBuilder`: Masks + height → test image (`#image`, `#write`).
|
|
43
|
+
- `TextImageBuilder`: Text + TTF font → clean pixel image (`#image`, `#write`, memoized metrics).
|
|
44
|
+
- `PixelFontTrie`:
|
|
45
|
+
- `insert(columns, char)`
|
|
46
|
+
- `recognize(columns)` (handles whitespace columns, variable-width glyphs)
|
|
47
|
+
- `PixelFontTrie.from_font(font_path, characters: "...", font_size: 8)` (builds from TrueType, trims whitespace).
|
|
48
|
+
|
|
49
|
+
## Testing Approach
|
|
50
|
+
- RSpec in `spec/`.
|
|
51
|
+
- Use `ColumnImageBuilder` to generate fixtures (e.g. exhaustive 256-pattern column test).
|
|
52
|
+
- Prefer in-memory tests over real image files.
|
|
53
|
+
- spec files should match the source files with the change from /lib to /spec and the addition of _spec for the spec files.
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
```ruby
|
|
57
|
+
trie = PixelFontTrieOCR::PixelFontTrie.from_font("font.ttf", characters: "ABC123!?")
|
|
58
|
+
masks = PixelFontTrieOCR::ImageColumnExtractor.new(image).extract
|
|
59
|
+
puts trie.recognize(masks)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Rake Tasks
|
|
63
|
+
- `bundle exec rake generate_glyphs` – Generate glyph images from font (default: `fonts/hex-synergy_font.ttf` → `tmp/glyphs/`).
|
|
64
|
+
- Env vars: `FONT_PATH`, `OUTPUT_DIR`, `FONT_SIZE`, `HEIGHT`.
|
|
65
|
+
- Uses `PixelFontTrieOCR::GlyphImageGenerator`.
|
|
66
|
+
|
|
67
|
+
## Next Steps (from project recap)
|
|
68
|
+
- Full recognition pipeline on real samples.
|
|
69
|
+
- CLI tool (`bin/pixel-font-trie-ocr`).
|
|
70
|
+
- Polish, release as gem.
|
|
71
|
+
|
|
72
|
+
Update this file when adding new commands, conventions, or architecture changes.
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"pixel_font_trie_ocr" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["rob@ferney.org"](mailto:"rob@ferney.org").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Robert Ferney
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# PixelFontTrieOCR
|
|
2
|
+
|
|
3
|
+
**Deterministic OCR for tiny (5–8px) crystal-clear pixel fonts using a multi-way Trie keyed by 8-bit column bitmasks.**
|
|
4
|
+
|
|
5
|
+
Replaces noisy general OCR (e.g., Tesseract) with perfect accuracy, early pruning, and microsecond performance on exact fonts.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'pixel_font_trie_ocr'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
$ bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install it yourself as:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
$ gem install pixel_font_trie_ocr
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Dependencies**: Requires RMagick for image handling and TTFunk for font parsing.
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
require 'pixel_font_trie_ocr'
|
|
33
|
+
|
|
34
|
+
ocr = PixelFontTrieOCR.new
|
|
35
|
+
ocr.font_name = 'hex-synergy_font.ttf' # Default font
|
|
36
|
+
ocr.font_size = 8 # Default size
|
|
37
|
+
|
|
38
|
+
# Render text to image
|
|
39
|
+
img = ocr.text_image('Hello World')
|
|
40
|
+
|
|
41
|
+
# Extract column bitmasks
|
|
42
|
+
masks = ocr.bitmask(img)
|
|
43
|
+
|
|
44
|
+
# Recognize text
|
|
45
|
+
text = ocr.parse_mask(masks)
|
|
46
|
+
puts text # => "Hello World"
|
|
47
|
+
|
|
48
|
+
# Or directly parse an image
|
|
49
|
+
text = ocr.parse_image(img)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Advanced Usage
|
|
53
|
+
|
|
54
|
+
### Building and Using the Trie
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
ocr = PixelFontTrieOCR.new
|
|
58
|
+
|
|
59
|
+
# Generate character masks from font (memoized)
|
|
60
|
+
char_masks = ocr.char_masks # { 'A' => [mask1, mask2, ...], ... }
|
|
61
|
+
|
|
62
|
+
# Build trie
|
|
63
|
+
trie = ocr.trie # Or PixelFontTrieOCR::Trie.new(char_masks)
|
|
64
|
+
|
|
65
|
+
# Insert custom character
|
|
66
|
+
trie.insert('!', [0b101, 0b010])
|
|
67
|
+
|
|
68
|
+
# Parse masks
|
|
69
|
+
masks = [0b101, 0b010, 0] # Example masks with trailing zero
|
|
70
|
+
text = trie.parse(masks) # Handles variable-width and whitespace
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Image Manipulation
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
# Create image from masks
|
|
77
|
+
mask_img = ocr.mask_image(masks)
|
|
78
|
+
|
|
79
|
+
# Write text image to file
|
|
80
|
+
ocr.write_text_image('Test', 'test.png')
|
|
81
|
+
|
|
82
|
+
# Bitmask utilities
|
|
83
|
+
bitmask = ocr.array_to_bitmask([1, 0, 1]) # => 5
|
|
84
|
+
bits = ocr.bitmask_to_array(5) # => [1, 0, 1]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Custom Font Configuration
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
ocr.font_dir = '/path/to/fonts'
|
|
91
|
+
ocr.font_name = 'custom_font.ttf'
|
|
92
|
+
ocr.font_size = 6
|
|
93
|
+
|
|
94
|
+
puts ocr.height # Calculated pixel height
|
|
95
|
+
puts ocr.characters.to_a # Supported characters
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Extracting Masks from Existing Image
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
img = Magick::Image.read('path/to/image.png').first
|
|
102
|
+
extractor = PixelFontTrieOCR::ImageColumnExtractor.new(img, threshold: 50000)
|
|
103
|
+
masks = extractor.extract
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Features
|
|
107
|
+
|
|
108
|
+
- **Deterministic Recognition**: 100% accuracy for exact pixel matches; no ML heuristics.
|
|
109
|
+
- **Fast**: Microsecond per character; early trie pruning.
|
|
110
|
+
- **Variable-Width Support**: Handles fonts with varying glyph widths.
|
|
111
|
+
- **Memoization**: Heavy use for performance (e.g., font loading, mask generation).
|
|
112
|
+
- **Utilities**: Image rendering, bitmask conversion, font metadata extraction.
|
|
113
|
+
- **Customization**: Adjustable threshold for black/white detection in images.
|
|
114
|
+
|
|
115
|
+
## Core Components
|
|
116
|
+
|
|
117
|
+
| Component | Purpose |
|
|
118
|
+
|----------------------------|---------|
|
|
119
|
+
| `PixelFontTrieOCR` | Main class including all modules; entry point for usage. |
|
|
120
|
+
| `Trie` | Core trie for inserting masks and parsing text. |
|
|
121
|
+
| `Trie::Node` | Trie nodes with recursive matching. |
|
|
122
|
+
| `ImageColumnExtractor` | Converts images to bitmask arrays. |
|
|
123
|
+
| `FontMetadata` (module) | Font loading and metadata (characters, ascent, etc.). |
|
|
124
|
+
| `ImageUtils` (module) | Image creation and writing (text_image, mask_image). |
|
|
125
|
+
| `Parsing` (module) | Builds trie and performs recognition (parse_image). |
|
|
126
|
+
| `Methods` (module) | Bitmask utilities (array_to_bitmask). |
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests.
|
|
131
|
+
|
|
132
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
133
|
+
|
|
134
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
135
|
+
|
|
136
|
+
### Rake Tasks
|
|
137
|
+
|
|
138
|
+
- `rake spec`: Run RSpec tests.
|
|
139
|
+
- `rake rubocop`: Run RuboCop linter.
|
|
140
|
+
- `rake font_map`: Generate glyph images and YAML mask map to `/tmp/`.
|
|
141
|
+
- `rake build`: Build the gem.
|
|
142
|
+
- `rake install`: Install locally.
|
|
143
|
+
- `rake release`: Tag and publish to RubyGems.
|
|
144
|
+
|
|
145
|
+
## Contributing
|
|
146
|
+
|
|
147
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[YOUR_USERNAME]/pixel_font_trie_ocr. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[YOUR_USERNAME]/pixel_font_trie_ocr/blob/master/CODE_OF_CONDUCT.md).
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
require "rubocop/rake_task"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require_relative "lib/pixel_font_trie_ocr"
|
|
8
|
+
|
|
9
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
10
|
+
RuboCop::RakeTask.new
|
|
11
|
+
Rake.add_rakelib(File.join(__dir__, "lib", "tasks"))
|
|
12
|
+
|
|
13
|
+
task default: %i[spec rubocop]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ttfunk"
|
|
4
|
+
|
|
5
|
+
class PixelFontTrieOCR
|
|
6
|
+
module FontMetadata
|
|
7
|
+
DEFAULT_FONT_NAME = "hex-synergy_font.ttf"
|
|
8
|
+
attr_writer :font_name, :font_size
|
|
9
|
+
|
|
10
|
+
def font_name
|
|
11
|
+
@font_name ||= DEFAULT_FONT_NAME
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def font_dir=(value)
|
|
15
|
+
@font_dir = Pathname.new(value)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def font_dir
|
|
19
|
+
@font_dir ||= Pathname.new(__dir__).join("fonts")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def font_path
|
|
23
|
+
@font_path ||= font_dir.join(font_name).to_s
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def font_size
|
|
27
|
+
@font_size ||= 8
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def font
|
|
31
|
+
@font ||= TTFunk::File.open(font_path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def font_map
|
|
35
|
+
@font_map ||= font.cmap.unicode.first
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def code_map
|
|
39
|
+
@code_map ||= font_map.respond_to?(:code_map) ? font_map.code_map : {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def characters
|
|
43
|
+
@characters ||= Set.new(
|
|
44
|
+
code_map.filter_map do |key, value|
|
|
45
|
+
value.positive? && key > 31 && [key].pack("U")
|
|
46
|
+
end
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def family
|
|
51
|
+
@family ||= font.name.font_name
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def subfamily
|
|
55
|
+
@subfamily ||= font.name.font_subfamily
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def postscript_name
|
|
59
|
+
@postscript_name ||= font.name.postscript_name
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def character_count
|
|
63
|
+
@character_count ||= characters.size
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def ascent
|
|
67
|
+
@ascent ||= font.os2.ascent
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def descent
|
|
71
|
+
@descent ||= font.os2.descent
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def units_per_em
|
|
75
|
+
@units_per_em ||= font.header.units_per_em
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ascent_ratio
|
|
79
|
+
@ascent_ratio ||= ascent / units_per_em.to_f
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def height
|
|
83
|
+
@height ||= (ascent_ratio * font_size).ceil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def uppercase
|
|
87
|
+
@uppercase ||= Set.new("A".."Z")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def lowercase
|
|
91
|
+
@lowercase ||= Set.new("a".."z")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def digits
|
|
95
|
+
@digits ||= Set.new("0".."9")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def alphanumeric
|
|
99
|
+
@alphanumeric ||= uppercase | lowercase | digits
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def whitespace
|
|
103
|
+
Set.new([" ", "\r", "\n", "\t"])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def symbols
|
|
107
|
+
@symbols ||= characters - alphanumeric - whitespace
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
Binary file
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PixelFontTrieOCR
|
|
4
|
+
class ImageColumnExtractor
|
|
5
|
+
attr_reader :image, :threshold
|
|
6
|
+
|
|
7
|
+
def initialize(image, threshold: 38_000)
|
|
8
|
+
@image = image
|
|
9
|
+
@threshold = threshold
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def extract
|
|
13
|
+
trim_masks(masks)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def width
|
|
19
|
+
@width ||= image.columns
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def height
|
|
23
|
+
@height ||= image.rows
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def get_column(col)
|
|
27
|
+
pixels = image.get_pixels(col, 0, 1, height).reverse
|
|
28
|
+
pixels.map.with_index do |p, row|
|
|
29
|
+
black?(p) ? (1 << row) : 0
|
|
30
|
+
end.sum
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def black?(pixel)
|
|
34
|
+
(pixel.red + pixel.green + pixel.blue) < threshold
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def masks
|
|
38
|
+
(0...width).map { |col| get_column(col) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def trim_masks(masks)
|
|
42
|
+
first_non_zero = masks.find_index(&:positive?)
|
|
43
|
+
return [0, 0] unless first_non_zero
|
|
44
|
+
|
|
45
|
+
last_non_zero = masks.rindex(&:positive?)
|
|
46
|
+
masks[first_non_zero..last_non_zero] + [0]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PixelFontTrieOCR
|
|
4
|
+
module ImageUtils
|
|
5
|
+
def temp_dir=(value)
|
|
6
|
+
@temp_dir = Pathname.new(value)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def temp_dir
|
|
10
|
+
@temp_dir ||= Pathname.new(__dir__).join("..", "..", "tmp")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def temp_file(name)
|
|
14
|
+
temp_dir.join(name).to_s
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def new_image(width)
|
|
18
|
+
Magick::Image.new(width, height) do |img|
|
|
19
|
+
img.background_color = "white"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def new_draw
|
|
24
|
+
draw = Magick::Draw.new
|
|
25
|
+
draw.font = font_path
|
|
26
|
+
draw.pointsize = font_size
|
|
27
|
+
draw.fill = "black"
|
|
28
|
+
draw
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def text_image(text, pad: 0)
|
|
32
|
+
draw = new_draw
|
|
33
|
+
metrics = draw.get_type_metrics(text)
|
|
34
|
+
width = metrics.width + pad
|
|
35
|
+
width = 2 if width.zero?
|
|
36
|
+
puts "drawing #{text.inspect} width: #{width}" if width < 1
|
|
37
|
+
image = new_image(width.ceil)
|
|
38
|
+
draw.text(0, metrics.ascent.ceil, text).draw(image)
|
|
39
|
+
image
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def write_text_image(text, name, pad: 0)
|
|
43
|
+
image = text_image(text, pad: pad)
|
|
44
|
+
image.write(temp_file(name))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def bitmask(image)
|
|
48
|
+
ImageColumnExtractor.new(image).extract
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def mask_image(mask_columns)
|
|
52
|
+
draw = new_draw
|
|
53
|
+
image = new_image(mask_columns.length)
|
|
54
|
+
mask_columns.each_with_index do |mask, col|
|
|
55
|
+
bitmask_to_array(mask).each_with_index do |bit, row|
|
|
56
|
+
draw.point(col, height - row - 1) if bit.positive?
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
draw.draw(image)
|
|
60
|
+
image
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PixelFontTrieOCR
|
|
4
|
+
module Methods
|
|
5
|
+
def array_to_bitmask(array)
|
|
6
|
+
array.inject(0) { |acc, bit| (acc << 1) | bit }
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def bitmask_to_array(bitmask, length: nil)
|
|
10
|
+
Array.new(length || bitmask.bit_length) { |i| (bitmask >> i) & 1 }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "trie"
|
|
4
|
+
|
|
5
|
+
class PixelFontTrieOCR
|
|
6
|
+
module Parsing
|
|
7
|
+
def character_images
|
|
8
|
+
@char_masks = {}
|
|
9
|
+
characters.map.with_index do |char, index|
|
|
10
|
+
image = text_image(char)
|
|
11
|
+
mask = bitmask(image)
|
|
12
|
+
yield(char, image, mask, index) if block_given?
|
|
13
|
+
@char_masks[char] = mask
|
|
14
|
+
end
|
|
15
|
+
@char_masks
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def char_masks
|
|
19
|
+
@char_masks || character_images
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def trie
|
|
23
|
+
@trie ||= Trie.new(char_masks)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse_mask(columns)
|
|
27
|
+
trie.parse(columns)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse_image(img)
|
|
31
|
+
parse_mask(bitmask(img))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class PixelFontTrieOCR
|
|
4
|
+
class Trie
|
|
5
|
+
class Node
|
|
6
|
+
attr_accessor :children, :character
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@children = {}
|
|
10
|
+
@character = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def leaf?
|
|
14
|
+
children.empty?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def match(columns, index)
|
|
18
|
+
if index < columns.length
|
|
19
|
+
child_char, child_idx = children[columns[index]]&.match(columns, index + 1)
|
|
20
|
+
return [child_char, child_idx] if child_char
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
[character, index]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lib/pixel_font_trie_ocr/trie.rb
|
|
4
|
+
require_relative "trie/node"
|
|
5
|
+
|
|
6
|
+
class PixelFontTrieOCR
|
|
7
|
+
class Trie
|
|
8
|
+
def initialize(char_masks = {})
|
|
9
|
+
insert_hash(char_masks)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def root
|
|
13
|
+
@root ||= Node.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def insert_hash(hash)
|
|
17
|
+
hash.each_pair do |character, columns|
|
|
18
|
+
insert(character, columns)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def insert(character, columns)
|
|
23
|
+
node = root
|
|
24
|
+
columns.each do |mask|
|
|
25
|
+
node.children[mask] ||= Node.new
|
|
26
|
+
node = node.children[mask]
|
|
27
|
+
end
|
|
28
|
+
node.character ||= character
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def parse(columns)
|
|
32
|
+
columns = columns.dup
|
|
33
|
+
columns << 0 unless columns.last.zero?
|
|
34
|
+
match(columns)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
protected
|
|
38
|
+
|
|
39
|
+
def match(columns, pos = 0)
|
|
40
|
+
return "" unless pos < columns.length
|
|
41
|
+
|
|
42
|
+
matched, new_pos = root.match(columns, pos)
|
|
43
|
+
if matched
|
|
44
|
+
matched + match(columns, new_pos)
|
|
45
|
+
else
|
|
46
|
+
match(columns, pos + 1)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rmagick"
|
|
4
|
+
require "ttfunk"
|
|
5
|
+
require_relative "pixel_font_trie_ocr/version"
|
|
6
|
+
require_relative "pixel_font_trie_ocr/methods"
|
|
7
|
+
require_relative "pixel_font_trie_ocr/font_metadata"
|
|
8
|
+
require_relative "pixel_font_trie_ocr/image_utils"
|
|
9
|
+
require_relative "pixel_font_trie_ocr/parsing"
|
|
10
|
+
require_relative "pixel_font_trie_ocr/image_column_extractor"
|
|
11
|
+
|
|
12
|
+
class PixelFontTrieOCR
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
include Methods
|
|
15
|
+
include FontMetadata
|
|
16
|
+
include ImageUtils
|
|
17
|
+
include Parsing
|
|
18
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
desc "Generate font glyph images and bitmask map to tmp/font/"
|
|
4
|
+
task :font_map do
|
|
5
|
+
require "yaml"
|
|
6
|
+
pft = PixelFontTrieOCR.new
|
|
7
|
+
pft.temp_dir.mkpath
|
|
8
|
+
|
|
9
|
+
map_data = pft.character_images
|
|
10
|
+
File.write("tmp/map.yaml", map_data.to_yaml)
|
|
11
|
+
puts "Generated #{map_data.size}"
|
|
12
|
+
|
|
13
|
+
pft.write_text_image(pft.uppercase.join, "uppercase.png")
|
|
14
|
+
pft.write_text_image(pft.lowercase.join, "lowercase.png")
|
|
15
|
+
pft.write_text_image(pft.digits.join, "digits.png")
|
|
16
|
+
pft.write_text_image(pft.symbols.join, "symbols.png", pad: 10)
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: pixel_font_trie_ocr
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Robert Ferney
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rmagick
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '6.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '6.3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ttfunk
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.8'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.8'
|
|
40
|
+
description: Provides perfect accuracy and microsecond performance for crystal-clear,
|
|
41
|
+
5-8px pixel fonts by building a trie from font glyphs and matching image column
|
|
42
|
+
bitmasks.
|
|
43
|
+
email:
|
|
44
|
+
- rob@ferney.org
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- AGENTS.md
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- CODE_OF_CONDUCT.md
|
|
52
|
+
- LICENSE.txt
|
|
53
|
+
- README.md
|
|
54
|
+
- Rakefile
|
|
55
|
+
- lib/pixel_font_trie_ocr.rb
|
|
56
|
+
- lib/pixel_font_trie_ocr/font_metadata.rb
|
|
57
|
+
- lib/pixel_font_trie_ocr/fonts/hex-synergy_font.ttf
|
|
58
|
+
- lib/pixel_font_trie_ocr/image_column_extractor.rb
|
|
59
|
+
- lib/pixel_font_trie_ocr/image_utils.rb
|
|
60
|
+
- lib/pixel_font_trie_ocr/methods.rb
|
|
61
|
+
- lib/pixel_font_trie_ocr/parsing.rb
|
|
62
|
+
- lib/pixel_font_trie_ocr/trie.rb
|
|
63
|
+
- lib/pixel_font_trie_ocr/trie/node.rb
|
|
64
|
+
- lib/pixel_font_trie_ocr/version.rb
|
|
65
|
+
- lib/tasks/font_map.rake
|
|
66
|
+
- sig/pixel_font_trie_ocr.rbs
|
|
67
|
+
homepage: https://github.com/Barbary-Horde-Studios/pixel_font_trie_ocr
|
|
68
|
+
licenses:
|
|
69
|
+
- MIT
|
|
70
|
+
metadata:
|
|
71
|
+
allowed_push_host: https://rubygems.org
|
|
72
|
+
homepage_uri: https://github.com/Barbary-Horde-Studios/pixel_font_trie_ocr
|
|
73
|
+
source_code_uri: https://github.com/Barbary-Horde-Studios/pixel_font_trie_ocr
|
|
74
|
+
changelog_uri: https://github.com/Barbary-Horde-Studios/pixel_font_trie_ocr/blob/main/CHANGELOG.md
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 3.2.0
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 4.0.10
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: Deterministic OCR for tiny pixel fonts using a Trie of column bitmasks
|
|
92
|
+
test_files: []
|