Resizing images and renaming them

We need to see your repo or a repo which reproduces the problem with code you can share.

1 Like

hereā€™s a code snippet that should be easily reproducable:

{{ with .Params.thumbnail }}
    {{ $thumbnail := $.Resources.GetMatch . }}
    {{ if $thumbnail }}
        {{ $title := $.Title | urlize }}
        {{ range $index, $size := $.Site.Params.thumbnail_sizes.sizes }}
            {{ $prefix := index $.Site.Params.thumbnail_sizes.prefixes $index }}
            {{ range $format := $.Site.Params.post_headers.formats }}
                {{ $resizedImage := $thumbnail.Resize $size }}
                {{ with $resizedImage | resources.Copy (print "optimized/" $title "-" $prefix "." $format) }}
		    {{ $resizedImage = . }}
		    {{/*
		    [ {{ $resizedImage.RelPermalink }} ]
                    [ {{ (print $prefix "_" $format) }} ]
                    {{ $.Scratch.Set (print $prefix "_" $format) $resizedImage }}
		    {{ $fn := (print $prefix "_" $format) }}
                    {{ $.Scratch.Set (print $prefix "_" $format) $resizedImage }}
		    */}}
                {{ end }}
            {{ end }}
        {{ end }}
    {{ end }}
{{ end }}

create any page bundle, in the front matter put thumbnail = ā€œany bundle image resourceā€
you can place this in the config.toml

[Params.post_headers]
  formats = ["jpg", "webp", "png"]
  [Params.thumbnail_sizes]
    sizes = ["1280x720", "640x360", "320x180", "160x90"]
    prefixes = ["TL", "TM", "TS", "TES"] # Thumbnail Large, Thumbnail Medium, Thumbnail Small, Thumbnail Extra Small
  [Params.header_sizes]
    sizes = ["1280x", "640x", "320x", "160x"]
    prefixes = ["HL", "HM", "HS", "HES"] # Header Large, Header Medium, Header Small, Header Extra Small

it will generate the images, in the script even copy the files to the correct place but fails to be able to access anything from the scrach file outside of the script.

just try generating images, rename and copy them. Thereā€™s so many bugs in this area of the code. If this code example is too difficult, thereā€™s no need to see the repo. This is the simplest example youā€™ll have.

Isnā€™t the purpose of hugo to pre-generate a site, this issue is such a major show problem because it seems thereā€™s no passage without building outside tools to preprocess the image and possibly run hugo afterwards.

It honestly cannot be this arduous to get a static site generate to images. A whole weekend worth of work trying different things all for them to fail because of random edge cases that are not documented.

What does that mean? What is the ā€œscratch fileā€, what do you need it for, and why would the template (thereā€™s no script) access it.

What does that mean? Does the code not run? Does it generate error messages? Does it generate wrong images?

There many people out there that have no problems generating images in the appropriate sizes and formats with the names they want (Iā€™m one of them). That might be an indication that the problem in this case is not Hugo.

Show your code. All of it, not only those randomly selected pieces that you think are somehow relevant. A git clone command is a lot easier than trying to stitch together something from the frustratred remarks youā€™re leaving here.

1 Like

the code sample to resize the image is a partial. create a partial with the code, call the partial.

thereā€™s two ways forward.

  1. duplicate this code all over the project
  2. try to create a partial with the code, the code works within the partial but you cannot access any of the variables set within the partial outside of the partial.

Try returning an object, using scratch space, passing context around.

They all fail

Have the partial return a string that you can use in your figure or img. Thatā€™s what I do (kind of)

it does not workā€¦

look at the code sample above, i print the RelPermalink in the partial, it works.

i set scratch name, check it right below the partial code in the template, nil

import re
import os
import sys
import yaml
import toml
import json
import hashlib
import argparse
from PIL import Image
from PIL import ImageOps

import pillow_avif

content_dir = None
output_dir  = None
folder_fingerprints = {}

# Configuration
config = {
    "thumbnail_sizes": ["1280x720", "640x360", "320x180", "160x90"],
    "header_sizes": ["1280x", "640x", "320x", "160x"],
    "formats": ["jpeg", "webp", "png", "avif"],
    "thumbnail_postfixes": ["TL", "TM", "TS", "TES"],
    "header_postfixes": ["HL", "HM", "HS", "HES"]
}

def parse_front_matter(file_content):
    # Check for YAML front matter
    if file_content.startswith('---'):
        return yaml.safe_load(file_content.split('---')[1])
    # Check for TOML front matter
    elif file_content.startswith('+++'):
        return toml.loads(file_content.split('+++')[1])
    # Check for JSON front matter
    elif file_content.startswith('{') and file_content.endswith('}'):
        return json.loads(file_content)
    else:
        return None

def urlize_test(title):
    return title.lower().replace(" ", "-").replace(":", "")

def urlize(s):
    # Convert to lowercase
    s = s.lower()
    # Keep only alphanumeric characters and spaces
    s = re.sub(r'[^a-z0-9 ]', '', s)
    # Replace spaces with hyphens
    s = s.replace(' ', '-')
    return s

def generate_filename_fingerprint(filename):
    return hashlib.sha256(filename.encode()).hexdigest()

def generate_fingerprint(file_path):
    with open(file_path, 'rb') as f:
        return hashlib.sha256(f.read()).hexdigest()

def has_file_changed(file_path, stored_fingerprint_path, original_fingerprint):
    if not os.path.exists(stored_fingerprint_path):
        print(f"file {file_path} has changed\n")
        return True

    with open(stored_fingerprint_path, 'r') as f:
        stored_fingerprint = f.read().strip()

    print(f"Stored Fingerprint: {stored_fingerprint}")
    print(f"Original Fingerprint: {original_fingerprint}")
    return stored_fingerprint != original_fingerprint


def process_image_test(image_path, image_type, title):
    global output_dir
    global content_dir
    sizes = config[f"{image_type}_sizes"]
    postfixes = config[f"{image_type}_postfixes"]

    for index, size in enumerate(sizes):
        postfix = postfixes[index]
        for format in config["formats"]:
            # Construct the new filename
            new_filename = f"{title}-{postfix}.{format}"
            # Print the details
            print(f"\n")
            print(f"{os.path.basename(image_path)}")
            print(f"directory: {os.path.dirname(image_path)}")
            print(f"Processing {image_path}")
            print(f"{os.path.abspath(image_path)})")
            print(f"New filename: {new_filename}")
            # For now, let's use the new filename as the fingerprint (you can replace this with a real fingerprinting method later)
            fingerprint = generate_filename_fingerprint(new_filename)
            print(f"Fingerprint: {fingerprint}")
            print(f"Output directory: {output_dir}")
            output_file = f"{output_dir}/{new_filename}"
            print(f"Output file: {output_file}")

def process_image(image_path, image_type, title):
    global output_dir
    global folder_fingerprints
    sizes = config[f"{image_type}_sizes"]
    postfixes = config[f"{image_type}_postfixes"]

    folder_path = os.path.dirname(image_path)
    if folder_path not in folder_fingerprints:
        # Generate a fingerprint for the original image
        folder_fingerprints[folder_path] = generate_fingerprint(image_path)
    
    original_fingerprint = folder_fingerprints[folder_path]

    for index, size in enumerate(sizes):
        postfix = postfixes[index]
        for format in config["formats"]:
            # Construct the new filename
            new_filename = f"{title}-{postfix}.{format}"
            output_file = f"{output_dir}/{new_filename}"
            fingerprint_file = f"{output_file}.hash"
            print(f"fingerprint_file {fingerprint_file}")

            # Check if the file has changed
            if has_file_changed(image_path, fingerprint_file, original_fingerprint):
                # Open the image
                with Image.open(image_path) as img:
                    # Resize the image


                    if image_type == "thumbnail":
                        # Smart fill for thumbnail sizes
                        width, height = map(int, size.split('x'))
                        img_resized = ImageOps.fit(img, (width, height), Image.LANCZOS)
                    elif image_type == "header":
                        # Resize based on max width for header sizes
                        width = int(size[:-1])  # Remove the trailing 'x' and convert to int
                        aspect_ratio = img.width / img.height
                        height = int(width / aspect_ratio)
                        img_resized = img.resize((width, height))


                    # img = img.resize((width, height))
                    # Save the image in the desired format
                    img_resized.save(output_file, format=format.upper())
                    # Store the fingerprint of the new image
                    with open(fingerprint_file, 'w') as f:
                        f.write(original_fingerprint)

                print(f"Processed and saved: {output_file}")
            else:
                print(f"File {output_file} has not changed. Skipping...")


def main():
    global output_dir
    global content_dir

    parser = argparse.ArgumentParser(description="Image Preprocessor for Hugo")
    parser.add_argument("--content-dir", required=True, help="Directory to crawl for content")
    parser.add_argument("--output-dir", required=True, help="Directory where processed images will be saved")
    args = parser.parse_args()

    content_dir = os.path.abspath(args.content_dir)  # Convert to absolute path
    output_dir = os.path.abspath(args.output_dir)  # Convert to absolute path

    print(f"Content Directory: {content_dir}")
    print(f"Output Directory: {output_dir}")


    for root, _, files in os.walk(content_dir):
        for file in files:
            if file.endswith(".md"):
                with open(os.path.join(root, file), 'r') as f:
                    content = f.read()
                    front_matter = parse_front_matter(content)
                    if front_matter:
                        raw_title = front_matter.get('title', '')
                        title = urlize(raw_title)
                        thumbnail = front_matter.get('thumbnail')
                        if thumbnail:
                            if isinstance(thumbnail, list):
                                for image in thumbnail:
                                    full_image_path = os.path.join(root, image)
                                    if not os.path.exists(full_image_path):
                                        print(f"Missing file: {full_image_path}")
                                        continue
                                    process_image(full_image_path, "thumbnail", title)
                                    process_image(full_image_path, "header", title)
                            else:
                                full_image_path = os.path.join(root, thumbnail)
                                if not os.path.exists(full_image_path):
                                    print(f"Missing file: {full_image_path}")
                                    continue
                                process_image(full_image_path, "thumbnail", title)
                                process_image(full_image_path, "header", title)


if __name__ == "__main__":
    main()

this code can preprocess images, i have some hardcoded values in here to make it a standalone project.

you might need to install the pillow_avif pluginf or python to get avif format to work.

thereā€™s rudamentary fingerprints to avoid reprocessing images if the main file hasnā€™t changed.

I agree with you regarding SEO. But having fixed file name (not random) also helps in combating one of the biggest problems on the internet: dead/broken links.

If someone shares a link to an image from my site on a forum, I guess it will turn into a broken link when I update my site or make changes to how I handle image resizing.