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.
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
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
+ ![License](https://img.shields.io/badge/license-MIT-blue.svg)
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
+ ![License](https://img.shields.io/badge/license-MIT-blue.svg)
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
@@ -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()