"""Image Service: part image upload, processing, and storage. Stores images at: /home/Autopartes/data/images/parts//_full.webp /home/Autopartes/data/images/parts//_thumb.webp Serves statically via Flask at /pos/static/images/parts// """ import os import uuid import requests from io import BytesIO from PIL import Image # Base directories DATA_DIR = '/home/Autopartes/data/images/parts' STATIC_DIR = '/home/Autopartes/pos/static/images/parts' # Image processing settings MAX_WIDTH = 1200 THUMB_SIZE = (300, 300) FORMAT = 'WEBP' QUALITY = 85 THUMB_QUALITY = 80 def _ensure_dir(path): os.makedirs(path, exist_ok=True) def _build_paths(tenant_id, item_id): """Return (data_dir, static_dir, basename) for an item.""" ddir = os.path.join(DATA_DIR, str(tenant_id)) sdir = os.path.join(STATIC_DIR, str(tenant_id)) basename = str(item_id) return ddir, sdir, basename def _process_image(img): """Resize and convert image to WebP. Returns (full_bytes, thumb_bytes).""" # Convert to RGB if necessary (e.g. RGBA -> RGB for WEBP compatibility) if img.mode in ('RGBA', 'P'): img = img.convert('RGB') # Resize original if too wide w, h = img.size if w > MAX_WIDTH: ratio = MAX_WIDTH / w new_h = int(h * ratio) img = img.resize((MAX_WIDTH, new_h), Image.LANCZOS) # Full image full_buf = BytesIO() img.save(full_buf, format=FORMAT, quality=QUALITY, optimize=True) full_buf.seek(0) # Thumbnail (crop to square from center) thumb = img.copy() tw, th = thumb.size min_dim = min(tw, th) left = (tw - min_dim) // 2 top = (th - min_dim) // 2 thumb = thumb.crop((left, top, left + min_dim, top + min_dim)) thumb = thumb.resize(THUMB_SIZE, Image.LANCZOS) thumb_buf = BytesIO() thumb.save(thumb_buf, format=FORMAT, quality=THUMB_QUALITY, optimize=True) thumb_buf.seek(0) return full_buf, thumb_buf def save_image(tenant_id, item_id, file_obj=None, image_url=None, filename_hint=None): """Save an image for an inventory item. Args: tenant_id: tenant ID for path isolation item_id: inventory item ID file_obj: file-like object (from Flask request.files) image_url: URL to download image from filename_hint: original filename for extension detection Returns: dict with 'image_url', 'thumb_url', 'size_full', 'size_thumb' """ if not file_obj and not image_url: raise ValueError("Either file_obj or image_url is required") # Load image if file_obj: img = Image.open(file_obj) else: resp = requests.get(image_url, timeout=30) resp.raise_for_status() img = Image.open(BytesIO(resp.content)) # Process full_buf, thumb_buf = _process_image(img) # Save to filesystem ddir, sdir, basename = _build_paths(tenant_id, item_id) _ensure_dir(ddir) _ensure_dir(sdir) full_path = os.path.join(ddir, f"{basename}_full.webp") thumb_path = os.path.join(ddir, f"{basename}_thumb.webp") # Also symlink/copy to static dir for Flask serving static_full = os.path.join(sdir, f"{basename}_full.webp") static_thumb = os.path.join(sdir, f"{basename}_thumb.webp") with open(full_path, 'wb') as f: f.write(full_buf.read()) with open(thumb_path, 'wb') as f: f.write(thumb_buf.read()) # Copy to static dir import shutil shutil.copy2(full_path, static_full) shutil.copy2(thumb_path, static_thumb) # Build URLs image_url = f"/pos/static/images/parts/{tenant_id}/{basename}_full.webp" thumb_url = f"/pos/static/images/parts/{tenant_id}/{basename}_thumb.webp" return { 'image_url': image_url, 'thumb_url': thumb_url, 'size_full': os.path.getsize(full_path), 'size_thumb': os.path.getsize(thumb_path), } def delete_image(tenant_id, item_id): """Delete all images for an inventory item.""" ddir, sdir, basename = _build_paths(tenant_id, item_id) deleted = [] for directory in [ddir, sdir]: for suffix in ['_full.webp', '_thumb.webp']: path = os.path.join(directory, f"{basename}{suffix}") if os.path.exists(path): os.remove(path) deleted.append(path) return {'deleted': deleted} def get_image_info(tenant_id, item_id): """Get image info for an inventory item.""" ddir, sdir, basename = _build_paths(tenant_id, item_id) full_path = os.path.join(ddir, f"{basename}_full.webp") thumb_path = os.path.join(ddir, f"{basename}_thumb.webp") info = {'has_image': False, 'image_url': None, 'thumb_url': None} if os.path.exists(full_path): info['has_image'] = True info['image_url'] = f"/pos/static/images/parts/{tenant_id}/{basename}_full.webp" info['thumb_url'] = f"/pos/static/images/parts/{tenant_id}/{basename}_thumb.webp" info['size_full'] = os.path.getsize(full_path) info['size_thumb'] = os.path.getsize(thumb_path) info['updated_at'] = os.path.getmtime(full_path) return info