makea-cli 0.2.28__py3-none-any.whl → 0.2.29__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 CHANGED
@@ -1 +1 @@
1
- __version__ = "0.2.28"
1
+ __version__ = "0.2.29"
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 = Image.open(io.BytesIO(_fix_preview_tiff(data[tif_off : tif_off + tif_len])))
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
- im.load()
578
- pictures.append(Picture("flattened image", _png(im)))
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 = Image.open(io.BytesIO(zlib.decompress(raw[4:])))
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 = []
@@ -1171,8 +1198,17 @@ def _read_image(data: bytes, name: str, depth: int) -> Reading:
1171
1198
  ext = os.path.splitext(name.lower())[1]
1172
1199
  im = Image.open(io.BytesIO(data))
1173
1200
  frames = getattr(im, "n_frames", 1)
1174
- im.load()
1175
1201
  text = f"{im.format or _format_label(ext)} image, {im.width} x {im.height} px"
1202
+ if im.width * im.height > MAX_DECODE_PIXELS:
1203
+ return Reading(
1204
+ "image",
1205
+ im.format or _format_label(ext),
1206
+ text + ". Too large to draw here.",
1207
+ readable=False,
1208
+ )
1209
+ if im.format == "JPEG":
1210
+ im.draft("RGB", (_PICTURE_SIDE, _PICTURE_SIDE))
1211
+ im.load()
1176
1212
  dpi = im.info.get("dpi")
1177
1213
  if dpi and dpi[0]:
1178
1214
  text += f", {dpi[0]:.0f} dpi ({im.width / dpi[0] * 25.4:.0f} x {im.height / dpi[1] * 25.4:.0f} mm)"
@@ -1234,3 +1270,78 @@ _READERS: List[Tuple[Callable[[bytes, str], bool], Callable[[bytes, str, int], R
1234
1270
  (_is_svg, _read_svg),
1235
1271
  (_is_image, _read_image),
1236
1272
  ]
1273
+
1274
+
1275
+ # ── Run in a child process ───────────────────────────────────────────────
1276
+ #
1277
+ # A long-lived worker never gives decoded-image memory back to the system, so
1278
+ # the backend reads through `python -m makea_cli.file_reader <name>` (file on
1279
+ # stdin, JSON on stdout): whatever the read costs is returned when the child
1280
+ # exits (TECH-145).
1281
+
1282
+
1283
+ def to_json(reading: Reading) -> dict:
1284
+ import base64 # noqa: PLC0415
1285
+
1286
+ return {
1287
+ "kind": reading.kind,
1288
+ "format": reading.format,
1289
+ "text": reading.text,
1290
+ "readable": reading.readable,
1291
+ "pictures": [
1292
+ {"label": p.label, "png": base64.b64encode(p.png).decode("ascii")}
1293
+ for p in reading.pictures
1294
+ ],
1295
+ "members": [[name, to_json(sub)] for name, sub in reading.members],
1296
+ "raw": base64.b64encode(reading.raw).decode("ascii") if reading.raw is not None else None,
1297
+ }
1298
+
1299
+
1300
+ def from_json(data: dict) -> Reading:
1301
+ import base64 # noqa: PLC0415
1302
+
1303
+ return Reading(
1304
+ kind=data["kind"],
1305
+ format=data["format"],
1306
+ text=data["text"],
1307
+ readable=data["readable"],
1308
+ pictures=[Picture(p["label"], base64.b64decode(p["png"])) for p in data["pictures"]],
1309
+ members=[(name, from_json(sub)) for name, sub in data["members"]],
1310
+ raw=base64.b64decode(data["raw"]) if data.get("raw") is not None else None,
1311
+ )
1312
+
1313
+
1314
+ def read_in_child(data: bytes, name: str, *, timeout: float = 90.0) -> Reading:
1315
+ """read_bytes in a child Python process; a failure or a timeout is a
1316
+ reading that says so, never an exception."""
1317
+ import json # noqa: PLC0415
1318
+ import subprocess # noqa: PLC0415
1319
+ import sys # noqa: PLC0415
1320
+
1321
+ try:
1322
+ done = subprocess.run(
1323
+ [sys.executable, "-m", "makea_cli.file_reader", name],
1324
+ input=data,
1325
+ capture_output=True,
1326
+ timeout=timeout,
1327
+ check=True,
1328
+ )
1329
+ return from_json(json.loads(done.stdout))
1330
+ except Exception as e: # noqa: BLE001 - the file stays uploaded either way
1331
+ detail = (
1332
+ "it took too long" if isinstance(e, subprocess.TimeoutExpired) else type(e).__name__
1333
+ )
1334
+ ext = os.path.splitext(name.lower())[1]
1335
+ return Reading(
1336
+ "unknown",
1337
+ _format_label(ext),
1338
+ f"{_format_label(ext)} file; it could not be read here ({detail}).",
1339
+ readable=False,
1340
+ )
1341
+
1342
+
1343
+ if __name__ == "__main__":
1344
+ import json
1345
+ import sys
1346
+
1347
+ 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.28
3
+ Version: 0.2.29
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
@@ -1,4 +1,4 @@
1
- makea_cli/__init__.py,sha256=KP-rlT7kt2ra8vABrHAwk3sAxXx9YgGE14_P5mGaLL4,23
1
+ makea_cli/__init__.py,sha256=UMXg-6yePyXOpL1XY9AsgEVhTAokVdbXvVLiJBpkwPI,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=NQamUyEdNtSw4e1dX_ysHKaocRkcMw6T5P7vlKJhMDU,46625
9
+ makea_cli/file_reader.py,sha256=vY1WlCAeutrDxUSMHMwYmaQnoObxrrLzGUH4vmKuwLo,50825
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
@@ -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.28.dist-info/METADATA,sha256=Og-CWSQOx383bH_VOY3ETP-JFtPjqQuz195eHrjyhn4,44527
59
- makea_cli-0.2.28.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
60
- makea_cli-0.2.28.dist-info/entry_points.txt,sha256=4x-lbVa1jBbNlkNq7L-rYyfRh2spZmc3BLIzP-Dl-Qk,50
61
- makea_cli-0.2.28.dist-info/RECORD,,
58
+ makea_cli-0.2.29.dist-info/METADATA,sha256=l0IX5F3K-yrNV7EGsM6A2Yv2QKp4qHnraqcEi6SAiao,44527
59
+ makea_cli-0.2.29.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
60
+ makea_cli-0.2.29.dist-info/entry_points.txt,sha256=4x-lbVa1jBbNlkNq7L-rYyfRh2spZmc3BLIzP-Dl-Qk,50
61
+ makea_cli-0.2.29.dist-info/RECORD,,