A shop uploads a product photo from the phone — 3 to 5 MB, 12 megapixels, sometimes rotated by its EXIF orientation — and the system has to serve it small: about 600 px for the product page, about 200 px for the list, as JPEG, without the photo's location. std/image is pure Nyx, native and wasm32-wasi, so neither libjpeg nor ImageMagick is needed. The sample photo is built inside the recipe with image_encode_jpeg; in real life it is the bytes of the multipart that arrived.
// Nyx by Example: miniaturas de la foto de un producto con std/image. // // El caso real: un comercio sube desde el teléfono la foto de un producto (3-5 MB, 12 // megapíxeles, a veces girada con la orientación EXIF) y el sistema la tiene que servir // chica: ~600 px para la ficha, ~200 px para la lista, en JPEG, sin la ubicación de la foto. // std/image es Nyx puro (nativo y wasm32-wasi): no hace falta libjpeg ni ImageMagick. // // Lo que muestra: // - image_thumbnail: decodificar + achicar + codificar en UN paso. Decodifica con la mayor // reducción posible (1/2, 1/4 o 1/8), así que una foto de 12 MP no se decodifica entera; // - image_thumbnails: varios tamaños de UNA sola decodificación (la de la ficha, la de la // lista y una de 80 px); // - la calidad (1-100; image_default_quality() es 80) y su efecto en los bytes; // - un PNG con transparencia (el logo) sale compuesto sobre blanco, que es lo que un JPEG // puede guardar; para otro fondo se llama image_flatten antes de codificar; // - los errores son Result con kind: "invalid" (parámetro o tope), "parse" (archivo dañado), // "unsupported" (HEIC, WebP, JPEG progresivo...); // - la foto de ejemplo se fabrica acá mismo con image_encode_jpeg para que la receta no // dependa de un archivo: en la vida real son los bytes del `multipart` que llegó. import "std/image" import "std/error" // Una "foto" sintética: un degradado de color con un círculo, de ancho x alto píxeles. fn foto_de_prueba(ancho: int, alto: int) -> Image { var filas: Array = [] var y: int = 0 while y < alto { var px: Array = [] var x: int = 0 while x < ancho { var r: int = (x * 255) / ancho var g: int = (y * 255) / alto var b: int = 128 let dx: int = x - ancho / 2 let dy: int = y - alto / 2 if dx * dx + dy * dy < (alto / 3) * (alto / 3) { r = 240 g = 200 b = 40 } px.push(r) px.push(g) px.push(b) x = x + 1 } filas.push(string_from_bytes(px, 0, ancho * 3)) y = y + 1 } return Image { width: ancho, height: alto, channels: 3, rows: filas, icc: "", reduction: 1 } } fn medidas(bytes: String) -> String { match image_info(bytes) { Result.Ok(i) => { return i.format + " " + int_to_string(i.width) + "x" + int_to_string(i.height) } Result.Err(e) => { return e.kind + ": " + e.msg } } } fn main() -> int { // La foto "del teléfono": 1600 x 1200 a calidad alta. var original: String = "" match image_encode_jpeg(foto_de_prueba(1600, 1200), 92) { Result.Ok(b) => { original = b } Result.Err(e) => { println("no se pudo armar la foto: " + e.msg) } } println("original: " + medidas(original) + ", " + int_to_string(original.length()) + " bytes") // 1. Una miniatura: cabe en 600 x 600 conservando la proporción (nunca agranda). match image_thumbnail(original, 600, 600, image_default_quality()) { Result.Ok(t) => { println("ficha: " + medidas(t) + ", " + int_to_string(t.length()) + " bytes") } Result.Err(e) => { println("error: " + e.kind + ": " + e.msg) } } // 2. Tres tamaños de una sola decodificación: se piden de mayor a menor. let tamanos: Array = [[600, 600], [200, 200], [80, 80]] match image_thumbnails(original, tamanos, 80) { Result.Ok(salidas) => { var i: int = 0 for s: String in salidas { println("tamaño " + int_to_string(i) + ": " + medidas(s) + ", " + int_to_string(s.length()) + " bytes") i = i + 1 } } Result.Err(e) => { println("error: " + e.kind + ": " + e.msg) } } // 3. La calidad: menos calidad, menos bytes. Para una lista con datos móviles caros, 60-70. for q: int in [95, 80, 60, 40] { match image_thumbnail(original, 200, 200, q) { Result.Ok(t) => { println("calidad " + int_to_string(q) + ": " + int_to_string(t.length()) + " bytes") } Result.Err(e) => { println("error: " + e.msg) } } } // 4. Un logo PNG con transparencia: el JPEG no guarda alfa, sale sobre blanco. let logo: String = include_bytes("examples/by-example/assets/logo-tornillo.png") match image_thumbnail(logo, 64, 64, 90) { Result.Ok(t) => { println("logo: " + medidas(t) + " (sobre blanco)") } Result.Err(e) => { println("error: " + e.kind + ": " + e.msg) } } // 5. Lo que no es una foto se rechaza con un motivo, sin abortar. match image_thumbnail("esto no es una imagen", 200, 200, 80) { Result.Ok(t) => { println("???") } Result.Err(e) => { println("rechazada (" + e.kind + "): " + e.msg) } } match image_thumbnail(original, 200, 200, 101) { Result.Ok(t) => { println("???") } Result.Err(e) => { println("rechazada (" + e.kind + "): " + e.msg) } } return 0 }
original: jpeg 1600x1200, 140360 bytes ficha: jpeg 600x450, 13167 bytes tamaño 0: jpeg 600x450, 13167 bytes tamaño 1: jpeg 200x150, 3175 bytes tamaño 2: jpeg 80x60, 1366 bytes calidad 95: 8025 bytes calidad 80: 3168 bytes calidad 60: 2504 bytes calidad 40: 2174 bytes logo: jpeg 64x64 (sobre blanco) rechazada (invalid): image_thumbnail: no parece una imagen JPEG ni PNG (firma desconocida) rechazada (invalid): image_thumbnail: la calidad va de 1 a 100 (llegó 101)
How it works
image_thumbnail decodes, shrinks and encodes in ONE step, decoding at the largest possible reduction (1/2, 1/4 or 1/8) so a 12 MP photo is not decoded in full. image_thumbnails makes several sizes from a SINGLE decode — the product page, the list and an 80 px one. Quality runs 1 to 100 (image_default_quality() is 80) and its effect on the bytes is visible in the four calidad
lines.
A PNG with transparency, the logo, comes out composited over white, which is what a JPEG can store; for another background call image_flatten before encoding. Errors are a Result with a kind: "invalid" (a parameter or a limit), "parse" (a damaged file) and "unsupported" (HEIC, WebP, progressive JPEG...). Both rejections at the end of the output are invalid.