NX-5 1.0.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.
- nx_5-1.0.0/LICENSE +21 -0
- nx_5-1.0.0/NX_5/__init__.py +6 -0
- nx_5-1.0.0/NX_5/constants.py +49 -0
- nx_5-1.0.0/NX_5/decoder.py +181 -0
- nx_5-1.0.0/NX_5/encoder.py +279 -0
- nx_5-1.0.0/PKG-INFO +679 -0
- nx_5-1.0.0/README.md +628 -0
- nx_5-1.0.0/hello.svg +10 -0
- nx_5-1.0.0/pyproject.toml +74 -0
- nx_5-1.0.0/tests/test_encoder.py +48 -0
nx_5-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mr0cloud
|
|
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.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Mappings
|
|
2
|
+
LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
3
|
+
LETTER_CODES = {}
|
|
4
|
+
for i, letter in enumerate(LETTERS):
|
|
5
|
+
code = format(i + 1, '05b')
|
|
6
|
+
LETTER_CODES[letter] = code
|
|
7
|
+
LETTER_CODES[letter.lower()] = code
|
|
8
|
+
|
|
9
|
+
_TOP_CHARS = (
|
|
10
|
+
'0','1','2','3','4','5','6','7','8','9',
|
|
11
|
+
'(',')', '[',']', '{','}', '<','>',
|
|
12
|
+
'!','@','#','$','%','/','&','*',
|
|
13
|
+
"'",'"', '-','_','=','+',
|
|
14
|
+
)
|
|
15
|
+
TOP_CODES = {ch: format(i, '05b') for i, ch in enumerate(_TOP_CHARS)}
|
|
16
|
+
|
|
17
|
+
_DIGITS = set('0123456789')
|
|
18
|
+
|
|
19
|
+
ALL_CODES = {}
|
|
20
|
+
for ch, code in LETTER_CODES.items():
|
|
21
|
+
color = '#2a6db5' if ch.isupper() else '#c0392b'
|
|
22
|
+
side = 'left' if ch.isupper() else 'right'
|
|
23
|
+
ALL_CODES[ch] = (side, code, color)
|
|
24
|
+
for ch, code in TOP_CODES.items():
|
|
25
|
+
color = '#27ae60' if ch in _DIGITS else '#9b59b6'
|
|
26
|
+
ALL_CODES[ch] = ('top', code, color)
|
|
27
|
+
|
|
28
|
+
# Reverse lookups for decoder
|
|
29
|
+
LEFT_LOOKUP = {format(i+1,'05b'): letter for i,letter in enumerate(LETTERS)}
|
|
30
|
+
RIGHT_LOOKUP = {format(i+1,'05b'): letter.lower() for i,letter in enumerate(LETTERS)}
|
|
31
|
+
TOP_LOOKUP = {format(i,'05b'): ch for i,ch in enumerate(_TOP_CHARS)}
|
|
32
|
+
|
|
33
|
+
COLOR_SIDE = {
|
|
34
|
+
'#2a6db5': 'left',
|
|
35
|
+
'#c0392b': 'right',
|
|
36
|
+
'#27ae60': 'top',
|
|
37
|
+
'#9b59b6': 'top',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# Layout defaults
|
|
41
|
+
INNER_W = 160
|
|
42
|
+
INNER_H = 160
|
|
43
|
+
LAYER_GAP = 150
|
|
44
|
+
NOTCH_DEPTH = 40
|
|
45
|
+
SEGMENTS = 5
|
|
46
|
+
PADDING = 50
|
|
47
|
+
WORD_GAP = 50
|
|
48
|
+
BG_PAD = 80
|
|
49
|
+
STROKE_WIDTH = 7
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NX-5 - Decoder
|
|
3
|
+
Reconstructs text from a Notch-encoded SVG using pure geometry.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import xml.etree.ElementTree as ET
|
|
7
|
+
import re
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from .constants import (
|
|
11
|
+
COLOR_SIDE, LEFT_LOOKUP, RIGHT_LOOKUP, TOP_LOOKUP, NOTCH_DEPTH,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Coordinate parser
|
|
16
|
+
def _parse_coords(d: str):
|
|
17
|
+
"""Extract all (x, y) pairs from an SVG path `d` attribute string"""
|
|
18
|
+
tokens = re.findall(r'[-+]?[0-9]*\.?[0-9]+', d)
|
|
19
|
+
nums = [float(t) for t in tokens]
|
|
20
|
+
return [(nums[i], nums[i+1]) for i in range(0, len(nums)-1, 2)]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Notch detectors
|
|
24
|
+
def _detect_right(pts, depth=NOTCH_DEPTH):
|
|
25
|
+
active = set()
|
|
26
|
+
rx = max(px for px, py in pts)
|
|
27
|
+
sq_y = min(py for px, py in pts if abs(px - (rx-depth)) > 1)
|
|
28
|
+
sq_h = max(py for px, py in pts if abs(px - (rx-depth)) > 1) - sq_y
|
|
29
|
+
seg_h = sq_h / 5
|
|
30
|
+
for i in range(len(pts)-1):
|
|
31
|
+
px1, py1 = pts[i]; px2, py2 = pts[i+1]
|
|
32
|
+
if px2 > px1 and abs(px2-px1-depth) < 1.0:
|
|
33
|
+
seg = round((py1 - sq_y) / seg_h)
|
|
34
|
+
if 0 <= seg < 5:
|
|
35
|
+
active.add(seg)
|
|
36
|
+
return ''.join('1' if s in active else '0' for s in range(5))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _detect_left(pts, depth=NOTCH_DEPTH):
|
|
40
|
+
active = set()
|
|
41
|
+
lx = min(px for px, py in pts)
|
|
42
|
+
sq_y = min(py for px, py in pts if abs(px - (lx+depth)) > 1)
|
|
43
|
+
sq_h = max(py for px, py in pts if abs(px - (lx+depth)) > 1) - sq_y
|
|
44
|
+
seg_h = sq_h / 5
|
|
45
|
+
for i in range(len(pts)-1):
|
|
46
|
+
px1, py1 = pts[i]; px2, py2 = pts[i+1]
|
|
47
|
+
if px1 > px2 and abs(px1-px2-depth) < 1.0:
|
|
48
|
+
seg = round((py1 - sq_y) / seg_h) - 1
|
|
49
|
+
if 0 <= seg < 5:
|
|
50
|
+
active.add(seg)
|
|
51
|
+
return ''.join('1' if s in active else '0' for s in range(5))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _detect_top(pts, depth=NOTCH_DEPTH):
|
|
55
|
+
active = set()
|
|
56
|
+
ty = min(py for px, py in pts)
|
|
57
|
+
sq_x = min(px for px, py in pts if abs(py - (ty+depth)) > 1)
|
|
58
|
+
sq_w = max(px for px, py in pts if abs(py - (ty+depth)) > 1) - sq_x
|
|
59
|
+
seg_w = sq_w / 5
|
|
60
|
+
for i in range(len(pts)-1):
|
|
61
|
+
px1, py1 = pts[i]; px2, py2 = pts[i+1]
|
|
62
|
+
if py1 > py2 and abs(py1-py2-depth) < 1.0:
|
|
63
|
+
seg = round((px1 - sq_x) / seg_w) - 1
|
|
64
|
+
if 0 <= seg < 5:
|
|
65
|
+
active.add(seg)
|
|
66
|
+
return ''.join('1' if s in active else '0' for s in range(5))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# Single path decoder
|
|
70
|
+
def _decode_path(path_el):
|
|
71
|
+
"""Decode one <path> element => (character, bounding_area) or None"""
|
|
72
|
+
stroke = path_el.get('stroke', '').lower().strip()
|
|
73
|
+
side = COLOR_SIDE.get(stroke)
|
|
74
|
+
if side is None:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
d = path_el.get('d', '')
|
|
78
|
+
pts = _parse_coords(d)
|
|
79
|
+
if not pts:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
if side == 'right':
|
|
83
|
+
code = _detect_right(pts)
|
|
84
|
+
lookup = RIGHT_LOOKUP
|
|
85
|
+
elif side == 'left':
|
|
86
|
+
code = _detect_left(pts)
|
|
87
|
+
lookup = LEFT_LOOKUP
|
|
88
|
+
else:
|
|
89
|
+
code = _detect_top(pts)
|
|
90
|
+
lookup = TOP_LOOKUP
|
|
91
|
+
|
|
92
|
+
ch = lookup.get(code)
|
|
93
|
+
if ch is None:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
xs = [px for px, py in pts]
|
|
97
|
+
ys = [py for px, py in pts]
|
|
98
|
+
area = (max(xs)-min(xs)) * (max(ys)-min(ys))
|
|
99
|
+
return ch, area
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def decode_svg(input_file: str) -> str:
|
|
104
|
+
if not os.path.exists(input_file):
|
|
105
|
+
raise FileNotFoundError(f"File not found: {input_file}")
|
|
106
|
+
|
|
107
|
+
tree = ET.parse(input_file)
|
|
108
|
+
root = tree.getroot()
|
|
109
|
+
|
|
110
|
+
for el in root.iter():
|
|
111
|
+
el.tag = el.tag.split('}')[-1]
|
|
112
|
+
|
|
113
|
+
groups = [el for el in root if el.tag == 'g']
|
|
114
|
+
if not groups:
|
|
115
|
+
raise ValueError("No word groups found, is this a NX-5 encoded SVG?")
|
|
116
|
+
|
|
117
|
+
decoded_words = []
|
|
118
|
+
|
|
119
|
+
for g_idx, group in enumerate(groups):
|
|
120
|
+
paths = group.findall('.//path')
|
|
121
|
+
if not paths:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
chars = []
|
|
125
|
+
for path_el in paths:
|
|
126
|
+
result = _decode_path(path_el)
|
|
127
|
+
if result:
|
|
128
|
+
chars.append(result)
|
|
129
|
+
|
|
130
|
+
if not chars:
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
chars.sort(key=lambda x: x[1])
|
|
134
|
+
word = ''.join(ch for ch, _ in chars)
|
|
135
|
+
decoded_words.append(word)
|
|
136
|
+
|
|
137
|
+
print(f" Word {g_idx+1}: '{word}'")
|
|
138
|
+
for ch, area in chars:
|
|
139
|
+
print(f" {repr(ch):4} area={area:.0f}")
|
|
140
|
+
|
|
141
|
+
return ' '.join(decoded_words)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# CLI
|
|
145
|
+
def main():
|
|
146
|
+
import sys
|
|
147
|
+
|
|
148
|
+
if len(sys.argv) > 1:
|
|
149
|
+
print(f"\nDecoding: {sys.argv[1]}\n")
|
|
150
|
+
try:
|
|
151
|
+
result = decode_svg(sys.argv[1])
|
|
152
|
+
print(f"\n Decoded: '{result}'\n")
|
|
153
|
+
except (FileNotFoundError, ValueError) as e:
|
|
154
|
+
print(f" X {e}")
|
|
155
|
+
sys.exit(0)
|
|
156
|
+
|
|
157
|
+
print("""
|
|
158
|
+
||============================================||
|
|
159
|
+
|| NX-5 Decoder v1.0 ||
|
|
160
|
+
|| reads svg files from the encoder ||
|
|
161
|
+
||============================================||
|
|
162
|
+
""")
|
|
163
|
+
print(" Type 'quit' or 'exit' to stop\n")
|
|
164
|
+
|
|
165
|
+
while True:
|
|
166
|
+
try:
|
|
167
|
+
svg_file = input("SVG file to decode: ").strip()
|
|
168
|
+
except (EOFError, KeyboardInterrupt):
|
|
169
|
+
print("\nBye!"); break
|
|
170
|
+
if not svg_file: continue
|
|
171
|
+
if svg_file.lower() in ('quit', 'exit'): print("Bye!"); break
|
|
172
|
+
print()
|
|
173
|
+
try:
|
|
174
|
+
result = decode_svg(svg_file)
|
|
175
|
+
print(f"\nDecoded: '{result}'\n")
|
|
176
|
+
except (FileNotFoundError, ValueError) as e:
|
|
177
|
+
print(f" X {e}\n")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == '__main__':
|
|
181
|
+
main()
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NX-5 - Encoder
|
|
3
|
+
Converts text into nested-square SVG (optionally PNG) output
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .constants import (
|
|
7
|
+
ALL_CODES, INNER_W, INNER_H, LAYER_GAP, NOTCH_DEPTH,
|
|
8
|
+
SEGMENTS, PADDING, WORD_GAP, BG_PAD, STROKE_WIDTH,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Path builders
|
|
13
|
+
def _build_path(x, y, w, h, side, code):
|
|
14
|
+
"""Return SVG path `d` string for a square with notches on one side"""
|
|
15
|
+
seg_h = h / SEGMENTS
|
|
16
|
+
seg_w = w / SEGMENTS
|
|
17
|
+
|
|
18
|
+
if side == 'right':
|
|
19
|
+
rx = x + w
|
|
20
|
+
d = f'M {x:.1f},{y:.1f} L {rx:.1f},{y:.1f} '
|
|
21
|
+
for s in range(SEGMENTS):
|
|
22
|
+
y1 = y + s * seg_h; y2 = y + (s+1) * seg_h
|
|
23
|
+
if code[s] == '1':
|
|
24
|
+
d += (
|
|
25
|
+
f'L {rx:.1f},{y1:.1f} L {rx+NOTCH_DEPTH:.1f},{y1:.1f} '
|
|
26
|
+
f'L {rx+NOTCH_DEPTH:.1f},{y2:.1f} L {rx:.1f},{y2:.1f} '
|
|
27
|
+
)
|
|
28
|
+
else:
|
|
29
|
+
d += f'L {rx:.1f},{y2:.1f} '
|
|
30
|
+
d += f'L {x:.1f},{y+h:.1f} Z'
|
|
31
|
+
|
|
32
|
+
elif side == 'left':
|
|
33
|
+
lx = x
|
|
34
|
+
d = (
|
|
35
|
+
f'M {lx:.1f},{y:.1f} L {lx+w:.1f},{y:.1f} '
|
|
36
|
+
f'L {lx+w:.1f},{y+h:.1f} L {lx:.1f},{y+h:.1f} '
|
|
37
|
+
)
|
|
38
|
+
for s in range(SEGMENTS-1, -1, -1):
|
|
39
|
+
y1 = y + s * seg_h; y2 = y + (s+1) * seg_h
|
|
40
|
+
if code[s] == '1':
|
|
41
|
+
d += (
|
|
42
|
+
f'L {lx:.1f},{y2:.1f} L {lx-NOTCH_DEPTH:.1f},{y2:.1f} '
|
|
43
|
+
f'L {lx-NOTCH_DEPTH:.1f},{y1:.1f} L {lx:.1f},{y1:.1f} '
|
|
44
|
+
)
|
|
45
|
+
else:
|
|
46
|
+
d += f'L {lx:.1f},{y1:.1f} '
|
|
47
|
+
d += 'Z'
|
|
48
|
+
|
|
49
|
+
elif side == 'top':
|
|
50
|
+
ty = y
|
|
51
|
+
d = (
|
|
52
|
+
f'M {x:.1f},{ty:.1f} L {x:.1f},{y+h:.1f} '
|
|
53
|
+
f'L {x+w:.1f},{y+h:.1f} L {x+w:.1f},{ty:.1f} '
|
|
54
|
+
)
|
|
55
|
+
for s in range(SEGMENTS-1, -1, -1):
|
|
56
|
+
x1 = x + s * seg_w; x2 = x + (s+1) * seg_w
|
|
57
|
+
if code[s] == '1':
|
|
58
|
+
d += (
|
|
59
|
+
f'L {x2:.1f},{ty:.1f} L {x2:.1f},{ty-NOTCH_DEPTH:.1f} '
|
|
60
|
+
f'L {x1:.1f},{ty-NOTCH_DEPTH:.1f} L {x1:.1f},{ty:.1f} '
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
d += f'L {x1:.1f},{ty:.1f} '
|
|
64
|
+
d += 'Z'
|
|
65
|
+
|
|
66
|
+
return d
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _build_pillow_pts(x, y, w, h, side, code):
|
|
70
|
+
"""Same geometry as _build_path but returns a point list for Pillow"""
|
|
71
|
+
seg_h = h / SEGMENTS
|
|
72
|
+
seg_w = w / SEGMENTS
|
|
73
|
+
pts = []
|
|
74
|
+
|
|
75
|
+
if side == 'right':
|
|
76
|
+
rx = x + w
|
|
77
|
+
pts += [(x, y), (rx, y)]
|
|
78
|
+
for s in range(SEGMENTS):
|
|
79
|
+
y1 = y + s * seg_h; y2 = y + (s+1) * seg_h
|
|
80
|
+
if code[s] == '1':
|
|
81
|
+
pts += [(rx, y1), (rx+NOTCH_DEPTH, y1),
|
|
82
|
+
(rx+NOTCH_DEPTH, y2), (rx, y2)]
|
|
83
|
+
else:
|
|
84
|
+
pts.append((rx, y2))
|
|
85
|
+
pts.append((x, y+h))
|
|
86
|
+
|
|
87
|
+
elif side == 'left':
|
|
88
|
+
lx = x
|
|
89
|
+
pts += [(lx, y), (lx+w, y), (lx+w, y+h), (lx, y+h)]
|
|
90
|
+
for s in range(SEGMENTS-1, -1, -1):
|
|
91
|
+
y1 = y + s * seg_h; y2 = y + (s+1) * seg_h
|
|
92
|
+
if code[s] == '1':
|
|
93
|
+
pts += [(lx, y2), (lx-NOTCH_DEPTH, y2),
|
|
94
|
+
(lx-NOTCH_DEPTH, y1), (lx, y1)]
|
|
95
|
+
else:
|
|
96
|
+
pts.append((lx, y1))
|
|
97
|
+
|
|
98
|
+
elif side == 'top':
|
|
99
|
+
ty = y
|
|
100
|
+
pts += [(x, ty), (x, y+h), (x+w, y+h), (x+w, ty)]
|
|
101
|
+
for s in range(SEGMENTS-1, -1, -1):
|
|
102
|
+
x1 = x + s * seg_w; x2 = x + (s+1) * seg_w
|
|
103
|
+
if code[s] == '1':
|
|
104
|
+
pts += [(x2, ty), (x2, ty-NOTCH_DEPTH),
|
|
105
|
+
(x1, ty-NOTCH_DEPTH), (x1, ty)]
|
|
106
|
+
else:
|
|
107
|
+
pts.append((x1, ty))
|
|
108
|
+
|
|
109
|
+
return [(int(px), int(py)) for px, py in pts]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Word encoder
|
|
113
|
+
def _encode_word(word):
|
|
114
|
+
chars = [c for c in word if c in ALL_CODES]
|
|
115
|
+
n = len(chars)
|
|
116
|
+
if not n:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
total_w = INNER_W + 2*(n-1)*LAYER_GAP + 2*PADDING + NOTCH_DEPTH + 4
|
|
120
|
+
total_h = INNER_H + 2*(n-1)*LAYER_GAP + 2*PADDING + NOTCH_DEPTH
|
|
121
|
+
cx = total_w / 2
|
|
122
|
+
cy = total_h / 2 + NOTCH_DEPTH / 2
|
|
123
|
+
|
|
124
|
+
paths = []
|
|
125
|
+
for i, ch in enumerate(chars):
|
|
126
|
+
side, code, color = ALL_CODES[ch]
|
|
127
|
+
w = INNER_W + 2*i*LAYER_GAP
|
|
128
|
+
h = INNER_H + 2*i*LAYER_GAP
|
|
129
|
+
x = cx - w/2
|
|
130
|
+
y = cy - h/2
|
|
131
|
+
d = _build_path(x, y, w, h, side, code)
|
|
132
|
+
paths.append(
|
|
133
|
+
f' <path d="{d}" fill="none" stroke="{color}" '
|
|
134
|
+
f'stroke-width="{STROKE_WIDTH}" stroke-linejoin="round"/>'
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
svg = (
|
|
138
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" '
|
|
139
|
+
f'width="{total_w:.0f}" height="{total_h:.0f}" '
|
|
140
|
+
f'viewBox="0 0 {total_w:.1f} {total_h:.1f}">\n'
|
|
141
|
+
f' <rect width="100%" height="100%" fill="#0f0f0f"/>\n'
|
|
142
|
+
+ '\n'.join(paths) + '\n</svg>'
|
|
143
|
+
)
|
|
144
|
+
return svg, total_w, total_h
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# PNG renderer
|
|
148
|
+
def _hex_to_rgb(hex_color):
|
|
149
|
+
h = hex_color.lstrip('#')
|
|
150
|
+
return tuple(int(h[i:i+2], 16) for i in (0, 2, 4))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _render_png(word_data, total_w, total_h, png_file):
|
|
154
|
+
from PIL import Image, ImageDraw
|
|
155
|
+
img = Image.new('RGB', (int(total_w), int(total_h)), (15, 15, 15))
|
|
156
|
+
draw = ImageDraw.Draw(img)
|
|
157
|
+
lw = max(2, STROKE_WIDTH)
|
|
158
|
+
|
|
159
|
+
cursor_x = BG_PAD
|
|
160
|
+
max_h = max(h for _, (_, _, h) in word_data)
|
|
161
|
+
|
|
162
|
+
for word, (_, w, h) in word_data:
|
|
163
|
+
chars = [c for c in word if c in ALL_CODES]
|
|
164
|
+
n = len(chars)
|
|
165
|
+
vert_off = BG_PAD + (max_h - h) / 2
|
|
166
|
+
|
|
167
|
+
inner_total_w = INNER_W + 2*(n-1)*LAYER_GAP + 2*PADDING + NOTCH_DEPTH + 4
|
|
168
|
+
inner_total_h = INNER_H + 2*(n-1)*LAYER_GAP + 2*PADDING + NOTCH_DEPTH
|
|
169
|
+
cx = cursor_x + inner_total_w / 2
|
|
170
|
+
cy = vert_off + inner_total_h / 2 + NOTCH_DEPTH / 2
|
|
171
|
+
|
|
172
|
+
for i, ch in enumerate(chars):
|
|
173
|
+
side, code, hex_color = ALL_CODES[ch]
|
|
174
|
+
color = _hex_to_rgb(hex_color)
|
|
175
|
+
sq_w = INNER_W + 2*i*LAYER_GAP
|
|
176
|
+
sq_h = INNER_H + 2*i*LAYER_GAP
|
|
177
|
+
x = cx - sq_w/2
|
|
178
|
+
y = cy - sq_h/2
|
|
179
|
+
pts = _build_pillow_pts(x, y, sq_w, sq_h, side, code)
|
|
180
|
+
draw.line(pts + [pts[0]], fill=color, width=lw, joint='curve')
|
|
181
|
+
|
|
182
|
+
cursor_x += w + WORD_GAP
|
|
183
|
+
|
|
184
|
+
img.save(png_file)
|
|
185
|
+
print(f" PNG: {png_file}")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def encode_text(text: str, output_file: str = 'output.svg', export_png: bool = False):
|
|
190
|
+
words = [w for w in text.split(' ') if w]
|
|
191
|
+
word_data = [(w, _encode_word(w)) for w in words]
|
|
192
|
+
word_data = [(w, r) for w, r in word_data if r]
|
|
193
|
+
|
|
194
|
+
if not word_data:
|
|
195
|
+
print("No valid characters to encode")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
max_h = max(h for _, (_, _, h) in word_data)
|
|
199
|
+
total_w = (sum(w for _, (_, w, _) in word_data)
|
|
200
|
+
+ WORD_GAP * (len(word_data)-1) + 2*BG_PAD)
|
|
201
|
+
total_h = max_h + 2*BG_PAD
|
|
202
|
+
|
|
203
|
+
parts = []
|
|
204
|
+
cursor_x = BG_PAD
|
|
205
|
+
|
|
206
|
+
for word, (svg_str, w, h) in word_data:
|
|
207
|
+
lines = svg_str.strip().split('\n')
|
|
208
|
+
inner_lines = [l for l in lines[1:-1] if '<rect' not in l]
|
|
209
|
+
vert_off = BG_PAD + (max_h - h) / 2
|
|
210
|
+
|
|
211
|
+
parts.append(f' <g transform="translate({cursor_x:.1f},{vert_off:.1f})">')
|
|
212
|
+
parts.extend(inner_lines)
|
|
213
|
+
parts.append(' </g>')
|
|
214
|
+
|
|
215
|
+
cursor_x += w + WORD_GAP
|
|
216
|
+
|
|
217
|
+
final_svg = (
|
|
218
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" '
|
|
219
|
+
f'width="{total_w:.0f}" height="{total_h:.0f}" '
|
|
220
|
+
f'viewBox="0 0 {total_w:.1f} {total_h:.1f}">\n'
|
|
221
|
+
f' <rect width="100%" height="100%" fill="#0f0f0f"/>\n'
|
|
222
|
+
+ '\n'.join(parts) + '\n</svg>'
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
with open(output_file, 'w') as f:
|
|
226
|
+
f.write(final_svg)
|
|
227
|
+
|
|
228
|
+
if export_png:
|
|
229
|
+
png_file = output_file.replace('.svg', '.png')
|
|
230
|
+
_render_png(word_data, total_w, total_h, png_file)
|
|
231
|
+
|
|
232
|
+
print(f"Encoded '{text}' → {output_file}")
|
|
233
|
+
print(f"Words : {len(word_data)}")
|
|
234
|
+
print(f"Canvas: {total_w:.0f} × {total_h:.0f} px")
|
|
235
|
+
for word, (_, w, h) in word_data:
|
|
236
|
+
chars = [c for c in word if c in ALL_CODES]
|
|
237
|
+
info = [f"{repr(c)}={ALL_CODES[c][0][0].upper()}:{ALL_CODES[c][1]}" for c in chars]
|
|
238
|
+
print(f" '{word}': {' | '.join(info)}")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# CLI
|
|
242
|
+
def main():
|
|
243
|
+
import sys
|
|
244
|
+
args = [a for a in sys.argv[1:] if a != '--png']
|
|
245
|
+
want_png = '--png' in sys.argv[1:]
|
|
246
|
+
|
|
247
|
+
if args:
|
|
248
|
+
text = ' '.join(args)
|
|
249
|
+
encode_text(text, text.replace(' ', '_') + '.svg', export_png=want_png)
|
|
250
|
+
sys.exit(0)
|
|
251
|
+
|
|
252
|
+
print("""
|
|
253
|
+
||============================================||
|
|
254
|
+
|| NX-5 Encoder v1.0 ||
|
|
255
|
+
|| left=CAP - right=lower - top=0-9+symbols ||
|
|
256
|
+
||============================================||
|
|
257
|
+
""")
|
|
258
|
+
|
|
259
|
+
print(" Type 'quit' or 'exit' to stop\n")
|
|
260
|
+
|
|
261
|
+
while True:
|
|
262
|
+
try:
|
|
263
|
+
text = input("Enter text to encode: ").strip()
|
|
264
|
+
except (EOFError, KeyboardInterrupt):
|
|
265
|
+
print("\nBye!"); break
|
|
266
|
+
if not text: print("Enter some text\n"); continue
|
|
267
|
+
if text.lower() in ('quit', 'exit'): print("Bye!"); break
|
|
268
|
+
bad = [c for c in text if c != ' ' and c not in ALL_CODES]
|
|
269
|
+
if bad:
|
|
270
|
+
print(f"Unsupported: {set(bad)}")
|
|
271
|
+
print("A-Z a-z 0-9 ( ) [ ] {{ }} < > ! @ # $ % / & * ' \" - _ = +\n")
|
|
272
|
+
continue
|
|
273
|
+
want_png = input(" export PNG? (y/N): ").strip().lower() == 'y'
|
|
274
|
+
encode_text(text, text.replace(' ', '_') + '.svg', export_png=want_png)
|
|
275
|
+
print()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == '__main__':
|
|
279
|
+
main()
|