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 Princess Mononoke, my first Ghibli“) 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.

toml
[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’:

python
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