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