import 'dart:convert'; import 'dart:typed_data'; import 'package:image/image.dart' as img; /// Builds a tiny `data:image/jpeg;base64,…` thumbnail from a full photo, small /// enough to ride *inside* a Nostr offer event — no media server, fully /// decentralized. The full-resolution photo stays in the encrypted local /// inventory; only this shrunken preview is published so peers can see the seed. /// /// Relays cap event size, so we downscale (longest edge) and re-encode until the /// base64 fits under [maxBytes]. Returns null when the bytes aren't a decodable /// image or no size small enough is reachable — the offer then publishes without /// a photo (graceful, never a hard failure). String? offerThumbnailDataUri( Uint8List bytes, { int maxBytes = 40000, List edges = const [320, 256, 192, 128], int quality = 68, }) { try { final decoded = img.decodeImage(bytes); if (decoded == null) return null; for (final edge in edges) { final resized = _fitWithin(decoded, edge); final jpeg = img.encodeJpg(resized, quality: quality); final b64 = base64.encode(jpeg); if (b64.length <= maxBytes) return 'data:image/jpeg;base64,$b64'; } return null; // even the smallest edge didn't fit — skip the image } catch (_) { return null; // undecodable/corrupt bytes — publish without a photo } } /// Builds a small JPEG thumbnail (longest edge [edge]px) for the inventory-list /// avatar, so the list never has to decode the full-resolution photo. Returns /// raw JPEG bytes, or null on undecodable input (the caller falls back to the /// full photo). Kept as local bytes — regenerable, so it is excluded from sync /// and backups. Uint8List? inventoryThumbnailBytes( Uint8List bytes, { int edge = 96, int quality = 72, }) { try { final decoded = img.decodeImage(bytes); if (decoded == null) return null; final resized = _fitWithin(decoded, edge); return Uint8List.fromList(img.encodeJpg(resized, quality: quality)); } catch (_) { return null; } } /// Extracts the raw bytes from a `data:...;base64,…` URI, or null when [uri] is /// not a base64 data URI. Used by the UI to render an inline thumbnail with /// `Image.memory` instead of a network fetch. Uint8List? decodeDataUri(String uri) { if (!uri.startsWith('data:')) return null; try { return Uri.parse(uri).data?.contentAsBytes(); } on FormatException { return null; } } /// Resizes [image] so its longest edge is at most [edge], preserving aspect /// ratio. Never upscales a photo that's already smaller. img.Image _fitWithin(img.Image image, int edge) { if (image.width <= edge && image.height <= edge) return image; return image.width >= image.height ? img.copyResize(image, width: edge) : img.copyResize(image, height: edge); }