DEV Community

casanovalabs
casanovalabs

Posted on • Originally published at casanovalabs.com

What a Portal's Resize Pipeline Actually Does to a Real Estate Listing Photo

A listing photo leaves the camera looking right. By the time a portal has resized it for a thumbnail grid and recompressed it for delivery, it can look flatter, softer, or faintly wrong in a way nobody can quite name. None of that requires a bug on anyone's part — every step is documented, default behaviour in the image libraries doing the work.

We build CasaNova Labs, an AI studio for real estate photo and video editing, and the same three operations — resize, recompress, convert format — run on every image the product touches. Here is what the defaults in sharp (the Node.js binding for libvips, the library most JS image pipelines end up calling) actually do at each step, read from sharp's own API docs rather than assumed.

The kernel decides what a downscale looks like

resize() takes a kernel option — the algorithm used to compute new pixel values when shrinking an image, with the inferred interpolator reused for upsampling. The default is lanczos3, not the linear or nearest-neighbour interpolation a naive implementation might reach for:

sharp(input)
  .resize(200, 300, { kernel: sharp.kernel.nearest })
  .toFile('output.png');
Enter fullscreen mode Exit fullscreen mode

That line is in sharp's own docs as an example of what nearest-neighbour resizing looks like — a hard "before" case for comparison, not a recommendation. The docs also flag a second, easy-to-miss knob: fastShrinkOnLoad, on by default, which takes advantage of JPEG and WebP's built-in shrink-on-load feature for speed. The tradeoff is explicit in the reference: it "can lead to a slight moiré pattern or round-down of an auto-scaled dimension." Fine for a thumbnail grid, worth turning off for a hero image.

Chroma subsampling isn't the same default across formats

Chroma subsampling throws away colour resolution the eye is less sensitive to while keeping full luminance detail. Sharp's jpeg() output defaults to '4:2:0' — three-quarters of the colour data discarded — unless you set it explicitly:

await sharp(input)
  .jpeg({ quality: 100, chromaSubsampling: '4:4:4' })
  .toBuffer();
Enter fullscreen mode Exit fullscreen mode

The part worth checking rather than assuming: avif() and heif() default the other way, to '4:4:4' (full chroma), and only drop to '4:2:0' if you ask for it. webp() sits in between, with a smartSubsample flag for higher-quality chroma subsampling and an effort scale from 0 (fastest) to 6 (slowest), default 4. Three formats, three different defaults for the same concept — a pipeline that hard-codes one assumption across all three will be right for one of them and wrong for the other two.

The colour profile gets stripped unless you say otherwise

This is the default sharp documents most plainly, and the one most likely to surprise: "the default behaviour, when keepMetadata is not used, is to convert to the device-independent sRGB colour space and strip all metadata, including the removal of any ICC profile." A photo shot in Adobe RGB or with a camera-embedded profile gets flattened to sRGB on the way out, silently, every time.

withMetadata() is the documented opt-out — it keeps EXIF, XMP and IPTC, and "will also convert to and add a web-friendly sRGB ICC profile if appropriate." For a specific target profile rather than the automatic one, withIccProfile() takes either a filesystem path or one of the built-in names: srgb, p3, cmyk.

await sharp(input).withIccProfile('p3').toBuffer();
Enter fullscreen mode Exit fullscreen mode

Cropping for a grid uses the same defaults, silently

A portal's thumbnail grid almost always crops rather than letterboxes, and that crop has its own default worth knowing. Sharp's resize() takes a fit option — cover (default), contain, fill, inside or outside — modelled on the CSS object-fit property. With fit: 'cover', the crop is centred by default (position: 'centre'), but it doesn't have to guess blindly: sharp also documents gravity values (north, northeast, … centre) and two content-aware strategies, entropy (crops toward the region with the highest Shannon entropy) and attention (crops toward the region of highest luminance frequency). A pipeline that leaves fit and position at their defaults gets a centred crop every time — fine for a portrait already centred on the subject, wrong for a wide shot where the interesting part of the room sits off to one side.

sharp(input)
  .resize(800, 600, { fit: 'cover', position: sharp.strategy.attention })
  .toFile('thumbnail.jpg');
Enter fullscreen mode Exit fullscreen mode

None of this throws an error

That is the actual problem with all three defaults: they are not bugs, so nothing fails and nothing logs. The image resizes, the format converts, the file ships — at three-quarters colour resolution, flattened to sRGB, through a kernel nobody chose on purpose. A pipeline that cares about the output has to set kernel, chromaSubsampling and the metadata/ICC handling deliberately, per format, rather than accept whatever the library shipped as a sane general-purpose default.

That is the layer we tune for every image CasaNova Labs outputs, so a listing photo that comes out of the pipeline keeps the colour fidelity it went in with, rather than whatever three unrelated defaults happen to agree on.

Top comments (0)