macocr 0.1.0__tar.gz
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.
- macocr-0.1.0/.gitignore +10 -0
- macocr-0.1.0/LICENSE +21 -0
- macocr-0.1.0/PKG-INFO +132 -0
- macocr-0.1.0/README.md +97 -0
- macocr-0.1.0/images/img_horz01.png +0 -0
- macocr-0.1.0/images/img_vert01.png +0 -0
- macocr-0.1.0/images/img_vert02.HEIC +0 -0
- macocr-0.1.0/noxfile.py +34 -0
- macocr-0.1.0/pyproject.toml +43 -0
- macocr-0.1.0/results/img_horz01.txt +15 -0
- macocr-0.1.0/results/img_vert01.txt +8 -0
- macocr-0.1.0/results/img_vert02.txt +9 -0
- macocr-0.1.0/src/macocr/__init__.py +1 -0
- macocr-0.1.0/src/macocr/main.py +117 -0
- macocr-0.1.0/test.py +127 -0
macocr-0.1.0/.gitignore
ADDED
macocr-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Rio Fujita
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
macocr-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: macocr
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: macocr is a python script for OCR on macOS
|
|
5
|
+
Project-URL: Homepage, https://github.com/rioriost/homebrew-macocr
|
|
6
|
+
Project-URL: Issues, https://github.com/rioriost/homebrew-macocr/issues
|
|
7
|
+
Author-email: Rio Fujita <rifujita@microsoft.com>
|
|
8
|
+
License: MIT License
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2025 Rio Fujita
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in all
|
|
20
|
+
copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
28
|
+
SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Python: >=3.11
|
|
31
|
+
Requires-Dist: ocrmac>=1.0.0
|
|
32
|
+
Requires-Dist: pillow-heif>=0.21.0
|
|
33
|
+
Requires-Dist: python-magic>=0.4.27
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# macocr
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
## Overview
|
|
41
|
+
|
|
42
|
+
macocr is a python script for OCR on macOS
|
|
43
|
+
|
|
44
|
+
## Table of Contents
|
|
45
|
+
|
|
46
|
+
- [Installation](#installation)
|
|
47
|
+
- [Prerequisites](#prerequisites)
|
|
48
|
+
- [Usage](#usage)
|
|
49
|
+
- [Release Notes](#release-notes)
|
|
50
|
+
- [License](#license)
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
Just add tap and install homebrew package.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
brew tap rioriost/macocr
|
|
58
|
+
brew install macocr
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Prerequisites
|
|
62
|
+
|
|
63
|
+
- Python 3.11 or higher
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
Execute macocr command.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
macocr --help
|
|
71
|
+
usage: macocr [-h] image_dir output_dir
|
|
72
|
+
|
|
73
|
+
positional arguments:
|
|
74
|
+
image_dir Directory containing images to OCR
|
|
75
|
+
output_dir Directory to output OCR results
|
|
76
|
+
|
|
77
|
+
options:
|
|
78
|
+
-h, --help show this help message and exit
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The indentical usage is shown below.
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
macocr images results
|
|
85
|
+
Processing /Users/rifujita/ownCloud/bin/macocr/images/img_horz01.png
|
|
86
|
+
Processing /Users/rifujita/ownCloud/bin/macocr/images/img_vert02.HEIC
|
|
87
|
+
Processing /Users/rifujita/ownCloud/bin/macocr/images/img_vert01.png
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
results
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
cat results/*
|
|
94
|
+
日本国内の交差点は約100万あるとされている
|
|
95
|
+
ので、そこに接続しているエッジは T字路で 3
|
|
96
|
+
本、2本の道路が交差していれば4本、さらに五
|
|
97
|
+
叉路や側道があるパターンでは、1 つのノードに
|
|
98
|
+
20 ぐらいのエッジが接続しているパターンも実
|
|
99
|
+
在します。
|
|
100
|
+
航空路線に例えると、ハブ空港が日本国内だけ
|
|
101
|
+
で約100万あり、エッジがその10倍・約1,000万
|
|
102
|
+
存在する、みたいなものです*3。そしてその巨大
|
|
103
|
+
なネットワーク内で乗り継ぎを数十回、数百回す
|
|
104
|
+
るというのが、私たちが日常行っている、自動車
|
|
105
|
+
の運転を含めた地上での移動です。
|
|
106
|
+
道路ネットワークよりもさらに大規模なネット
|
|
107
|
+
ワークとしては、SNS やウェブのリンクなども
|
|
108
|
+
考えられます。PostgreSQL に航空路線のデータを格納する例を考えてみましょう。現在世界には約三千五百の空港があり、それらの空港を結ぶ航空路線
|
|
109
|
+
は数万に及びますが、羽田空港からシアトルタコマ空港への直行便は、航空会社の別を考慮しなければ一路線しかありません。条件は「羽田発
|
|
110
|
+
シアトルタコマ着」のみです。
|
|
111
|
+
しかし乗り継ぎを考慮すると、羽田からシアトルタコマへの経路は複数通り存在します。クエリの条件は「羽田発・空港(A)着」「空港(A)発シ
|
|
112
|
+
アトルタコマ着」です。さらに乗り継ぎが二回になると、「羽田発・空港(A)着」「空港(A)発-空港(B)着」「空港(B)着・シアトルタコマ着」とな
|
|
113
|
+
り、経路のパターンが増えることが分かります。空港(ノード)の数より、路線(エッジ)の数がかなり多くなるのはこういう理由です。
|
|
114
|
+
航空路線の場合、乗り継ぎ回数がそれほど増えることはないので、クエリもあまり難しくなるとは考えにくく、SQL で十分対応できるでしょ
|
|
115
|
+
う。•この薬はあなたの症状に合わせ処方したものです。他の人には譲らないでください。
|
|
116
|
+
•お薬は指示どおりにお飲みください。自己判断で服用量や回数を加減しないでください。
|
|
117
|
+
•お薬はお子様の手の届かない場所に置いてください。
|
|
118
|
+
•この薬以外に他の医療機関の薬又は市販薬を服用している方は、医師又は薬剤師に申し
|
|
119
|
+
出てください。
|
|
120
|
+
口食前とは食事前30分位、食後とは食後30分位に服用することです。
|
|
121
|
+
口食間とは食後2時間位に服用することです。
|
|
122
|
+
口(糖尿病の方を除き)食事のとれない方でも食事をする時と同じ時間に服用してください。
|
|
123
|
+
口時間ごとに定められた薬は食事に関係なく服用してください。
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Release Notes
|
|
127
|
+
|
|
128
|
+
### 0.1.0 Release
|
|
129
|
+
* Initial release.
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
MIT License
|
macocr-0.1.0/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# macocr
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
macocr is a python script for OCR on macOS
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Prerequisites](#prerequisites)
|
|
13
|
+
- [Usage](#usage)
|
|
14
|
+
- [Release Notes](#release-notes)
|
|
15
|
+
- [License](#license)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Just add tap and install homebrew package.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
brew tap rioriost/macocr
|
|
23
|
+
brew install macocr
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Prerequisites
|
|
27
|
+
|
|
28
|
+
- Python 3.11 or higher
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Execute macocr command.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
macocr --help
|
|
36
|
+
usage: macocr [-h] image_dir output_dir
|
|
37
|
+
|
|
38
|
+
positional arguments:
|
|
39
|
+
image_dir Directory containing images to OCR
|
|
40
|
+
output_dir Directory to output OCR results
|
|
41
|
+
|
|
42
|
+
options:
|
|
43
|
+
-h, --help show this help message and exit
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The indentical usage is shown below.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
macocr images results
|
|
50
|
+
Processing /Users/rifujita/ownCloud/bin/macocr/images/img_horz01.png
|
|
51
|
+
Processing /Users/rifujita/ownCloud/bin/macocr/images/img_vert02.HEIC
|
|
52
|
+
Processing /Users/rifujita/ownCloud/bin/macocr/images/img_vert01.png
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
results
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cat results/*
|
|
59
|
+
日本国内の交差点は約100万あるとされている
|
|
60
|
+
ので、そこに接続しているエッジは T字路で 3
|
|
61
|
+
本、2本の道路が交差していれば4本、さらに五
|
|
62
|
+
叉路や側道があるパターンでは、1 つのノードに
|
|
63
|
+
20 ぐらいのエッジが接続しているパターンも実
|
|
64
|
+
在します。
|
|
65
|
+
航空路線に例えると、ハブ空港が日本国内だけ
|
|
66
|
+
で約100万あり、エッジがその10倍・約1,000万
|
|
67
|
+
存在する、みたいなものです*3。そしてその巨大
|
|
68
|
+
なネットワーク内で乗り継ぎを数十回、数百回す
|
|
69
|
+
るというのが、私たちが日常行っている、自動車
|
|
70
|
+
の運転を含めた地上での移動です。
|
|
71
|
+
道路ネットワークよりもさらに大規模なネット
|
|
72
|
+
ワークとしては、SNS やウェブのリンクなども
|
|
73
|
+
考えられます。PostgreSQL に航空路線のデータを格納する例を考えてみましょう。現在世界には約三千五百の空港があり、それらの空港を結ぶ航空路線
|
|
74
|
+
は数万に及びますが、羽田空港からシアトルタコマ空港への直行便は、航空会社の別を考慮しなければ一路線しかありません。条件は「羽田発
|
|
75
|
+
シアトルタコマ着」のみです。
|
|
76
|
+
しかし乗り継ぎを考慮すると、羽田からシアトルタコマへの経路は複数通り存在します。クエリの条件は「羽田発・空港(A)着」「空港(A)発シ
|
|
77
|
+
アトルタコマ着」です。さらに乗り継ぎが二回になると、「羽田発・空港(A)着」「空港(A)発-空港(B)着」「空港(B)着・シアトルタコマ着」とな
|
|
78
|
+
り、経路のパターンが増えることが分かります。空港(ノード)の数より、路線(エッジ)の数がかなり多くなるのはこういう理由です。
|
|
79
|
+
航空路線の場合、乗り継ぎ回数がそれほど増えることはないので、クエリもあまり難しくなるとは考えにくく、SQL で十分対応できるでしょ
|
|
80
|
+
う。•この薬はあなたの症状に合わせ処方したものです。他の人には譲らないでください。
|
|
81
|
+
•お薬は指示どおりにお飲みください。自己判断で服用量や回数を加減しないでください。
|
|
82
|
+
•お薬はお子様の手の届かない場所に置いてください。
|
|
83
|
+
•この薬以外に他の医療機関の薬又は市販薬を服用している方は、医師又は薬剤師に申し
|
|
84
|
+
出てください。
|
|
85
|
+
口食前とは食事前30分位、食後とは食後30分位に服用することです。
|
|
86
|
+
口食間とは食後2時間位に服用することです。
|
|
87
|
+
口(糖尿病の方を除き)食事のとれない方でも食事をする時と同じ時間に服用してください。
|
|
88
|
+
口時間ごとに定められた薬は食事に関係なく服用してください。
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Release Notes
|
|
92
|
+
|
|
93
|
+
### 0.1.0 Release
|
|
94
|
+
* Initial release.
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
MIT License
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
macocr-0.1.0/noxfile.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import nox
|
|
2
|
+
|
|
3
|
+
nox.options.python = "3.11"
|
|
4
|
+
nox.options.default_venv_backend = "uv"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@nox.session(python=["3.11"], tags=["lint"])
|
|
8
|
+
def lint(session):
|
|
9
|
+
session.install("ruff")
|
|
10
|
+
session.run("uv", "run", "ruff", "check")
|
|
11
|
+
session.run("uv", "run", "ruff", "format")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@nox.session(python=["3.11"], tags=["mypy"])
|
|
15
|
+
def mypy(session):
|
|
16
|
+
session.install(".")
|
|
17
|
+
session.install("mypy")
|
|
18
|
+
session.run("uv", "run", "mypy", "src")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@nox.session(python=["3.11"], tags=["pytest"])
|
|
22
|
+
def pytest(session):
|
|
23
|
+
session.install(".")
|
|
24
|
+
session.install("pytest", "pytest-cov")
|
|
25
|
+
test_files = ["test.py"]
|
|
26
|
+
session.run(
|
|
27
|
+
"uv",
|
|
28
|
+
"run",
|
|
29
|
+
"pytest",
|
|
30
|
+
"--maxfail=1",
|
|
31
|
+
"--cov=macocr",
|
|
32
|
+
"--cov-report=term",
|
|
33
|
+
*test_files,
|
|
34
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "macocr"
|
|
3
|
+
authors = [
|
|
4
|
+
{name = "Rio Fujita", email = "rifujita@microsoft.com"},
|
|
5
|
+
]
|
|
6
|
+
version = "0.1.0"
|
|
7
|
+
license = {file = "LICENSE"}
|
|
8
|
+
description = "macocr is a python script for OCR on macOS"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"ocrmac>=1.0.0",
|
|
14
|
+
"pillow-heif>=0.21.0",
|
|
15
|
+
"python-magic>=0.4.27",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://github.com/rioriost/homebrew-macocr"
|
|
20
|
+
Issues = "https://github.com/rioriost/homebrew-macocr/issues"
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
macocr = "macocr.main:main"
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["hatchling"]
|
|
27
|
+
build-backend = "hatchling.build"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/macocr"]
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.sdist]
|
|
33
|
+
include = [
|
|
34
|
+
"src/macocr/*.py",
|
|
35
|
+
"*.py",
|
|
36
|
+
"images/*",
|
|
37
|
+
"results/*",
|
|
38
|
+
]
|
|
39
|
+
exclude = [
|
|
40
|
+
"macocr.rb",
|
|
41
|
+
"uv.lock",
|
|
42
|
+
"dist/.DS_Store",
|
|
43
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
日本国内の交差点は約100万あるとされている
|
|
2
|
+
ので、そこに接続しているエッジは T字路で 3
|
|
3
|
+
本、2本の道路が交差していれば4本、さらに五
|
|
4
|
+
叉路や側道があるパターンでは、1 つのノードに
|
|
5
|
+
20 ぐらいのエッジが接続しているパターンも実
|
|
6
|
+
在します。
|
|
7
|
+
航空路線に例えると、ハブ空港が日本国内だけ
|
|
8
|
+
で約100万あり、エッジがその10倍・約1,000万
|
|
9
|
+
存在する、みたいなものです*3。そしてその巨大
|
|
10
|
+
なネットワーク内で乗り継ぎを数十回、数百回す
|
|
11
|
+
るというのが、私たちが日常行っている、自動車
|
|
12
|
+
の運転を含めた地上での移動です。
|
|
13
|
+
道路ネットワークよりもさらに大規模なネット
|
|
14
|
+
ワークとしては、SNS やウェブのリンクなども
|
|
15
|
+
考えられます。
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
PostgreSQL に航空路線のデータを格納する例を考えてみましょう。現在世界には約三千五百の空港があり、それらの空港を結ぶ航空路線
|
|
2
|
+
は数万に及びますが、羽田空港からシアトルタコマ空港への直行便は、航空会社の別を考慮しなければ一路線しかありません。条件は「羽田発
|
|
3
|
+
シアトルタコマ着」のみです。
|
|
4
|
+
しかし乗り継ぎを考慮すると、羽田からシアトルタコマへの経路は複数通り存在します。クエリの条件は「羽田発・空港(A)着」「空港(A)発シ
|
|
5
|
+
アトルタコマ着」です。さらに乗り継ぎが二回になると、「羽田発・空港(A)着」「空港(A)発-空港(B)着」「空港(B)着・シアトルタコマ着」とな
|
|
6
|
+
り、経路のパターンが増えることが分かります。空港(ノード)の数より、路線(エッジ)の数がかなり多くなるのはこういう理由です。
|
|
7
|
+
航空路線の場合、乗り継ぎ回数がそれほど増えることはないので、クエリもあまり難しくなるとは考えにくく、SQL で十分対応できるでしょ
|
|
8
|
+
う。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
•この薬はあなたの症状に合わせ処方したものです。他の人には譲らないでください。
|
|
2
|
+
•お薬は指示どおりにお飲みください。自己判断で服用量や回数を加減しないでください。
|
|
3
|
+
•お薬はお子様の手の届かない場所に置いてください。
|
|
4
|
+
•この薬以外に他の医療機関の薬又は市販薬を服用している方は、医師又は薬剤師に申し
|
|
5
|
+
出てください。
|
|
6
|
+
口食前とは食事前30分位、食後とは食後30分位に服用することです。
|
|
7
|
+
口食間とは食後2時間位に服用することです。
|
|
8
|
+
口(糖尿病の方を除き)食事のとれない方でも食事をする時と同じ時間に服用してください。
|
|
9
|
+
口時間ごとに定められた薬は食事に関係なく服用してください。
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import os
|
|
6
|
+
import glob
|
|
7
|
+
import magic
|
|
8
|
+
import math
|
|
9
|
+
from pillow_heif import register_heif_opener # type: ignore
|
|
10
|
+
from ocrmac import ocrmac # type: ignore
|
|
11
|
+
|
|
12
|
+
register_heif_opener()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OCRProcessor:
|
|
16
|
+
def __init__(self, image_dir: str, output_dir: str):
|
|
17
|
+
# Convert provided directories to absolute paths
|
|
18
|
+
self.image_dir = os.path.realpath(image_dir)
|
|
19
|
+
self.output_dir = os.path.realpath(output_dir)
|
|
20
|
+
|
|
21
|
+
def run(self) -> None:
|
|
22
|
+
"""Main method to process all images from the input directory."""
|
|
23
|
+
for image in self.get_images():
|
|
24
|
+
print(f"Processing {image}")
|
|
25
|
+
annotations = self.process_image(image)
|
|
26
|
+
if not annotations:
|
|
27
|
+
print(f"Failed to OCR {image}")
|
|
28
|
+
continue
|
|
29
|
+
self.save_annotations(image, annotations)
|
|
30
|
+
|
|
31
|
+
def get_images(self) -> list:
|
|
32
|
+
"""
|
|
33
|
+
Retrieves image paths from the input directory.
|
|
34
|
+
Only includes images with MIME types 'image/jpeg', 'image/png', or 'image/heic'.
|
|
35
|
+
"""
|
|
36
|
+
pattern = os.path.join(self.image_dir, "*")
|
|
37
|
+
images = [
|
|
38
|
+
f
|
|
39
|
+
for f in glob.glob(pattern)
|
|
40
|
+
if magic.from_file(f, mime=True)
|
|
41
|
+
in ["image/jpeg", "image/png", "image/heic"]
|
|
42
|
+
]
|
|
43
|
+
return images
|
|
44
|
+
|
|
45
|
+
def process_image(self, image_path: str) -> list:
|
|
46
|
+
"""
|
|
47
|
+
Uses the ocrmac.OCR class to perform OCR on the image.
|
|
48
|
+
The "livetext" framework is used with language preference set to Japanese.
|
|
49
|
+
"""
|
|
50
|
+
ocr_instance = ocrmac.OCR(
|
|
51
|
+
image_path, framework="livetext", language_preference=["ja-JP"]
|
|
52
|
+
)
|
|
53
|
+
return ocr_instance.recognize()
|
|
54
|
+
|
|
55
|
+
def save_annotations(self, image_path: str, annotations: list) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Formats the OCR annotations and writes them to a text file.
|
|
58
|
+
The filename is based on the original image filename without extension.
|
|
59
|
+
"""
|
|
60
|
+
bn_no_ext = os.path.splitext(os.path.basename(image_path))[0]
|
|
61
|
+
output_file = os.path.join(self.output_dir, f"{bn_no_ext}.txt")
|
|
62
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
63
|
+
f.write(self.format_annotations(annotations))
|
|
64
|
+
|
|
65
|
+
def format_annotations(self, annotations: list) -> str:
|
|
66
|
+
"""
|
|
67
|
+
Formats OCR annotations into a string, attempting to replicate the original layout.
|
|
68
|
+
Uses average character width and height from the annotations to determine spacing.
|
|
69
|
+
"""
|
|
70
|
+
SPACE = 1.0
|
|
71
|
+
avg_font_width = (
|
|
72
|
+
sum(ann[2][2] for ann in annotations) / len(annotations) * SPACE
|
|
73
|
+
)
|
|
74
|
+
avg_font_height = (
|
|
75
|
+
sum(ann[2][3] for ann in annotations) / len(annotations) * SPACE
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
lines = []
|
|
79
|
+
sv_x, sv_y = 0, 0
|
|
80
|
+
|
|
81
|
+
for ann in annotations:
|
|
82
|
+
x, y, width, height = ann[2]
|
|
83
|
+
# If the current annotation's position is offset enough from the previous annotation,
|
|
84
|
+
# treat it as a new line
|
|
85
|
+
if (abs(x - sv_x) > avg_font_width) and (abs(y - sv_y) > avg_font_height):
|
|
86
|
+
lines.append(ann[0])
|
|
87
|
+
else:
|
|
88
|
+
# Calculate the number of spaces based on the position differences
|
|
89
|
+
spaces = math.floor(
|
|
90
|
+
max(
|
|
91
|
+
abs(x - sv_x) / avg_font_width * 0.5,
|
|
92
|
+
abs(y - sv_y) / avg_font_height * 0.5,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
if lines:
|
|
96
|
+
lines[-1] += " " * spaces + ann[0]
|
|
97
|
+
else:
|
|
98
|
+
lines.append(" " * spaces + ann[0])
|
|
99
|
+
sv_x, sv_y = x, y
|
|
100
|
+
|
|
101
|
+
return "\n".join(lines)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main() -> None:
|
|
105
|
+
# Parse command-line arguments
|
|
106
|
+
parser = argparse.ArgumentParser()
|
|
107
|
+
parser.add_argument("image_dir", help="Directory containing images to OCR")
|
|
108
|
+
parser.add_argument("output_dir", help="Directory to output OCR results")
|
|
109
|
+
args = parser.parse_args()
|
|
110
|
+
|
|
111
|
+
# Create an OCRProcessor instance and run the OCR processing
|
|
112
|
+
processor = OCRProcessor(args.image_dir, args.output_dir)
|
|
113
|
+
processor.run()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
main()
|
macocr-0.1.0/test.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
import shutil
|
|
8
|
+
import unittest
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
# Insert the src directory (which contains the macocr package) into sys.path.
|
|
12
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
|
13
|
+
|
|
14
|
+
# Import the main module from the macocr package.
|
|
15
|
+
import macocr.main as main_module
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# A fake OCR class for testing.
|
|
19
|
+
class FakeOCR:
|
|
20
|
+
def __init__(self, image):
|
|
21
|
+
self.image = image
|
|
22
|
+
|
|
23
|
+
def recognize(self):
|
|
24
|
+
# If the image filename includes 'file1', return a dummy annotation list.
|
|
25
|
+
# Otherwise (e.g. for file2.jpg) return an empty list.
|
|
26
|
+
if "file1.jpg" in self.image:
|
|
27
|
+
# A dummy annotation: tuple (text, ignored, [x, y, width, height])
|
|
28
|
+
# The values below were chosen such that format_annotations returns "hello".
|
|
29
|
+
return [("hello", None, [0, 0, 5, 5])]
|
|
30
|
+
else:
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def fake_ocr_constructor(image, framework, language_preference):
|
|
35
|
+
return FakeOCR(image)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TestMacocrMain(unittest.TestCase):
|
|
39
|
+
def setUp(self):
|
|
40
|
+
# Create temporary directories for the input images and output results.
|
|
41
|
+
self.temp_input_dir = tempfile.mkdtemp(prefix="macocr_input_")
|
|
42
|
+
self.temp_output_dir = tempfile.mkdtemp(prefix="macocr_output_")
|
|
43
|
+
# Create two dummy image files in the input directory.
|
|
44
|
+
# Their content is irrelevant because we patch magic.from_file.
|
|
45
|
+
self.image_file1 = os.path.join(self.temp_input_dir, "file1.jpg")
|
|
46
|
+
self.image_file2 = os.path.join(self.temp_input_dir, "file2.jpg")
|
|
47
|
+
with open(self.image_file1, "w") as f:
|
|
48
|
+
f.write("dummy content")
|
|
49
|
+
with open(self.image_file2, "w") as f:
|
|
50
|
+
f.write("dummy content")
|
|
51
|
+
|
|
52
|
+
def tearDown(self):
|
|
53
|
+
# Remove temporary directories and their contents.
|
|
54
|
+
shutil.rmtree(self.temp_input_dir)
|
|
55
|
+
shutil.rmtree(self.temp_output_dir)
|
|
56
|
+
|
|
57
|
+
@patch("macocr.main.magic.from_file", return_value="image/jpeg")
|
|
58
|
+
@patch("macocr.main.ocrmac.OCR", side_effect=fake_ocr_constructor)
|
|
59
|
+
def test_main(self, mock_ocr, mock_magic):
|
|
60
|
+
# Patch sys.argv so that main() sees our temporary directories.
|
|
61
|
+
test_argv = [
|
|
62
|
+
"macocr.py", # dummy name of the script
|
|
63
|
+
self.temp_input_dir,
|
|
64
|
+
self.temp_output_dir,
|
|
65
|
+
]
|
|
66
|
+
with patch.object(sys, "argv", test_argv):
|
|
67
|
+
# Run the main function from macocr.main.
|
|
68
|
+
main_module.main()
|
|
69
|
+
|
|
70
|
+
# Check that file1.jpg produced an output text file since its annotation is non-empty.
|
|
71
|
+
expected_output_file1 = os.path.join(self.temp_output_dir, "file1.txt")
|
|
72
|
+
self.assertTrue(
|
|
73
|
+
os.path.exists(expected_output_file1),
|
|
74
|
+
f"Expected output file {expected_output_file1} does not exist.",
|
|
75
|
+
)
|
|
76
|
+
with open(expected_output_file1, "r", encoding="utf-8") as f:
|
|
77
|
+
content = f.read()
|
|
78
|
+
# Given our dummy annotation and logic in format_annotations, the output should be "hello".
|
|
79
|
+
self.assertEqual(content, "hello")
|
|
80
|
+
|
|
81
|
+
# Check that file2.jpg did not generate an output file, as its OCR recognized no text.
|
|
82
|
+
expected_output_file2 = os.path.join(self.temp_output_dir, "file2.txt")
|
|
83
|
+
self.assertFalse(
|
|
84
|
+
os.path.exists(expected_output_file2),
|
|
85
|
+
f"Output file {expected_output_file2} should not exist.",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
@patch("macocr.main.magic.from_file", return_value="image/jpeg")
|
|
89
|
+
@patch("macocr.main.ocrmac.OCR", side_effect=fake_ocr_constructor)
|
|
90
|
+
def test_format_annotations_multiple_entries(self, mock_ocr, mock_magic):
|
|
91
|
+
"""
|
|
92
|
+
Test the format_annotations method with multiple annotations.
|
|
93
|
+
This covers the branch in format_annotations where space calculation is performed.
|
|
94
|
+
"""
|
|
95
|
+
# Create dummy annotations with two entries having positions that trigger a new line.
|
|
96
|
+
annotations = [
|
|
97
|
+
("Hello", None, [0, 0, 5, 5]),
|
|
98
|
+
("World", None, [20, 20, 5, 5]),
|
|
99
|
+
]
|
|
100
|
+
# Instead of needing an instance, call the method on the class:
|
|
101
|
+
output = main_module.OCRProcessor.format_annotations(
|
|
102
|
+
main_module.OCRProcessor, annotations
|
|
103
|
+
)
|
|
104
|
+
expected = "Hello\nWorld"
|
|
105
|
+
self.assertEqual(output, expected)
|
|
106
|
+
|
|
107
|
+
@patch("macocr.main.magic.from_file", return_value="image/jpeg")
|
|
108
|
+
@patch("macocr.main.ocrmac.OCR", side_effect=fake_ocr_constructor)
|
|
109
|
+
def test_format_annotations_same_line(self, mock_ocr, mock_magic):
|
|
110
|
+
"""
|
|
111
|
+
Test the format_annotations method when annotations are close enough that they are on the same line.
|
|
112
|
+
"""
|
|
113
|
+
# Create dummy annotations that are side by side.
|
|
114
|
+
annotations = [
|
|
115
|
+
("Hello", None, [0, 0, 5, 5]),
|
|
116
|
+
("World", None, [3, 0, 5, 5]), # small horizontal difference
|
|
117
|
+
]
|
|
118
|
+
output = main_module.OCRProcessor.format_annotations(
|
|
119
|
+
main_module.OCRProcessor, annotations
|
|
120
|
+
)
|
|
121
|
+
# Depending on spacing calculation, "World" should be concatenated directly to "Hello".
|
|
122
|
+
expected = "HelloWorld"
|
|
123
|
+
self.assertEqual(output, expected)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
unittest.main()
|