makea-cli 0.2.28__py3-none-any.whl → 0.2.30__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.
- makea_cli/__init__.py +1 -1
- makea_cli/commands/files/cmd.py +2 -2
- makea_cli/file_reader.py +377 -10
- {makea_cli-0.2.28.dist-info → makea_cli-0.2.30.dist-info}/METADATA +4 -4
- {makea_cli-0.2.28.dist-info → makea_cli-0.2.30.dist-info}/RECORD +7 -7
- {makea_cli-0.2.28.dist-info → makea_cli-0.2.30.dist-info}/WHEEL +0 -0
- {makea_cli-0.2.28.dist-info → makea_cli-0.2.30.dist-info}/entry_points.txt +0 -0
makea_cli/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.2.
|
|
1
|
+
__version__ = "0.2.30"
|
makea_cli/commands/files/cmd.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""``makea-cli <persona> get file PATH``: read a file a brand sent, locally.
|
|
2
2
|
|
|
3
|
-
Pattern DXF/RUL, Illustrator, EPS, Photoshop, embroidery (PES/DST/EMB), ZIP,
|
|
3
|
+
Pattern DXF/RUL/PLT, Illustrator, EPS, Photoshop, embroidery (PES/DST/EMB), ZIP,
|
|
4
4
|
AVIF/TIFF/SVG. No API call: the file never leaves the machine. Pictures of what
|
|
5
5
|
the file draws are saved next to it so the caller can look at them (an agent
|
|
6
6
|
reads them with its image tool).
|
|
@@ -21,7 +21,7 @@ from makea_cli.registry import command
|
|
|
21
21
|
console = Console()
|
|
22
22
|
|
|
23
23
|
_HELP = (
|
|
24
|
-
"Read a pattern (DXF/RUL), artwork (AI/EPS/PSD/SVG), embroidery (PES/DST/EMB), ZIP or "
|
|
24
|
+
"Read a pattern (DXF/RUL/PLT), artwork (AI/EPS/PSD/SVG), embroidery (PES/DST/EMB), ZIP or "
|
|
25
25
|
"AVIF/TIFF file on this machine: what it states plus PNG pictures of what it draws."
|
|
26
26
|
)
|
|
27
27
|
|
makea_cli/file_reader.py
CHANGED
|
@@ -33,6 +33,26 @@ _MAX_MEMBERS = 40
|
|
|
33
33
|
_MAX_PICTURES = 8
|
|
34
34
|
_PICTURE_SIDE = 1800
|
|
35
35
|
|
|
36
|
+
# Never decode an image bigger than this. A decoded image costs w x h x 4
|
|
37
|
+
# bytes whatever the file size, and this runs in web workers (previews) as
|
|
38
|
+
# well as Celery: a 410 MB decode killed one and 502'd the site (TECH-145).
|
|
39
|
+
# Same bound as garmentby_service/common/image_limits.MAX_DECODE_PIXELS.
|
|
40
|
+
MAX_DECODE_PIXELS = 120_000_000
|
|
41
|
+
_MAX_PIECES_DRAWN = 40
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _open_bounded(data: bytes):
|
|
45
|
+
"""Open an image and decode it only when it fits MAX_DECODE_PIXELS; JPEGs
|
|
46
|
+
are decoded at reduced scale. Raises ValueError when it is too big."""
|
|
47
|
+
Image = _pil()
|
|
48
|
+
im = Image.open(io.BytesIO(data))
|
|
49
|
+
if im.width * im.height > MAX_DECODE_PIXELS:
|
|
50
|
+
raise ValueError(f"{im.width} x {im.height} px is too large to draw here")
|
|
51
|
+
if im.format == "JPEG":
|
|
52
|
+
im.draft("RGB", (_PICTURE_SIDE, _PICTURE_SIDE))
|
|
53
|
+
im.load()
|
|
54
|
+
return im
|
|
55
|
+
|
|
36
56
|
|
|
37
57
|
@dataclass
|
|
38
58
|
class Picture:
|
|
@@ -109,20 +129,24 @@ def _pil():
|
|
|
109
129
|
Image,
|
|
110
130
|
) # noqa: PLC0415 - optional at import time, required to draw
|
|
111
131
|
|
|
112
|
-
Image.MAX_IMAGE_PIXELS = 400_000_000
|
|
113
132
|
return Image
|
|
114
133
|
except ImportError: # pragma: no cover - Pillow is a dependency
|
|
115
134
|
return None
|
|
116
135
|
|
|
117
136
|
|
|
118
137
|
def _png(im) -> bytes:
|
|
138
|
+
"""Shrink first, then flatten: every conversion of the full-size image is
|
|
139
|
+
another full-size copy in memory."""
|
|
119
140
|
Image = _pil()
|
|
141
|
+
if max(im.size) > _PICTURE_SIDE:
|
|
142
|
+
if im.mode in ("P", "PA", "1"):
|
|
143
|
+
im = im.convert("RGBA") # palette images cannot be resampled
|
|
144
|
+
im.thumbnail((_PICTURE_SIDE, _PICTURE_SIDE))
|
|
120
145
|
if im.mode not in ("RGB", "L"):
|
|
121
146
|
rgba = im.convert("RGBA")
|
|
122
147
|
bg = Image.new("RGB", rgba.size, "white")
|
|
123
148
|
bg.paste(rgba, mask=rgba.split()[-1])
|
|
124
149
|
im = bg
|
|
125
|
-
im.thumbnail((_PICTURE_SIDE, _PICTURE_SIDE))
|
|
126
150
|
buf = io.BytesIO()
|
|
127
151
|
im.save(buf, "PNG", optimize=True)
|
|
128
152
|
return buf.getvalue()
|
|
@@ -499,8 +523,7 @@ def _read_eps(data: bytes, name: str, depth: int) -> Reading:
|
|
|
499
523
|
Image = _pil()
|
|
500
524
|
if tif_len and Image:
|
|
501
525
|
try:
|
|
502
|
-
im =
|
|
503
|
-
im.load()
|
|
526
|
+
im = _open_bounded(_fix_preview_tiff(data[tif_off : tif_off + tif_len]))
|
|
504
527
|
pictures.append(Picture("preview", _png(im)))
|
|
505
528
|
except Exception: # noqa: BLE001 - no preview is still a reading
|
|
506
529
|
pass
|
|
@@ -574,8 +597,11 @@ def _read_psd(data: bytes, name: str, depth: int) -> Reading:
|
|
|
574
597
|
layers = [layer[0] for layer in (getattr(im, "layers", None) or []) if layer[0]]
|
|
575
598
|
if layers:
|
|
576
599
|
lines.append(f"{len(layers)} layer(s): " + "; ".join(layers[:30]) + ".")
|
|
577
|
-
|
|
578
|
-
|
|
600
|
+
if width * height > MAX_DECODE_PIXELS:
|
|
601
|
+
lines.append("Too large to draw here.")
|
|
602
|
+
else:
|
|
603
|
+
im.load()
|
|
604
|
+
pictures.append(Picture("flattened image", _png(im)))
|
|
579
605
|
except Exception: # noqa: BLE001
|
|
580
606
|
lines.append("The flattened image could not be decoded.")
|
|
581
607
|
return Reading("psd", "PSD", " ".join(lines), pictures, readable=bool(pictures))
|
|
@@ -711,8 +737,7 @@ def _read_wilcom(data: bytes, name: str, depth: int) -> Reading:
|
|
|
711
737
|
if Image and ole.exists(stream):
|
|
712
738
|
raw = ole.openstream(stream).read()
|
|
713
739
|
try:
|
|
714
|
-
im =
|
|
715
|
-
im.load()
|
|
740
|
+
im = _open_bounded(zlib.decompress(raw[4:]))
|
|
716
741
|
pictures.append(Picture("stitch-out preview", _png(im)))
|
|
717
742
|
break
|
|
718
743
|
except Exception: # noqa: BLE001
|
|
@@ -974,10 +999,12 @@ def _seam_allowance(b: _Block, cut: Tuple[float, float, float, float]) -> str:
|
|
|
974
999
|
|
|
975
1000
|
|
|
976
1001
|
def _draw_dxf(items: List[Tuple[str, _Block]]) -> List[Picture]:
|
|
977
|
-
"""One sheet with every piece of the base size, numbered
|
|
1002
|
+
"""One sheet with every piece of the base size, numbered (the first
|
|
1003
|
+
_MAX_PIECES_DRAWN, so the sheet stays a bounded size)."""
|
|
978
1004
|
Image = _pil()
|
|
979
1005
|
if not Image or not items:
|
|
980
1006
|
return []
|
|
1007
|
+
items = items[:_MAX_PIECES_DRAWN]
|
|
981
1008
|
from PIL import ImageDraw # noqa: PLC0415
|
|
982
1009
|
|
|
983
1010
|
shapes = []
|
|
@@ -1044,6 +1071,261 @@ def _read_rul(data: bytes, name: str, depth: int) -> Reading:
|
|
|
1044
1071
|
return Reading("grade-rules", "RUL", " ".join(lines) + "\n\n```\n" + text[:6000] + "\n```")
|
|
1045
1072
|
|
|
1046
1073
|
|
|
1074
|
+
# ── Pattern: HPGL plot files (.plt) ──────────────────────────────────────
|
|
1075
|
+
#
|
|
1076
|
+
# What a pattern CAD sends to a cutting or marker plotter: pen-up / pen-down
|
|
1077
|
+
# moves in plotter units (40 to the mm), with piece names and sizes as LB text.
|
|
1078
|
+
# A plot has no pieces, only strokes, so it says what it draws and how big; the
|
|
1079
|
+
# piece table comes from the DXF.
|
|
1080
|
+
|
|
1081
|
+
_HPGL_EXTS = (".plt", ".hpgl", ".hpg", ".hgl")
|
|
1082
|
+
_HPGL_UNITS_PER_MM = 40.0
|
|
1083
|
+
# A full marker is millions of moves; past this the drawing is cut short (and
|
|
1084
|
+
# says so) so a read stays bounded in time and memory.
|
|
1085
|
+
_HPGL_MAX_POINTS = 2_000_000
|
|
1086
|
+
_HPGL_CMD = re.compile(rb"[A-Za-z]{2}")
|
|
1087
|
+
_HPGL_NUM = re.compile(rb"[-+]?(?:\d+\.?\d*|\.\d+)")
|
|
1088
|
+
# PCL / PJL wrapping around HP-GL/2: "ESC%-12345X@PJL ...", "ESC%1B", "ESCE".
|
|
1089
|
+
_PCL_ESCAPE = re.compile(rb"\x1b(?:%-12345X|%-?\d*[AB]|E|\*[a-z]\d*[A-Z])|@PJL[^\n]*\n?")
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
def _is_hpgl(head: bytes, ext: str) -> bool:
|
|
1093
|
+
if ext in _HPGL_EXTS:
|
|
1094
|
+
return True
|
|
1095
|
+
h = _PCL_ESCAPE.sub(b"", head).lstrip(b"\x00\r\n\t ;")
|
|
1096
|
+
return bool(re.match(rb"(?:IN|DF)\s*;\s*(?:[A-Z]{2}[^;]*;\s*){0,6}(?:SP|PU|PD|PA)", h))
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
@dataclass
|
|
1100
|
+
class _Plot:
|
|
1101
|
+
strokes: List[Tuple[int, List[Tuple[float, float]]]] = field(
|
|
1102
|
+
default_factory=list
|
|
1103
|
+
) # (pen, points)
|
|
1104
|
+
labels: List[str] = field(default_factory=list)
|
|
1105
|
+
raw_labels: List[bytes] = field(default_factory=list)
|
|
1106
|
+
points: int = 0
|
|
1107
|
+
truncated: bool = False
|
|
1108
|
+
scaled: bool = False
|
|
1109
|
+
pens: List[int] = field(default_factory=list)
|
|
1110
|
+
|
|
1111
|
+
|
|
1112
|
+
_HPGL_ABS_POINTS = (b"EA", b"RA")
|
|
1113
|
+
_HPGL_REL_POINTS = (b"PR", b"ER", b"RR", b"AR")
|
|
1114
|
+
|
|
1115
|
+
|
|
1116
|
+
def _hpgl_to_plotter(cmd: bytes, params: List[float], absolute: bool, t) -> List[float]:
|
|
1117
|
+
"""A command's coordinates from user units (after SC) to plotter units."""
|
|
1118
|
+
fx, fy, ox, oy = t
|
|
1119
|
+
if cmd == b"CI":
|
|
1120
|
+
return [params[0] * fx] + params[1:] if params else params
|
|
1121
|
+
if cmd in (b"AA", b"AR"):
|
|
1122
|
+
# Centre then sweep angle (degrees) and chord tolerance: only the
|
|
1123
|
+
# centre is a coordinate.
|
|
1124
|
+
head = _hpgl_to_plotter(b"PA" if cmd == b"AA" else b"PR", params[:2], True, t)
|
|
1125
|
+
return head + params[2:]
|
|
1126
|
+
is_abs = cmd == b"PA" or cmd in _HPGL_ABS_POINTS or (cmd in (b"PU", b"PD") and absolute)
|
|
1127
|
+
if not is_abs and cmd not in _HPGL_REL_POINTS + (b"PU", b"PD"):
|
|
1128
|
+
return params
|
|
1129
|
+
if is_abs:
|
|
1130
|
+
return [(ox + v * fx) if k % 2 == 0 else (oy + v * fy) for k, v in enumerate(params)]
|
|
1131
|
+
return [v * (fx if k % 2 == 0 else fy) for k, v in enumerate(params)]
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
def _parse_hpgl(data: bytes) -> _Plot:
|
|
1135
|
+
"""A small HP-GL / HP-GL/2 interpreter: PU PD PA PR, SP, LB (and DT),
|
|
1136
|
+
IP + SC scaling, CI and the rectangles. Anything else is skipped; arcs are
|
|
1137
|
+
joined as chords. Everything is kept in plotter units (40 to the mm)."""
|
|
1138
|
+
data = _PCL_ESCAPE.sub(b"", data)
|
|
1139
|
+
plot = _Plot()
|
|
1140
|
+
x = y = 0.0
|
|
1141
|
+
# User units -> plotter units: X = ox + x * fx. Identity until SC maps the
|
|
1142
|
+
# IP points (some CADs plot in 1/1000 inch: IP0,0,1016,1016;SC0,1000,0,1000).
|
|
1143
|
+
ip: Optional[Tuple[float, float, float, float]] = None
|
|
1144
|
+
fx = fy = 1.0
|
|
1145
|
+
ox = oy = 0.0
|
|
1146
|
+
absolute, pen_down, pen = True, False, 1
|
|
1147
|
+
cur: Optional[List[Tuple[float, float]]] = None
|
|
1148
|
+
label_end = b"\x03"
|
|
1149
|
+
n = len(data)
|
|
1150
|
+
i = 0
|
|
1151
|
+
|
|
1152
|
+
def start():
|
|
1153
|
+
nonlocal cur
|
|
1154
|
+
cur = [(x, y)]
|
|
1155
|
+
plot.strokes.append((pen, cur))
|
|
1156
|
+
|
|
1157
|
+
def move(nx: float, ny: float):
|
|
1158
|
+
nonlocal x, y, cur
|
|
1159
|
+
if plot.points >= _HPGL_MAX_POINTS:
|
|
1160
|
+
plot.truncated = True
|
|
1161
|
+
x, y = nx, ny
|
|
1162
|
+
return
|
|
1163
|
+
if pen_down:
|
|
1164
|
+
if cur is None:
|
|
1165
|
+
start()
|
|
1166
|
+
cur.append((nx, ny))
|
|
1167
|
+
plot.points += 1
|
|
1168
|
+
else:
|
|
1169
|
+
cur = None
|
|
1170
|
+
x, y = nx, ny
|
|
1171
|
+
|
|
1172
|
+
while i < n:
|
|
1173
|
+
m = _HPGL_CMD.search(data, i)
|
|
1174
|
+
if not m:
|
|
1175
|
+
break
|
|
1176
|
+
cmd = m.group().upper()
|
|
1177
|
+
i = m.end()
|
|
1178
|
+
if cmd == b"LB":
|
|
1179
|
+
end = data.find(label_end, i)
|
|
1180
|
+
end = n if end < 0 else end
|
|
1181
|
+
plot.raw_labels.append(data[i:end])
|
|
1182
|
+
i = end + len(label_end)
|
|
1183
|
+
continue
|
|
1184
|
+
if cmd == b"DT":
|
|
1185
|
+
j = i
|
|
1186
|
+
while j < n and data[j : j + 1] in (b" ", b"\t"):
|
|
1187
|
+
j += 1
|
|
1188
|
+
if j < n and data[j : j + 1] not in (b";",):
|
|
1189
|
+
label_end = data[j : j + 1]
|
|
1190
|
+
i = j + 1
|
|
1191
|
+
continue
|
|
1192
|
+
# Parameters run to the next command letter or ';'.
|
|
1193
|
+
j = i
|
|
1194
|
+
while j < n and not (data[j : j + 1].isalpha() or data[j : j + 1] == b";"):
|
|
1195
|
+
j += 1
|
|
1196
|
+
params = [float(v) for v in _HPGL_NUM.findall(data[i:j])]
|
|
1197
|
+
i = j + 1 if j < n and data[j : j + 1] == b";" else j
|
|
1198
|
+
if (fx, fy, ox, oy) != (1.0, 1.0, 0.0, 0.0):
|
|
1199
|
+
params = _hpgl_to_plotter(cmd, params, absolute, (fx, fy, ox, oy))
|
|
1200
|
+
if cmd in (b"PU", b"PD", b"PA", b"PR"):
|
|
1201
|
+
if cmd == b"PU":
|
|
1202
|
+
pen_down = False
|
|
1203
|
+
cur = None
|
|
1204
|
+
elif cmd == b"PD":
|
|
1205
|
+
pen_down = True
|
|
1206
|
+
elif cmd == b"PA":
|
|
1207
|
+
absolute = True
|
|
1208
|
+
else:
|
|
1209
|
+
absolute = False
|
|
1210
|
+
for k in range(0, len(params) - 1, 2):
|
|
1211
|
+
nx, ny = params[k], params[k + 1]
|
|
1212
|
+
move(nx, ny) if absolute else move(x + nx, y + ny)
|
|
1213
|
+
elif cmd == b"SP":
|
|
1214
|
+
pen = int(params[0]) if params else 0
|
|
1215
|
+
cur = None
|
|
1216
|
+
if pen and pen not in plot.pens:
|
|
1217
|
+
plot.pens.append(pen)
|
|
1218
|
+
elif cmd == b"CI" and params:
|
|
1219
|
+
r = params[0]
|
|
1220
|
+
cx, cy = x, y
|
|
1221
|
+
ring = [
|
|
1222
|
+
(cx + r * math.cos(t / 24 * 2 * math.pi), cy + r * math.sin(t / 24 * 2 * math.pi))
|
|
1223
|
+
for t in range(25)
|
|
1224
|
+
]
|
|
1225
|
+
plot.strokes.append((pen, ring))
|
|
1226
|
+
plot.points += len(ring)
|
|
1227
|
+
elif cmd in (b"EA", b"ER", b"RA", b"RR") and len(params) >= 2:
|
|
1228
|
+
x1, y1 = (
|
|
1229
|
+
(params[0], params[1]) if cmd in (b"EA", b"RA") else (x + params[0], y + params[1])
|
|
1230
|
+
)
|
|
1231
|
+
plot.strokes.append((pen, [(x, y), (x1, y), (x1, y1), (x, y1), (x, y)]))
|
|
1232
|
+
plot.points += 5
|
|
1233
|
+
elif cmd in (b"AA", b"AR") and len(params) >= 3:
|
|
1234
|
+
cx, cy = (params[0], params[1]) if cmd == b"AA" else (x + params[0], y + params[1])
|
|
1235
|
+
r = math.hypot(x - cx, y - cy)
|
|
1236
|
+
a0 = math.atan2(y - cy, x - cx)
|
|
1237
|
+
sweep = math.radians(params[2])
|
|
1238
|
+
steps = max(2, int(abs(params[2]) / 10))
|
|
1239
|
+
for k in range(1, steps + 1):
|
|
1240
|
+
a = a0 + sweep * k / steps
|
|
1241
|
+
move(cx + r * math.cos(a), cy + r * math.sin(a))
|
|
1242
|
+
elif cmd == b"IP":
|
|
1243
|
+
ip = tuple(params[:4]) if len(params) >= 4 else None
|
|
1244
|
+
elif cmd == b"SC":
|
|
1245
|
+
if len(params) >= 4 and ip and params[1] != params[0] and params[3] != params[2]:
|
|
1246
|
+
fx = (ip[2] - ip[0]) / (params[1] - params[0])
|
|
1247
|
+
fy = (ip[3] - ip[1]) / (params[3] - params[2])
|
|
1248
|
+
ox, oy = ip[0] - params[0] * fx, ip[1] - params[2] * fy
|
|
1249
|
+
elif len(params) >= 4:
|
|
1250
|
+
plot.scaled = True # scaled against the plotter's own P1/P2: size unknown
|
|
1251
|
+
else:
|
|
1252
|
+
fx = fy = 1.0
|
|
1253
|
+
ox = oy = 0.0
|
|
1254
|
+
elif cmd == b"IN":
|
|
1255
|
+
absolute, pen_down, cur = True, False, None
|
|
1256
|
+
ip, fx, fy, ox, oy = None, 1.0, 1.0, 0.0, 0.0
|
|
1257
|
+
plot.strokes = [(p, pts) for p, pts in plot.strokes if len(pts) > 1]
|
|
1258
|
+
# One encoding for every label, decided on all of them together: a
|
|
1259
|
+
# one-character GBK piece name (领, 前) is no evidence on its own and
|
|
1260
|
+
# reads as Latin or as valid UTF-8 nonsense.
|
|
1261
|
+
joined = _decode_text(b"\x00".join(plot.raw_labels))
|
|
1262
|
+
plot.labels = [t.replace("\r", " ").replace("\n", " ").strip() for t in joined.split("\x00")]
|
|
1263
|
+
plot.labels = [t for t in plot.labels if t]
|
|
1264
|
+
return plot
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
def _read_hpgl(data: bytes, name: str, depth: int) -> Reading:
|
|
1268
|
+
ext = os.path.splitext(name.lower())[1]
|
|
1269
|
+
plot = _parse_hpgl(data)
|
|
1270
|
+
if not plot.strokes:
|
|
1271
|
+
return Reading(
|
|
1272
|
+
"pattern",
|
|
1273
|
+
_format_label(ext),
|
|
1274
|
+
f"{_format_label(ext)} file with no HP-GL drawing in it; it may be a plot file in "
|
|
1275
|
+
"another plotter language. To have it read, send an AAMA/ASTM DXF + RUL export "
|
|
1276
|
+
"of the pattern, or a PDF of the pieces.",
|
|
1277
|
+
readable=False,
|
|
1278
|
+
)
|
|
1279
|
+
pts = [p for _pen, s in plot.strokes for p in s]
|
|
1280
|
+
x0, y0, x1, y1 = _bbox(pts)
|
|
1281
|
+
labels = list(dict.fromkeys(plot.labels))
|
|
1282
|
+
head = ["HP-GL plot file: a pattern or marker as sent to a cutting plotter."]
|
|
1283
|
+
if plot.scaled:
|
|
1284
|
+
head.append(
|
|
1285
|
+
f"It sets its own drawing scale, so its size is in its own units: "
|
|
1286
|
+
f"{_fmt(x1 - x0)} x {_fmt(y1 - y0)}."
|
|
1287
|
+
)
|
|
1288
|
+
else:
|
|
1289
|
+
w_cm = (x1 - x0) / _HPGL_UNITS_PER_MM / 10
|
|
1290
|
+
h_cm = (y1 - y0) / _HPGL_UNITS_PER_MM / 10
|
|
1291
|
+
head.append(f"It draws {_fmt(w_cm)} x {_fmt(h_cm)} cm (40 plotter units to the mm).")
|
|
1292
|
+
head.append(
|
|
1293
|
+
f"{len(plot.strokes)} line(s), {plot.points} point(s), pen(s) {', '.join(map(str, plot.pens)) or '1'}."
|
|
1294
|
+
)
|
|
1295
|
+
if plot.truncated:
|
|
1296
|
+
head.append(f"Only the first {_HPGL_MAX_POINTS} points were read.")
|
|
1297
|
+
body = " ".join(head)
|
|
1298
|
+
if labels:
|
|
1299
|
+
shown = labels[:80]
|
|
1300
|
+
body += "\n\nText in the plot (piece names, sizes and notes, in plot order):\n" + "\n".join(
|
|
1301
|
+
f"- {t}" for t in shown
|
|
1302
|
+
)
|
|
1303
|
+
if len(labels) > len(shown):
|
|
1304
|
+
body += f"\n- … and {len(labels) - len(shown)} more"
|
|
1305
|
+
body += (
|
|
1306
|
+
"\n\nA plot file has no pattern pieces, only strokes, so piece sizes, seam "
|
|
1307
|
+
"allowance and grading are not stated; those come from the DXF + RUL."
|
|
1308
|
+
)
|
|
1309
|
+
return Reading("pattern", _format_label(ext), body, _draw_hpgl(plot, (x0, y0, x1, y1)))
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def _draw_hpgl(plot: _Plot, bb: Tuple[float, float, float, float]) -> List[Picture]:
|
|
1313
|
+
Image = _pil()
|
|
1314
|
+
if not Image:
|
|
1315
|
+
return []
|
|
1316
|
+
from PIL import ImageDraw # noqa: PLC0415
|
|
1317
|
+
|
|
1318
|
+
w, h = max(bb[2] - bb[0], 1e-6), max(bb[3] - bb[1], 1e-6)
|
|
1319
|
+
scale = (_PICTURE_SIDE - 40) / max(w, h)
|
|
1320
|
+
im = Image.new("RGB", (max(1, int(w * scale) + 40), max(1, int(h * scale) + 40)), "white")
|
|
1321
|
+
draw = ImageDraw.Draw(im)
|
|
1322
|
+
colours = ["#111111", "#d0021b", "#4a90e2", "#2e7d32", "#9b9b9b", "#5a12c4", "#e68a00"]
|
|
1323
|
+
for pen, pts in plot.strokes:
|
|
1324
|
+
xy = [(20 + (px - bb[0]) * scale, 20 + (bb[3] - py) * scale) for px, py in pts]
|
|
1325
|
+
draw.line(xy, fill=colours[(max(pen, 1) - 1) % len(colours)], width=2)
|
|
1326
|
+
return [Picture("plot (each pen in its own colour)", _png(im))]
|
|
1327
|
+
|
|
1328
|
+
|
|
1047
1329
|
# ── Archives ─────────────────────────────────────────────────────────────
|
|
1048
1330
|
|
|
1049
1331
|
|
|
@@ -1171,8 +1453,17 @@ def _read_image(data: bytes, name: str, depth: int) -> Reading:
|
|
|
1171
1453
|
ext = os.path.splitext(name.lower())[1]
|
|
1172
1454
|
im = Image.open(io.BytesIO(data))
|
|
1173
1455
|
frames = getattr(im, "n_frames", 1)
|
|
1174
|
-
im.load()
|
|
1175
1456
|
text = f"{im.format or _format_label(ext)} image, {im.width} x {im.height} px"
|
|
1457
|
+
if im.width * im.height > MAX_DECODE_PIXELS:
|
|
1458
|
+
return Reading(
|
|
1459
|
+
"image",
|
|
1460
|
+
im.format or _format_label(ext),
|
|
1461
|
+
text + ". Too large to draw here.",
|
|
1462
|
+
readable=False,
|
|
1463
|
+
)
|
|
1464
|
+
if im.format == "JPEG":
|
|
1465
|
+
im.draft("RGB", (_PICTURE_SIDE, _PICTURE_SIDE))
|
|
1466
|
+
im.load()
|
|
1176
1467
|
dpi = im.info.get("dpi")
|
|
1177
1468
|
if dpi and dpi[0]:
|
|
1178
1469
|
text += f", {dpi[0]:.0f} dpi ({im.width / dpi[0] * 25.4:.0f} x {im.height / dpi[1] * 25.4:.0f} mm)"
|
|
@@ -1230,7 +1521,83 @@ _READERS: List[Tuple[Callable[[bytes, str], bool], Callable[[bytes, str, int], R
|
|
|
1230
1521
|
(_is_stitch, _read_stitch),
|
|
1231
1522
|
(_is_rul, _read_rul),
|
|
1232
1523
|
(_is_dxf, _read_dxf),
|
|
1524
|
+
(_is_hpgl, _read_hpgl),
|
|
1233
1525
|
(_is_zip, _read_zip),
|
|
1234
1526
|
(_is_svg, _read_svg),
|
|
1235
1527
|
(_is_image, _read_image),
|
|
1236
1528
|
]
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
# ── Run in a child process ───────────────────────────────────────────────
|
|
1532
|
+
#
|
|
1533
|
+
# A long-lived worker never gives decoded-image memory back to the system, so
|
|
1534
|
+
# the backend reads through `python -m makea_cli.file_reader <name>` (file on
|
|
1535
|
+
# stdin, JSON on stdout): whatever the read costs is returned when the child
|
|
1536
|
+
# exits (TECH-145).
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
def to_json(reading: Reading) -> dict:
|
|
1540
|
+
import base64 # noqa: PLC0415
|
|
1541
|
+
|
|
1542
|
+
return {
|
|
1543
|
+
"kind": reading.kind,
|
|
1544
|
+
"format": reading.format,
|
|
1545
|
+
"text": reading.text,
|
|
1546
|
+
"readable": reading.readable,
|
|
1547
|
+
"pictures": [
|
|
1548
|
+
{"label": p.label, "png": base64.b64encode(p.png).decode("ascii")}
|
|
1549
|
+
for p in reading.pictures
|
|
1550
|
+
],
|
|
1551
|
+
"members": [[name, to_json(sub)] for name, sub in reading.members],
|
|
1552
|
+
"raw": base64.b64encode(reading.raw).decode("ascii") if reading.raw is not None else None,
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
def from_json(data: dict) -> Reading:
|
|
1557
|
+
import base64 # noqa: PLC0415
|
|
1558
|
+
|
|
1559
|
+
return Reading(
|
|
1560
|
+
kind=data["kind"],
|
|
1561
|
+
format=data["format"],
|
|
1562
|
+
text=data["text"],
|
|
1563
|
+
readable=data["readable"],
|
|
1564
|
+
pictures=[Picture(p["label"], base64.b64decode(p["png"])) for p in data["pictures"]],
|
|
1565
|
+
members=[(name, from_json(sub)) for name, sub in data["members"]],
|
|
1566
|
+
raw=base64.b64decode(data["raw"]) if data.get("raw") is not None else None,
|
|
1567
|
+
)
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
def read_in_child(data: bytes, name: str, *, timeout: float = 90.0) -> Reading:
|
|
1571
|
+
"""read_bytes in a child Python process; a failure or a timeout is a
|
|
1572
|
+
reading that says so, never an exception."""
|
|
1573
|
+
import json # noqa: PLC0415
|
|
1574
|
+
import subprocess # noqa: PLC0415
|
|
1575
|
+
import sys # noqa: PLC0415
|
|
1576
|
+
|
|
1577
|
+
try:
|
|
1578
|
+
done = subprocess.run(
|
|
1579
|
+
[sys.executable, "-m", "makea_cli.file_reader", name],
|
|
1580
|
+
input=data,
|
|
1581
|
+
capture_output=True,
|
|
1582
|
+
timeout=timeout,
|
|
1583
|
+
check=True,
|
|
1584
|
+
)
|
|
1585
|
+
return from_json(json.loads(done.stdout))
|
|
1586
|
+
except Exception as e: # noqa: BLE001 - the file stays uploaded either way
|
|
1587
|
+
detail = (
|
|
1588
|
+
"it took too long" if isinstance(e, subprocess.TimeoutExpired) else type(e).__name__
|
|
1589
|
+
)
|
|
1590
|
+
ext = os.path.splitext(name.lower())[1]
|
|
1591
|
+
return Reading(
|
|
1592
|
+
"unknown",
|
|
1593
|
+
_format_label(ext),
|
|
1594
|
+
f"{_format_label(ext)} file; it could not be read here ({detail}).",
|
|
1595
|
+
readable=False,
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
|
|
1599
|
+
if __name__ == "__main__":
|
|
1600
|
+
import json
|
|
1601
|
+
import sys
|
|
1602
|
+
|
|
1603
|
+
sys.stdout.write(json.dumps(to_json(read_bytes(sys.stdin.buffer.read(), sys.argv[1]))))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: makea-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.30
|
|
4
4
|
Summary: Makea admin CLI (browser login + Makea admin HTTP API)
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: click<9,>=8.1
|
|
@@ -122,7 +122,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
122
122
|
| `makea-cli admin get about` | read | `GET /admin/about/{target}` | one account's About you (`--user`: a supplier account's person gets their factory's page; `--supplier`): facts, removed, unknown, paused, topic keys. | — |
|
|
123
123
|
| `makea-cli admin get active-users` | read | `POST /admin/analytics/query` | Distinct active users per day over the last N days (DAU series). | `analytics-active-users` |
|
|
124
124
|
| `makea-cli admin get analytics-query` | read | `POST /admin/analytics/query` | Run an arbitrary HogQL query (escape hatch). Example: analytics-query "SELECT count() FROM events" | `analytics-query` |
|
|
125
|
-
| `makea-cli admin get file` | read | local (makea_cli.file_reader) | read a pattern (DXF/RUL), artwork (AI/EPS/PSD/SVG), embroidery (PES/DST/EMB), ZIP or AVIF/TIFF file on this machine; saves PNG pictures of what it draws next to it. No API call. | — |
|
|
125
|
+
| `makea-cli admin get file` | read | local (makea_cli.file_reader) | read a pattern (DXF/RUL/PLT), artwork (AI/EPS/PSD/SVG), embroidery (PES/DST/EMB), ZIP or AVIF/TIFF file on this machine; saves PNG pictures of what it draws next to it. No API call. | — |
|
|
126
126
|
| `makea-cli admin get currency-conversion` | read | `GET /admin/fx/rates` | the platform's own exchange rates and rounding (rate table for several --from; one --from with --amount converts it). | `convert-currency` |
|
|
127
127
|
| `makea-cli admin get feature-usage` | read | `POST /admin/analytics/query` | How many distinct users used a feature recently, plus event volume. Matches $pageview path and feature_time_spent eve… | `analytics-feature-usage` |
|
|
128
128
|
| `makea-cli admin get growth-snapshot` | read | `GET /admin/growth/snapshots/{id}` | one reconciled Growth Analysis snapshot (default `latest`); `--block` prints one block (headline, gates, review_items, …). | — |
|
|
@@ -207,7 +207,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
207
207
|
| `makea-cli designer delete sampling-order` | write(never) | `DELETE /designer/sampling_orders/{sampling_order_id}` | delete a DRAFT sample order. The backend refuses any other status. Graded `never`. | — |
|
|
208
208
|
| `makea-cli designer delete shipping-order` | write(never) | `POST /designer/shipping_orders/{shipping_order_id}/cancel` | the endpoint is called cancel but it unlinks every production order and removes the shipment, so it is a delete here.… | — |
|
|
209
209
|
| `makea-cli designer download document` | read | `GET /designer/document/download` | save bytes for user_id + document_id (designer-authenticated; same query contract as admin document download). | `designer-download-document` |
|
|
210
|
-
| `makea-cli designer get file` | read | local (makea_cli.file_reader) | read a pattern (DXF/RUL), artwork (AI/EPS/PSD/SVG), embroidery (PES/DST/EMB), ZIP or AVIF/TIFF file on this machine; saves PNG pictures of what it draws next to it. No API call. | — |
|
|
210
|
+
| `makea-cli designer get file` | read | local (makea_cli.file_reader) | read a pattern (DXF/RUL/PLT), artwork (AI/EPS/PSD/SVG), embroidery (PES/DST/EMB), ZIP or AVIF/TIFF file on this machine; saves PNG pictures of what it draws next to it. No API call. | — |
|
|
211
211
|
| `makea-cli designer get collection` | read | `GET /designer/collections/{collection_id}/overview` | one collection with its products, orders and lifecycle in a single read. | — |
|
|
212
212
|
| `makea-cli designer get lifecycle` | read | `GET /designer/product_orders/{product_order_id}/lifecycle` | which stage this product is in. | — |
|
|
213
213
|
| `makea-cli designer get product` | read | `GET /designer/get_product` | one product: status, documents, prices, links. | — |
|
|
@@ -251,7 +251,7 @@ exit code:`0` 成功 · `2` 用法错 · `3` 未登录或身份与 persona 不
|
|
|
251
251
|
|---|---|---|---|---|
|
|
252
252
|
| `makea-cli supplier create note` | write(ask) | `POST /supplier/products/{id}/channels/{key}/notes` | post a message into a product channel you are part of. | — |
|
|
253
253
|
| `makea-cli supplier download document` | read | `GET /supplier/document/download` | save a document you are allowed to read. | — |
|
|
254
|
-
| `makea-cli supplier get file` | read | local (makea_cli.file_reader) | read a pattern (DXF/RUL), artwork (AI/EPS/PSD/SVG), embroidery (PES/DST/EMB), ZIP or AVIF/TIFF file on this machine; saves PNG pictures of what it draws next to it. No API call. | — |
|
|
254
|
+
| `makea-cli supplier get file` | read | local (makea_cli.file_reader) | read a pattern (DXF/RUL/PLT), artwork (AI/EPS/PSD/SVG), embroidery (PES/DST/EMB), ZIP or AVIF/TIFF file on this machine; saves PNG pictures of what it draws next to it. No API call. | — |
|
|
255
255
|
| `makea-cli supplier get pack` | read | `GET /supplier/quote_requests/{id}/technical-specifications` | the tech pack for an RFQ, as the factory is allowed to see it (`--readable`: labels, values and blank essentials only). | — |
|
|
256
256
|
| `makea-cli supplier get pack-changes` | read | `GET /supplier/products/{product_id}/specification-change-history` | what changed in the pack since you quoted, field by field. | — |
|
|
257
257
|
| `makea-cli supplier get profile` | read | `GET /supplier/profile` | your own supplier profile. | — |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
makea_cli/__init__.py,sha256=
|
|
1
|
+
makea_cli/__init__.py,sha256=R75QzgJ-X9Ixa6UnK5Wm3Jn0Wd9dnVdrtup0aqecq6U,23
|
|
2
2
|
makea_cli/__main__.py,sha256=AkQjxWfKE3w9iD1d6I-2Zqvv2t9cct0oYyFF0UiA-Z0,144
|
|
3
3
|
makea_cli/api_client.py,sha256=jQX6ogn9kt5rHXSmWst0J-1esLb0BYXzDTpPEboEN7Q,80996
|
|
4
4
|
makea_cli/auth_pkce.py,sha256=iXSyJ7DM3qVuj-3FwPiqM25CxXz9ebFs9FNthZdw3ws,7315
|
|
@@ -6,7 +6,7 @@ makea_cli/broker.py,sha256=5O650nAdRuF-u9E1MuVM2jVv796iBdxJGIaJasxHij0,3019
|
|
|
6
6
|
makea_cli/companion.py,sha256=lEEMRDbS-5-CWPawlkPDeCij08pAYW8MsXLVSG3_8V0,10116
|
|
7
7
|
makea_cli/config.py,sha256=hz8MnJSkhxcIEZkcpHR005JrDceo1Xmi8qwIUq50cMU,3003
|
|
8
8
|
makea_cli/errors.py,sha256=_2qKjvkU8YWeBcUd04Yn62lKkZoqQY52kH8klNMyxhM,2780
|
|
9
|
-
makea_cli/file_reader.py,sha256=
|
|
9
|
+
makea_cli/file_reader.py,sha256=UUpFCIklCzSGEp1pcg6Z2xlp3WuYnVA53ZpDmgUNgOU,61489
|
|
10
10
|
makea_cli/identity.py,sha256=pfYT-asaY1ZTzoAzfqI-v4Eyz1RtxqR2YgvxRGx_lxg,2758
|
|
11
11
|
makea_cli/main.py,sha256=oU7cMXiZwCnT8gZS1vvuZYgtpDnyqTtb0dlkKIULZzo,9445
|
|
12
12
|
makea_cli/output.py,sha256=6xOJFjoIBphc10nz53xKCyQiz06an7oGtFJ0bap1G-Y,6739
|
|
@@ -33,7 +33,7 @@ makea_cli/commands/email/cmd.py,sha256=s22JQjJ7bPdpgBaL7Tmr8hx80ki_JN_iGQtUekXmm
|
|
|
33
33
|
makea_cli/commands/factory/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
34
|
makea_cli/commands/factory/cmd.py,sha256=k_UGkqpzxxZhN9WjKt1rb95KOTGHMpMJRpfNHTGZ_nA,18691
|
|
35
35
|
makea_cli/commands/files/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
-
makea_cli/commands/files/cmd.py,sha256=
|
|
36
|
+
makea_cli/commands/files/cmd.py,sha256=Z1LWYmCUf0C8nHCd5LPwdb1sylG5rNA1X4mUWyxZgT4,2620
|
|
37
37
|
makea_cli/commands/flags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
38
|
makea_cli/commands/flags/cmd.py,sha256=BC9Vu2h9HKid5S2SNNUqtgQaqTWRx2Ne529gzGmqrKM,5613
|
|
39
39
|
makea_cli/commands/library/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -55,7 +55,7 @@ makea_cli/commands/supplier/__init__.py,sha256=qfQDeQdQ-B7I_uwFVfcp1vXFUTL4fAcNL
|
|
|
55
55
|
makea_cli/commands/supplier/cmd.py,sha256=3M7OYhBd-uQk5naoM2I87vJ6IA_P-gdrsbIX2vbyDns,63763
|
|
56
56
|
makea_cli/commands/tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
57
57
|
makea_cli/commands/tasks/cmd.py,sha256=72MrVigmjjQCpDVF2a3aJn_gclw2iBbJdFmyd87mXec,8549
|
|
58
|
-
makea_cli-0.2.
|
|
59
|
-
makea_cli-0.2.
|
|
60
|
-
makea_cli-0.2.
|
|
61
|
-
makea_cli-0.2.
|
|
58
|
+
makea_cli-0.2.30.dist-info/METADATA,sha256=XTw-VoX0stheFedgcCuM-P5Hn6jxWGUv5mzP-2CT5-I,44539
|
|
59
|
+
makea_cli-0.2.30.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
60
|
+
makea_cli-0.2.30.dist-info/entry_points.txt,sha256=4x-lbVa1jBbNlkNq7L-rYyfRh2spZmc3BLIzP-Dl-Qk,50
|
|
61
|
+
makea_cli-0.2.30.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|