Convert heavy PNGs to JPEG where appropriate

While viewing my nonsense page with the network tab of the developer tools open I noticed the page was transferring a total of 15.8MB to load. The page loads hundreds of posts so this isn’t completely absurd, but it felt excessive. Turns out most of the weight was coming from a handful of PNG posters and screenshots.

In the general case, JPEGs are smaller than PNGs, but I didn’t want to hardcode conversion of all PNGs because there are times when lossless or transparent images are wanted.

I toyed around with a couple of implementations. My first prototype added a convert_to key to the asset manifests, but in the end I decided to simply convert on the basis of the output_path suffix being different than the input_path suffix in a manifest entry, allowing every stage of the build to seamlessly pick up the correct path to reference.

Switching those few heavy-weight PNGs to JPEG output cut the page weight from 15.8MB down to 5.18MB.

The most egregious offender was a lossless poster for Blade Runner (1982) coming in at 2.8MB initially, down to 240kB after conversion to JPEG. Shown below, the only change required in a manifest entry is to change the suffix/file-extension.

[27991f33-f40f-48ab-9690-0003db7b2038]
type = "poster"
title = "The poster for the film Blade Runner (1982)"
filepath = "/home/silas/library/images/posters/films/1982_Blade-Runner.png"
slug = "library/images/posters/films/1982_Blade-Runner.jpg"

The only change I needed to make to the build script was to check the colour-space when converting to JPEG and switch it to RGB in the case that the source colour-space was either ‘RGBA’ or ‘P’:

def process_image_parallel(input_data: Tuple[Path, Path, int]) -> None:
    input_image, output_path, output_width = input_data
    lock_path = output_path.with_suffix(".lock")
    lock = FileLock(str(lock_path))

    try:
        with lock:
            if output_path.exists():
                return

            os.makedirs(output_path.parent, exist_ok=True)

            with Image.open(input_image) as im:
                original_format = im.format
                im = ImageOps.exif_transpose(im)
                output_height = int(im.size[1] * (output_width / im.size[0]))

                with im.resize(
                    (output_width, output_height), Image.Resampling.LANCZOS
                ) as output_image:
                    if (
                        original_format != "JPEG"
                        and str(output_path).endswith("jpg")
                        and output_image.mode in ("RGBA", "P")
                    ):
                        output_image = output_image.convert("RGB")

                    output_image.save(output_path, quality=85, optimize=True)

            logger.debug(f"Processed image: {input_image} -> {output_path}")

    except OSError as e:
        logger.error(f"OS error processing {input_image}: {e}")
    except Exception as e:
        logger.error(f"Error processing {input_image}: {e}")
    finally:
        if lock_path.exists():
            try:
                lock_path.unlink()
            except OSError:
                pass