keycap-designer 0.1.3__py3-none-any.whl
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.
- keycap_designer/.gitignore +1 -0
- keycap_designer/__init__.py +7 -0
- keycap_designer/__main__.py +254 -0
- keycap_designer/_version.py +21 -0
- keycap_designer/area/junana/15u-convex-front.png +0 -0
- keycap_designer/area/junana/15u-convex-tf.png +0 -0
- keycap_designer/area/junana/15u-convex-top.png +0 -0
- keycap_designer/area/junana/15u-front.png +0 -0
- keycap_designer/area/junana/15u-tf.png +0 -0
- keycap_designer/area/junana/15u-top.png +0 -0
- keycap_designer/area/junana/15u.png +0 -0
- keycap_designer/area/junana/1u-convex-front.png +0 -0
- keycap_designer/area/junana/1u-convex-tf.png +0 -0
- keycap_designer/area/junana/1u-convex-top.png +0 -0
- keycap_designer/area/junana/1u-front.png +0 -0
- keycap_designer/area/junana/1u-tf.png +0 -0
- keycap_designer/area/junana/1u-top.png +0 -0
- keycap_designer/area/junana/1u.png +0 -0
- keycap_designer/area/junana/225u-convex-front.png +0 -0
- keycap_designer/area/junana/225u-convex-tf.png +0 -0
- keycap_designer/area/junana/225u-convex-top.png +0 -0
- keycap_designer/area/junana/225u-front.png +0 -0
- keycap_designer/area/junana/225u-tf.png +0 -0
- keycap_designer/area/junana/225u-top.png +0 -0
- keycap_designer/area/junana/225u.png +0 -0
- keycap_designer/area/junana/note.txt +5 -0
- keycap_designer/area/xda/125u.png +0 -0
- keycap_designer/area/xda/15u.png +0 -0
- keycap_designer/area/xda/175u.png +0 -0
- keycap_designer/area/xda/1u-front.png +0 -0
- keycap_designer/area/xda/1u.png +0 -0
- keycap_designer/area/xda/225u.png +0 -0
- keycap_designer/area/xda/275u.png +0 -0
- keycap_designer/area/xda/2u.png +0 -0
- keycap_designer/color_management.py +176 -0
- keycap_designer/constants.py +75 -0
- keycap_designer/deform/__init__.py +0 -0
- keycap_designer/deform/deform_test.py +46 -0
- keycap_designer/deform/junana/__init__.py +0 -0
- keycap_designer/deform/junana/area/f15u.png +0 -0
- keycap_designer/deform/junana/area/f15u_convex.png +0 -0
- keycap_designer/deform/junana/area/f1u.png +0 -0
- keycap_designer/deform/junana/area/f1u_convex.png +0 -0
- keycap_designer/deform/junana/area/f225u.png +0 -0
- keycap_designer/deform/junana/area/f225u_convex-pattern.png +0 -0
- keycap_designer/deform/junana/area/f225u_convex.png +0 -0
- keycap_designer/deform/junana/f15u.py +28 -0
- keycap_designer/deform/junana/f15u_convex.py +28 -0
- keycap_designer/deform/junana/f1u.py +26 -0
- keycap_designer/deform/junana/f1u_convex.py +37 -0
- keycap_designer/deform/junana/f225u.py +28 -0
- keycap_designer/deform/junana/f225u_convex.py +27 -0
- keycap_designer/deform/junana/pattern/f1u.png +0 -0
- keycap_designer/deform/junana/pattern/f1u_convex.png +0 -0
- keycap_designer/font/NotoSansMono-VariableFont_wdth,wght.ttf +0 -0
- keycap_designer/font/OFL.txt +93 -0
- keycap_designer/icc/Linear P3D65.icc +0 -0
- keycap_designer/icc/sRGBz.icc +0 -0
- keycap_designer/icc/sublinova-epson4pigment-PBT-20231121_srgb.icc +0 -0
- keycap_designer/image.py +237 -0
- keycap_designer/manuscript.py +901 -0
- keycap_designer/overwrite_reportlab.py +125 -0
- keycap_designer/preview.py +439 -0
- keycap_designer/profile/__init__.py +67 -0
- keycap_designer/profile/junana.py +158 -0
- keycap_designer/profile/xda.py +59 -0
- keycap_designer/repo/content/all_available_junana.py +28 -0
- keycap_designer/repo/content/all_available_xda.py +35 -0
- keycap_designer/repo/content/example_ansi_104.py +133 -0
- keycap_designer/repo/content/starter-kit/00.png +0 -0
- keycap_designer/repo/content/starter-kit/01.png +0 -0
- keycap_designer/repo/content/starter-kit/02.png +0 -0
- keycap_designer/repo/content/starter-kit/03.png +0 -0
- keycap_designer/repo/content/starter-kit/04.png +0 -0
- keycap_designer/repo/content/starter-kit/05.png +0 -0
- keycap_designer/repo/content/starter-kit/06.png +0 -0
- keycap_designer/repo/content/starter-kit/07.png +0 -0
- keycap_designer/repo/content/starter-kit/08.png +0 -0
- keycap_designer/repo/content/starter-kit/09.png +0 -0
- keycap_designer/repo/content/starter-kit/10.png +0 -0
- keycap_designer/repo/content/test_image.png +0 -0
- keycap_designer/repo/content/test_pattern.png +0 -0
- keycap_designer/repo/content/tutorial_1.py +94 -0
- keycap_designer/repo/content/tutorial_2.py +76 -0
- keycap_designer/repo/content/tutorial_3.py +112 -0
- keycap_designer/repo/content/tutorial_junana.py +46 -0
- keycap_designer/repo/font/OFL.txt +93 -0
- keycap_designer/repo/font/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- keycap_designer/repo/font/OpenSans-VariableFont_wdth,wght.ttf +0 -0
- keycap_designer/repo/layout/ansi-104.json +227 -0
- keycap_designer/repo/layout/p2ppcb-starter-kit.json +77 -0
- keycap_designer/repo/layout/test.json +61 -0
- keycap_designer/repo/vscode/launch.json +16 -0
- keycap_designer/repo/vscode/settings.json +10 -0
- keycap_designer-0.1.3.dist-info/METADATA +320 -0
- keycap_designer-0.1.3.dist-info/RECORD +100 -0
- keycap_designer-0.1.3.dist-info/WHEEL +5 -0
- keycap_designer-0.1.3.dist-info/entry_points.txt +2 -0
- keycap_designer-0.1.3.dist-info/licenses/LICENSE +21 -0
- keycap_designer-0.1.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
_version.py
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import faulthandler
|
|
2
|
+
faulthandler.enable()
|
|
3
|
+
import typing as ty
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import importlib
|
|
8
|
+
import traceback
|
|
9
|
+
import sys
|
|
10
|
+
import platform
|
|
11
|
+
|
|
12
|
+
from prompt_toolkit import PromptSession
|
|
13
|
+
from prompt_toolkit.completion import NestedCompleter, PathCompleter
|
|
14
|
+
from prompt_toolkit.history import FileHistory
|
|
15
|
+
from prompt_toolkit.styles import Style
|
|
16
|
+
from prompt_toolkit.shortcuts import yes_no_dialog
|
|
17
|
+
|
|
18
|
+
from keycap_designer.constants import CURRENT_DIR, RESOURCE_DIR
|
|
19
|
+
from keycap_designer.manuscript import manuscript_to_artwork
|
|
20
|
+
from keycap_designer.preview import print_rc_map, print_preview
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
CONTENT_DIR_NAME = 'content'
|
|
24
|
+
FONT_DIR_NAME = 'font'
|
|
25
|
+
LAYOUT_DIR_NAME = 'layout'
|
|
26
|
+
VSCODE_DIR_NAME = '.vscode'
|
|
27
|
+
CONTENT_DIR = CURRENT_DIR / CONTENT_DIR_NAME
|
|
28
|
+
OUTPUT_DIR = CURRENT_DIR / 'tmp'
|
|
29
|
+
REPO_DIR_NAMES = [CONTENT_DIR_NAME, FONT_DIR_NAME, LAYOUT_DIR_NAME]
|
|
30
|
+
REPO_DIRS = [CURRENT_DIR / d for d in REPO_DIR_NAMES]
|
|
31
|
+
MODULE = None
|
|
32
|
+
IMPORTED_MODULE_D: dict[str, ty.Any] = {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
style = Style.from_dict({
|
|
36
|
+
'module_name': '#00dd00',
|
|
37
|
+
'pound': 'ansicyan',
|
|
38
|
+
'sheet_size': '#884444',
|
|
39
|
+
'separator': '#0000aa',
|
|
40
|
+
'status': '#aa0000',
|
|
41
|
+
})
|
|
42
|
+
session = PromptSession(history=FileHistory('history.txt'), style=style)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def show_pdf(pdf: Path):
|
|
46
|
+
s = platform.system()
|
|
47
|
+
if s == 'Darwin':
|
|
48
|
+
import shlex
|
|
49
|
+
subprocess.Popen([f'open {shlex.quote(str(pdf))}'], shell=True)
|
|
50
|
+
elif s == 'Windows':
|
|
51
|
+
subprocess.Popen([str(pdf)], shell=True)
|
|
52
|
+
else:
|
|
53
|
+
print(f'Platform {s} is not supported. Open {str(pdf)} by yourself.')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def show_preview():
|
|
57
|
+
if MODULE is None:
|
|
58
|
+
raise Exception('show_preview called but MODULE is None.')
|
|
59
|
+
if not hasattr(MODULE, 'CONTENT'):
|
|
60
|
+
print(f"Error: {MODULE.__name__} doesn't have CONTENT.")
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
pdf = OUTPUT_DIR / f'{MODULE.__name__}_preview.pdf'
|
|
64
|
+
try:
|
|
65
|
+
print_preview([manuscript_to_artwork(i) for i in MODULE.CONTENT], pdf)
|
|
66
|
+
except PermissionError:
|
|
67
|
+
print('Error: Close the PDF file.')
|
|
68
|
+
return
|
|
69
|
+
except FileNotFoundError as e:
|
|
70
|
+
print(f'File not found: {str(e)}')
|
|
71
|
+
return
|
|
72
|
+
except Exception as e:
|
|
73
|
+
print('Error: show_preview failed. Exception:')
|
|
74
|
+
traceback.print_exception(e)
|
|
75
|
+
return
|
|
76
|
+
show_pdf(pdf)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def rc_map(text: str):
|
|
80
|
+
layout_fn = text.split()[1]
|
|
81
|
+
p = Path(CURRENT_DIR / LAYOUT_DIR_NAME / layout_fn)
|
|
82
|
+
pdf = OUTPUT_DIR / f'{p.stem}_rc_map.pdf'
|
|
83
|
+
try:
|
|
84
|
+
print_rc_map(p, pdf)
|
|
85
|
+
except PermissionError:
|
|
86
|
+
print('Error: Close the PDF file.')
|
|
87
|
+
return
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print('Error: rc_map failed. Exception:')
|
|
90
|
+
traceback.print_exception(e)
|
|
91
|
+
return
|
|
92
|
+
show_pdf(pdf)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def reload():
|
|
96
|
+
global MODULE
|
|
97
|
+
if MODULE is None:
|
|
98
|
+
print('Error: content not loaded yet.')
|
|
99
|
+
return
|
|
100
|
+
try:
|
|
101
|
+
MODULE = importlib.reload(MODULE)
|
|
102
|
+
IMPORTED_MODULE_D[MODULE.__name__] = MODULE
|
|
103
|
+
except Exception as e:
|
|
104
|
+
print(f'Error: {MODULE.__name__} reload failed. Exception:')
|
|
105
|
+
traceback.print_exception(e)
|
|
106
|
+
MODULE = None
|
|
107
|
+
return
|
|
108
|
+
if not hasattr(MODULE, 'CONTENT'):
|
|
109
|
+
print(f"Error: {MODULE.__name__} doesn't have CONTENT.")
|
|
110
|
+
MODULE = None
|
|
111
|
+
return
|
|
112
|
+
show_preview()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def load(text: str):
|
|
116
|
+
global MODULE
|
|
117
|
+
fn = text.split()[1]
|
|
118
|
+
p = Path(CONTENT_DIR / fn).relative_to(CONTENT_DIR)
|
|
119
|
+
module_name = str.join('.', (CONTENT_DIR_NAME, ) + p.parts[:-1] + (p.stem, ))
|
|
120
|
+
importlib.invalidate_caches()
|
|
121
|
+
try:
|
|
122
|
+
if module_name in IMPORTED_MODULE_D:
|
|
123
|
+
MODULE = importlib.reload(IMPORTED_MODULE_D[module_name])
|
|
124
|
+
else:
|
|
125
|
+
MODULE = importlib.import_module(module_name)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
MODULE = None
|
|
128
|
+
print(f'Error: {module_name} import failed. Exception:')
|
|
129
|
+
traceback.print_exception(e)
|
|
130
|
+
return
|
|
131
|
+
IMPORTED_MODULE_D[module_name] = MODULE
|
|
132
|
+
if not hasattr(MODULE, 'CONTENT'):
|
|
133
|
+
print(f"Error: {module_name} doesn't have CONTENT.")
|
|
134
|
+
MODULE = None
|
|
135
|
+
return
|
|
136
|
+
show_preview()
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def remove_file_if_exists(p: Path):
|
|
141
|
+
if not p.exists():
|
|
142
|
+
return True
|
|
143
|
+
try:
|
|
144
|
+
p.unlink()
|
|
145
|
+
except Exception:
|
|
146
|
+
return False
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def pump():
|
|
151
|
+
module_path_completer = PathCompleter(get_paths=lambda: [CONTENT_DIR_NAME], file_filter=lambda fn: os.path.isdir(fn) or fn.endswith('.py'))
|
|
152
|
+
layout_path_completer = PathCompleter(get_paths=lambda: [LAYOUT_DIR_NAME], file_filter=lambda fn: os.path.isdir(fn) or fn.endswith('.json'))
|
|
153
|
+
root_completer = NestedCompleter.from_nested_dict({
|
|
154
|
+
'show': None,
|
|
155
|
+
'reload': None,
|
|
156
|
+
'load': module_path_completer,
|
|
157
|
+
'rc_map': layout_path_completer,
|
|
158
|
+
'help': None,
|
|
159
|
+
'exit': None
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
message = []
|
|
163
|
+
|
|
164
|
+
message += [
|
|
165
|
+
('class:module_name', '('),
|
|
166
|
+
('class:module_name', 'None' if MODULE is None else MODULE.__name__),
|
|
167
|
+
('class:module_name', ')'),
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
message.append(('class:pound', '# '))
|
|
171
|
+
|
|
172
|
+
text: str = session.prompt(message, completer=root_completer, bottom_toolbar='To exit this app, type "exit" and hit Enter key.')
|
|
173
|
+
|
|
174
|
+
if text == 'show':
|
|
175
|
+
show_preview()
|
|
176
|
+
if text.startswith('rc_map'):
|
|
177
|
+
rc_map(text)
|
|
178
|
+
elif text == '' or text == 'reload':
|
|
179
|
+
reload()
|
|
180
|
+
elif text.startswith('load'):
|
|
181
|
+
load(text)
|
|
182
|
+
elif text.startswith('exit'):
|
|
183
|
+
return False
|
|
184
|
+
elif text.startswith('help'):
|
|
185
|
+
print('''Commands:
|
|
186
|
+
load {content script file}:
|
|
187
|
+
Loads the content script and shows the preview.
|
|
188
|
+
reload (or just hit Enter key):
|
|
189
|
+
Reloads current content script and shows the preview.
|
|
190
|
+
show:
|
|
191
|
+
Shows the preview of current content script without reloading.
|
|
192
|
+
rc_map {layout KLE JSON file}:
|
|
193
|
+
Shows Row/Col map of the layout KLE JSON file.
|
|
194
|
+
exit:
|
|
195
|
+
Exits this app.''')
|
|
196
|
+
else:
|
|
197
|
+
print(f'Error: {text} is invalid.')
|
|
198
|
+
|
|
199
|
+
return True
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def repo_ok():
|
|
203
|
+
if not OUTPUT_DIR.is_dir():
|
|
204
|
+
if not remove_file_if_exists(OUTPUT_DIR):
|
|
205
|
+
print(f'Error: Cannot remove {OUTPUT_DIR} file. Please remove the file by yourself.')
|
|
206
|
+
sys.exit(1)
|
|
207
|
+
OUTPUT_DIR.mkdir()
|
|
208
|
+
|
|
209
|
+
if all(d.is_dir() for d in REPO_DIRS + [CURRENT_DIR / VSCODE_DIR_NAME]):
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
result = yes_no_dialog(
|
|
213
|
+
title="keycap-designer",
|
|
214
|
+
text="Do you initialize current directory for keycap-designer?"
|
|
215
|
+
).run()
|
|
216
|
+
|
|
217
|
+
if result:
|
|
218
|
+
import shutil
|
|
219
|
+
for n, d in zip(REPO_DIR_NAMES, REPO_DIRS):
|
|
220
|
+
shutil.copytree(RESOURCE_DIR / 'repo' / n, d, dirs_exist_ok=True)
|
|
221
|
+
shutil.copytree(RESOURCE_DIR / 'repo/vscode', CURRENT_DIR / VSCODE_DIR_NAME, dirs_exist_ok=True)
|
|
222
|
+
|
|
223
|
+
return result
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def main():
|
|
227
|
+
from keycap_designer import version
|
|
228
|
+
print(f'keycap-designer {version} (C) 2023-2025 DecentKeyboards; MIT License')
|
|
229
|
+
if not repo_ok():
|
|
230
|
+
return
|
|
231
|
+
if len(sys.argv) > 1:
|
|
232
|
+
v = sys.argv[1]
|
|
233
|
+
try:
|
|
234
|
+
vp = Path(v)
|
|
235
|
+
except Exception:
|
|
236
|
+
print('Error: The arg {v} is not a valid file path.')
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
try:
|
|
239
|
+
rp = vp.relative_to(CONTENT_DIR)
|
|
240
|
+
except Exception:
|
|
241
|
+
print('Error: The file {v} is not in ./content folder.')
|
|
242
|
+
sys.exit(1)
|
|
243
|
+
load('load ' + str(rp))
|
|
244
|
+
|
|
245
|
+
sys.path.append(os.getcwd())
|
|
246
|
+
print('Type "help" and hit Enter key to show the command help.')
|
|
247
|
+
while True:
|
|
248
|
+
cont = pump()
|
|
249
|
+
if not cont:
|
|
250
|
+
break
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
if __name__ == '__main__':
|
|
254
|
+
main()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
6
|
+
TYPE_CHECKING = False
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
+
else:
|
|
13
|
+
VERSION_TUPLE = object
|
|
14
|
+
|
|
15
|
+
version: str
|
|
16
|
+
__version__: str
|
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
|
18
|
+
version_tuple: VERSION_TUPLE
|
|
19
|
+
|
|
20
|
+
__version__ = version = '0.1.3'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 1, 3)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Margin line width should be 1 pt.
|
|
2
|
+
|
|
3
|
+
For top side, the inner edge of the margin line should be the surface edge of the top side. The fillets are not included.
|
|
4
|
+
|
|
5
|
+
For front side, margin line should be placed on the edge of fillet-less surface. The projection angle is 60 degree.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import typing as ty
|
|
2
|
+
from enum import Enum, auto
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import numpy as np
|
|
6
|
+
from numpy.typing import NDArray
|
|
7
|
+
from PIL import Image as PILImageModule
|
|
8
|
+
import cv2
|
|
9
|
+
import cmm
|
|
10
|
+
from .overwrite_reportlab import overwrite_reportlab
|
|
11
|
+
|
|
12
|
+
ICC_DIR = Path(__file__).parent / 'icc'
|
|
13
|
+
WORKSPACE_PROFILE_PATH = ICC_DIR / 'Linear P3D65.icc'
|
|
14
|
+
WS_HP = None
|
|
15
|
+
with open(ICC_DIR / 'sRGBz.icc', 'rb') as f:
|
|
16
|
+
SRGB_PROF = f.read()
|
|
17
|
+
overwrite_reportlab()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RenderingIntent(Enum):
|
|
21
|
+
Perceptual = cmm.INTENT_PERCEPTUAL
|
|
22
|
+
Relative = cmm.INTENT_RELATIVE_COLORIMETRIC
|
|
23
|
+
Saturation = cmm.INTENT_SATURATION
|
|
24
|
+
Absolute = cmm.INTENT_ABSOLUTE_COLORIMETRIC
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _ColorSpaceType(Enum):
|
|
28
|
+
SRGB = auto()
|
|
29
|
+
WORKSPACE = auto()
|
|
30
|
+
DEVICE_RGB = auto()
|
|
31
|
+
DEVICE_RGB_16 = auto()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_CST_FMT = {
|
|
35
|
+
_ColorSpaceType.SRGB: cmm.get_transform_formatter(0, cmm.PT_RGB, 3, 1, 0, 0),
|
|
36
|
+
_ColorSpaceType.WORKSPACE: cmm.get_transform_formatter(0, cmm.PT_RGB, 3, 2, 0, 0),
|
|
37
|
+
_ColorSpaceType.DEVICE_RGB: cmm.get_transform_formatter(0, cmm.PT_RGB, 3, 1, 0, 0),
|
|
38
|
+
_ColorSpaceType.DEVICE_RGB_16: cmm.get_transform_formatter(0, cmm.PT_RGB, 3, 2, 0, 0)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
with open(WORKSPACE_PROFILE_PATH, 'rb') as f:
|
|
43
|
+
WS_HP = cmm.open_profile_from_mem(f.read())
|
|
44
|
+
del f
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def check_ws_img(ws_img: NDArray[np.uint16]):
|
|
48
|
+
if ws_img.shape[2] != 3:
|
|
49
|
+
raise Exception('ws_img should be BGR image')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class _ColorConversionType:
|
|
54
|
+
source: _ColorSpaceType
|
|
55
|
+
target: _ColorSpaceType
|
|
56
|
+
bpc: bool
|
|
57
|
+
soft_proof: bool = False
|
|
58
|
+
soft_proof_cs: _ColorSpaceType = _ColorSpaceType.SRGB
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ColorConverter:
|
|
62
|
+
def __init__(self, device_rgb_profile_path: Path) -> None:
|
|
63
|
+
self._transform_cache: dict[tuple[_ColorConversionType, RenderingIntent], ty.Any] = {}
|
|
64
|
+
with open(device_rgb_profile_path, 'rb') as f:
|
|
65
|
+
hp = cmm.open_profile_from_mem(f.read())
|
|
66
|
+
self.cst_hp = {
|
|
67
|
+
_ColorSpaceType.SRGB: cmm.create_srgb_profile(),
|
|
68
|
+
_ColorSpaceType.WORKSPACE: WS_HP,
|
|
69
|
+
_ColorSpaceType.DEVICE_RGB: hp,
|
|
70
|
+
_ColorSpaceType.DEVICE_RGB_16: hp
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def _get_transform(self, cct: _ColorConversionType, rendering_intent: RenderingIntent):
|
|
74
|
+
if (cct, rendering_intent) in self._transform_cache:
|
|
75
|
+
return self._transform_cache[cct, rendering_intent]
|
|
76
|
+
bpc = cmm.cmsFLAGS_BLACKPOINTCOMPENSATION if cct.bpc else 0
|
|
77
|
+
if cct.soft_proof:
|
|
78
|
+
tr = cmm.create_proofing_transform(
|
|
79
|
+
self.cst_hp[cct.source], _CST_FMT[cct.source],
|
|
80
|
+
self.cst_hp[cct.soft_proof_cs], _CST_FMT[cct.soft_proof_cs],
|
|
81
|
+
self.cst_hp[_ColorSpaceType.DEVICE_RGB],
|
|
82
|
+
rendering_intent.value, cmm.INTENT_RELATIVE_COLORIMETRIC,
|
|
83
|
+
bpc)
|
|
84
|
+
else:
|
|
85
|
+
tr = cmm.create_transform(
|
|
86
|
+
self.cst_hp[cct.source], _CST_FMT[cct.source],
|
|
87
|
+
self.cst_hp[cct.target], _CST_FMT[cct.target],
|
|
88
|
+
rendering_intent.value,
|
|
89
|
+
bpc)
|
|
90
|
+
self._transform_cache[cct, rendering_intent] = tr
|
|
91
|
+
return tr
|
|
92
|
+
|
|
93
|
+
def workspace_to_device_rgb(self, ws_img: NDArray[np.uint16], rendering_intent: RenderingIntent, bpc=True):
|
|
94
|
+
check_ws_img(ws_img)
|
|
95
|
+
trg_img = np.zeros(ws_img.shape, dtype=np.uint8)
|
|
96
|
+
tr = self._get_transform(_ColorConversionType(_ColorSpaceType.WORKSPACE, _ColorSpaceType.DEVICE_RGB, bpc), rendering_intent)
|
|
97
|
+
cmm.do_transform_16_8(tr, cv2.cvtColor(ws_img, cv2.COLOR_BGR2RGB), trg_img, ws_img.size // 3)
|
|
98
|
+
return PILImageModule.fromarray(trg_img)
|
|
99
|
+
|
|
100
|
+
def workspace_to_device_rgb_as_cv2(self, ws_img: NDArray[np.uint16], rendering_intent: RenderingIntent, bpc=True) -> NDArray[np.uint16]:
|
|
101
|
+
check_ws_img(ws_img)
|
|
102
|
+
trg_img = np.zeros(ws_img.shape, dtype=np.uint16)
|
|
103
|
+
tr = self._get_transform(_ColorConversionType(_ColorSpaceType.WORKSPACE, _ColorSpaceType.DEVICE_RGB_16, bpc), rendering_intent)
|
|
104
|
+
cmm.do_transform_16_16(tr, cv2.cvtColor(ws_img, cv2.COLOR_BGR2RGB), trg_img, ws_img.size // 3)
|
|
105
|
+
return cv2.cvtColor(trg_img, cv2.COLOR_RGB2BGR) # type: ignore
|
|
106
|
+
|
|
107
|
+
def device_rgb_as_cv2_to_workspace(self, device_rgb_img: NDArray[np.uint16]) -> NDArray[np.uint16]:
|
|
108
|
+
trg_img = np.zeros(device_rgb_img.shape, dtype=np.uint16)
|
|
109
|
+
tr = self._get_transform(_ColorConversionType(_ColorSpaceType.DEVICE_RGB_16, _ColorSpaceType.WORKSPACE, False), RenderingIntent.Relative)
|
|
110
|
+
cmm.do_transform_16_16(tr, cv2.cvtColor(device_rgb_img, cv2.COLOR_BGR2RGB), trg_img, device_rgb_img.size // 3)
|
|
111
|
+
return cv2.cvtColor(trg_img, cv2.COLOR_RGB2BGR) # type: ignore
|
|
112
|
+
|
|
113
|
+
def source_to_workspace(self, pil_img: PILImageModule.Image) -> NDArray[np.uint16]:
|
|
114
|
+
src_fmt = cmm.get_transform_formatter(0, cmm.PT_RGB, 3, 1, 0, 0)
|
|
115
|
+
src_hp = None
|
|
116
|
+
if 'icc_profile' in pil_img.info:
|
|
117
|
+
profile_mem = pil_img.info['icc_profile']
|
|
118
|
+
src_hp = cmm.open_profile_from_mem(profile_mem)
|
|
119
|
+
tr = cmm.create_transform(
|
|
120
|
+
src_hp, src_fmt,
|
|
121
|
+
self.cst_hp[_ColorSpaceType.WORKSPACE], _CST_FMT[_ColorSpaceType.WORKSPACE],
|
|
122
|
+
cmm.INTENT_RELATIVE_COLORIMETRIC, 0)
|
|
123
|
+
else:
|
|
124
|
+
tr = self._get_transform(_ColorConversionType(_ColorSpaceType.SRGB, _ColorSpaceType.WORKSPACE, False), RenderingIntent.Relative)
|
|
125
|
+
src_img = np.array(pil_img.convert('RGB'))
|
|
126
|
+
trg_img = np.zeros(src_img.shape, dtype=np.uint16)
|
|
127
|
+
cmm.do_transform_8_16(tr, src_img, trg_img, src_img.size // 3)
|
|
128
|
+
if src_hp is not None:
|
|
129
|
+
cmm.close_profile(src_hp)
|
|
130
|
+
ret = cv2.cvtColor(trg_img, cv2.COLOR_RGB2BGR)
|
|
131
|
+
if 'A' in pil_img.mode:
|
|
132
|
+
ret = cv2.cvtColor(ret, cv2.COLOR_BGR2BGRA)
|
|
133
|
+
ret[:, :, 3] = np.array(pil_img.getchannel('A'), np.uint16) * 257
|
|
134
|
+
return ret # type: ignore
|
|
135
|
+
|
|
136
|
+
def workspace_to_soft_proof(self, ws_img: NDArray[np.uint16], rendering_intent: RenderingIntent, bpc=True):
|
|
137
|
+
'''
|
|
138
|
+
The result is sRGB.
|
|
139
|
+
'''
|
|
140
|
+
check_ws_img(ws_img)
|
|
141
|
+
trg_img = np.zeros(ws_img.shape, dtype=np.uint8)
|
|
142
|
+
tr = self._get_transform(_ColorConversionType(_ColorSpaceType.WORKSPACE, _ColorSpaceType.DEVICE_RGB, bpc, soft_proof=True), rendering_intent)
|
|
143
|
+
cmm.do_transform_16_8(tr, cv2.cvtColor(ws_img, cv2.COLOR_BGR2RGB), trg_img, ws_img.size // 3)
|
|
144
|
+
pimg = PILImageModule.fromarray(trg_img)
|
|
145
|
+
pimg.info['icc_profile'] = SRGB_PROF
|
|
146
|
+
return pimg
|
|
147
|
+
|
|
148
|
+
def workspace_to_soft_proof_as_workspace(self, ws_img: NDArray[np.uint16], rendering_intent: RenderingIntent, bpc=True) -> NDArray[np.uint16]:
|
|
149
|
+
check_ws_img(ws_img)
|
|
150
|
+
trg_img = np.zeros(ws_img.shape, dtype=np.uint16)
|
|
151
|
+
tr = self._get_transform(_ColorConversionType(_ColorSpaceType.WORKSPACE, _ColorSpaceType.DEVICE_RGB, bpc, soft_proof=True, soft_proof_cs=_ColorSpaceType.WORKSPACE), rendering_intent)
|
|
152
|
+
cmm.do_transform_16_16(tr, cv2.cvtColor(ws_img, cv2.COLOR_BGR2RGB), trg_img, ws_img.size // 3)
|
|
153
|
+
return cv2.cvtColor(trg_img, cv2.COLOR_RGB2BGR) # type: ignore
|
|
154
|
+
|
|
155
|
+
def workspace_to_srgb_as_cv2(self, ws_img: NDArray[np.uint16]) -> NDArray[np.uint8]:
|
|
156
|
+
check_ws_img(ws_img)
|
|
157
|
+
trg_img = np.zeros(ws_img.shape, dtype=np.uint8)
|
|
158
|
+
tr = self._get_transform(_ColorConversionType(_ColorSpaceType.WORKSPACE, _ColorSpaceType.SRGB, False), RenderingIntent.Relative)
|
|
159
|
+
cmm.do_transform_16_8(tr, cv2.cvtColor(ws_img, cv2.COLOR_BGR2RGB), trg_img, ws_img.size // 3)
|
|
160
|
+
return cv2.cvtColor(trg_img, cv2.COLOR_RGB2BGR) # type: ignore
|
|
161
|
+
|
|
162
|
+
def workspace_to_srgb(self, ws_img: NDArray[np.uint16]):
|
|
163
|
+
'''
|
|
164
|
+
For debugging.
|
|
165
|
+
'''
|
|
166
|
+
if ws_img.shape[2] == 4:
|
|
167
|
+
ws_img = ws_img[:, :, :3]
|
|
168
|
+
trg_img = np.zeros(ws_img.shape, dtype=np.uint8)
|
|
169
|
+
tr = self._get_transform(_ColorConversionType(_ColorSpaceType.WORKSPACE, _ColorSpaceType.SRGB, False), RenderingIntent.Relative)
|
|
170
|
+
cmm.do_transform_16_8(tr, cv2.cvtColor(ws_img, cv2.COLOR_BGR2RGB), trg_img, ws_img.size // 3)
|
|
171
|
+
pimg = PILImageModule.fromarray(trg_img)
|
|
172
|
+
pimg.info['icc_profile'] = SRGB_PROF
|
|
173
|
+
return pimg
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
DEFAULT_CC = ColorConverter(ICC_DIR / 'sublinova-epson4pigment-PBT-20231121_srgb.icc')
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
from ordered_enum.ordered_enum import OrderedEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Orientation(Enum):
|
|
9
|
+
LeftTop = auto()
|
|
10
|
+
Center = auto()
|
|
11
|
+
RightBottom = auto()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Left = Orientation.LeftTop
|
|
15
|
+
Center = Orientation.Center
|
|
16
|
+
Right = Orientation.RightBottom
|
|
17
|
+
Top = Left
|
|
18
|
+
Bottom = Right
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ImageFit(Enum):
|
|
22
|
+
KeepAspect = auto()
|
|
23
|
+
Crop = auto()
|
|
24
|
+
Expand = auto()
|
|
25
|
+
PixelWise = auto()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
Aspect = ImageFit.KeepAspect
|
|
29
|
+
Crop = ImageFit.Crop
|
|
30
|
+
Expand = ImageFit.Expand
|
|
31
|
+
PixelWise = ImageFit.PixelWise
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ImageInterpolate(Enum):
|
|
35
|
+
Cubic = auto()
|
|
36
|
+
Nearest = auto()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
Cubic = ImageInterpolate.Cubic
|
|
40
|
+
Nearest = ImageInterpolate.Nearest
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ImageTrim(Enum):
|
|
44
|
+
Inner = auto()
|
|
45
|
+
Outer = auto()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
TrimInner = ImageTrim.Inner
|
|
49
|
+
TrimOuter = ImageTrim.Outer
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Side(OrderedEnum):
|
|
53
|
+
Top = auto()
|
|
54
|
+
Front = auto()
|
|
55
|
+
TopFront = auto()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
TopSide = Side.Top
|
|
59
|
+
FrontSide = Side.Front
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
CURRENT_DIR = Path(os.getcwd())
|
|
63
|
+
RESOURCE_DIR = Path(os.path.dirname(__file__))
|
|
64
|
+
OS_FONT_DIR = {
|
|
65
|
+
'Windows': Path(r'C:\Windows\Fonts'),
|
|
66
|
+
'Darwin': Path('/Library/Fonts'),
|
|
67
|
+
'Linux': Path('/usr/share/fonts')
|
|
68
|
+
}[platform.system()]
|
|
69
|
+
APP_FONT_DIR = CURRENT_DIR / 'font'
|
|
70
|
+
DESC_FONT_PATH = RESOURCE_DIR / 'font/NotoSansMono-VariableFont_wdth,wght.ttf'
|
|
71
|
+
DPI = 720
|
|
72
|
+
IPM = 25.4
|
|
73
|
+
DPM = DPI / IPM
|
|
74
|
+
MAX_RANK = 100
|
|
75
|
+
MAX_N_CB = 10
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from importlib import import_module
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import numpy as np
|
|
4
|
+
from skimage.transform import PiecewiseAffineTransform, warp
|
|
5
|
+
import cv2
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
AREA_NAME = 'f1u_convex'
|
|
9
|
+
DATA_DIR = Path(__file__).parent / "junana"
|
|
10
|
+
AREA = True
|
|
11
|
+
|
|
12
|
+
d = 'area' if AREA else 'pattern'
|
|
13
|
+
IMG_FILENAME = str(DATA_DIR / f"{d}/{AREA_NAME}.png")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
image = cv2.imread(IMG_FILENAME, cv2.IMREAD_UNCHANGED)
|
|
17
|
+
if AREA:
|
|
18
|
+
alpha = image[:, :, 3].copy()
|
|
19
|
+
image[alpha == 0] = 255
|
|
20
|
+
image[:, :, 3] = alpha
|
|
21
|
+
else:
|
|
22
|
+
i = np.full(image.shape[:2] + (4, ), 255, np.uint8)
|
|
23
|
+
i[:, :, :3] = image
|
|
24
|
+
image = i
|
|
25
|
+
image[:, :, 3] = 255 - image[:, :, 3] # type: ignore
|
|
26
|
+
rows, cols = image.shape[:2]
|
|
27
|
+
|
|
28
|
+
landmarks = import_module(f'junana.{AREA_NAME}').TABLE
|
|
29
|
+
dst = landmarks[:, 0]
|
|
30
|
+
src = landmarks[:, 1]
|
|
31
|
+
if AREA:
|
|
32
|
+
src, dst = dst, src
|
|
33
|
+
|
|
34
|
+
out_cols, out_rows = src.max(axis=0)
|
|
35
|
+
|
|
36
|
+
tform = PiecewiseAffineTransform()
|
|
37
|
+
_ = tform.estimate(src, dst)
|
|
38
|
+
|
|
39
|
+
out = (warp(image, tform, output_shape=(out_rows, out_cols), cval=0.5) * 255).astype(np.uint8)
|
|
40
|
+
|
|
41
|
+
out[:, :, 3] = 255 - out[:, :, 3]
|
|
42
|
+
|
|
43
|
+
cv2.imshow('img', out)
|
|
44
|
+
cv2.waitKey(0)
|
|
45
|
+
cv2.destroyAllWindows()
|
|
46
|
+
cv2.imwrite('tmp/out.png', out)
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|