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 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,6 @@
1
+ from .encoder import encode_text
2
+ from .decoder import decode_svg
3
+
4
+ __version__ = "1.0.0"
5
+ __author__ = "Abdul"
6
+ __all__ = ["encode_text", "decode_svg"]
@@ -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()