Skip to content

Instantly share code, notes, and snippets.

@0x9900
Created June 21, 2026 18:08
Show Gist options
  • Select an option

  • Save 0x9900/78f098a6e4eac0773a28067bd93758ea to your computer and use it in GitHub Desktop.

Select an option

Save 0x9900/78f098a6e4eac0773a28067bd93758ea to your computer and use it in GitHub Desktop.
Pelican extention for image
#! /usr/bin/env python
# vim:fenc=utf-8
#
# Copyright © 2024-2026 fred <github-fred@hidzz.com>
#
# Distributed under terms of the BSD 3-Clause license.
"""
Usage example:
![Small trap using thin coax](images/NanoVNA/IMG_1421.JPG)
size=240, 520, 760
class=aligncenter, imgshadow
link=https://0x9900.com/
Regular paragraph text here.
![Another example](images/example.jpg)
class=alignright
"""
import re
import uuid
from pathlib import Path
from lxml import etree
from lxml import html as html_parser
from markdown.extensions import Extension
from markdown.postprocessors import Postprocessor
from markdown.preprocessors import Preprocessor
from PIL import Image
from PIL.PngImagePlugin import PngInfo
def process_images(source, target, url, sizes):
if url.is_absolute():
url = url.relative_to('/')
source_img = source / url
target_path = target / url.parent
target_path.mkdir(parents=True, exist_ok=True)
try:
img = Image.open(source_img)
except Image.UnidentifiedImageError as err:
print(f'Image format not supported: {err}')
raise
is_animated = hasattr(img, 'is_animated') and img.is_animated
h_size, v_size = img.size
metadata = PngInfo()
metadata.add_text("Author", "W6BSD @ 0x9900.com")
target_images = []
for width in sizes:
target_img = (target_path / f'{url.stem}-{width}').with_suffix(url.suffix)
ratio = width / float(h_size)
height = int(v_size * ratio)
target_images.append((width, '/' + str(target_img.relative_to(target))))
if target_img.exists():
continue
if not is_animated:
res_img = img.resize((width, height), resample=Image.Resampling.LANCZOS)
res_img.save(target_img, dpi=(72, 72), pnginfo=metadata)
else:
frames = []
durations = []
try:
while True: # Loop through all frames
duration = img.info.get('duration', 100) # 100ms if not specified
durations.append(duration)
frames.append(img.resize((width, height), Image.LANCZOS))
img.seek(img.tell() + 1)
except EOFError:
pass # We've reached the last frame
# Save the resized animated WebP
frames[0].save(target_img, save_all=True, append_images=frames[1:],
duration=durations, loop=0)
target_images.sort()
return target_images
class ImageProcessor(Preprocessor):
"""
Preprocessor to handle multi-line commands in Markdown.
For example:
[Caption](path/to/image.jpg)
size=240, 520, 760
class=aligncenter, imgshadow
link=https://example.com/
"""
pattern_start = re.compile(r'^!\[(.*?)\]\((.*?)\)$')
pattern_attribute = re.compile(r'^\s+([a-zA-Z0-9_-]+)=(.+)$')
def __init__(self, md):
super().__init__(md)
self.placeholders = {}
self.figcaption = None
def create_placeholder(self, html):
"""Create a unique placeholder and store the HTML for later replacement"""
placeholder_id = str(uuid.uuid1())
self.placeholders[placeholder_id] = html
return placeholder_id
def run(self, lines):
"""
Process lines of text, looking for image tag and generating HTML with
placeholders.
"""
new_lines = []
i = 0
while i < len(lines):
line = lines[i]
match = self.pattern_start.match(line)
if not match:
new_lines.append(line)
i += 1
continue
caption, url = match.group(1), match.group(2)
attributes, j = self._parse_attributes(lines, i + 1)
html = self.generate_html(caption, url, attributes)
placeholder = self.create_placeholder(html)
new_lines.append(placeholder)
i = j # Skip processed lines
return new_lines
def _parse_attributes(self, lines, start_index):
"""
Parses valid attribute lines starting at start_index.
Returns a dictionary of attributes and the index after the last attribute line.
"""
attributes = {}
i = start_index
while i < len(lines):
match = self.pattern_attribute.match(lines[i])
if not match:
break
key, raw_value = match.group(1).lower(), match.group(2).strip()
if key not in ('size', 'class', 'link'):
raise KeyError(f'Attribute "{key}" unknown')
if key == 'link':
attributes[key] = raw_value
else:
values = [v.strip() for v in raw_value.split(',')]
if any(' ' in v for v in values):
raise ValueError(f'Bad character in: {raw_value}')
attributes[key] = values
i += 1
return attributes, i
def generate_html(self, caption, url, attributes):
"""
Generate HTML for the multi-line command based on attributes.
This is a simple implementation - extend as needed.
"""
self.figcaption = caption
root = figure = etree.Element('figure')
if 'class' in attributes and attributes['class']:
figure.set('class', ' '.join(attributes['class']))
if 'link' in attributes and attributes['link']:
link = etree.SubElement(figure, 'a')
link.set('href', attributes['link'])
link.set('title', caption)
root = link
picture = etree.SubElement(root, 'picture')
if 'size' in attributes and attributes['size']:
sizes = [int(s) for s in attributes['size']]
if url.startswith('http'):
img_sets = [(s, url) for s in sizes]
else:
img_sets = process_images(self.source, self.target, Path(url), sizes)
for size, img_set in img_sets:
source = etree.SubElement(picture, 'source')
source.set('media', f"(max-width: {size}px)")
source.set('srcset', str(img_set))
img = etree.SubElement(picture, 'img')
img.set('src', img_sets[-1][1])
img.set('width', str(img_sets[-1][0]))
else:
img = etree.SubElement(picture, 'img')
img.set('src', url)
img.set('alt', caption)
figcaption = etree.SubElement(figure, 'figcaption')
fragment = html_parser.fragments_fromstring(caption)
for node in fragment:
if isinstance(node, str):
if figcaption.text:
figcaption.text += node
else:
figcaption.text = node
else:
figcaption.append(node)
html = etree.tostring(figure, encoding='unicode', method='html')
html = html.replace('</source>', '') # this is some bullshit.
return html
class ImagePlaceholderPostprocessor(Postprocessor):
"""
Postprocessor to replace placeholders with actual HTML content.
"""
def __init__(self, md, placeholders):
super().__init__(self)
self.placeholders = placeholders
def run(self, text):
# Replace all placeholders with their corresponding HTML
for placeholder, html in self.placeholders.items():
# Using regex to handle both the placeholder inside paragraph
# tags and standalone
text = re.sub(r'<p>\s*' + placeholder + r'\s*</p>', html, text)
text = text.replace(placeholder, html)
return text
class ImageExtension(Extension):
def __init__(self, **kwargs):
self.config = {
'source': [Path('content'), 'Source path for images'],
'target': [Path('output'), 'Destination path for images'],
}
super().__init__(**kwargs)
def extendMarkdown(self, md):
# Register the preprocessor with high priority (1)
processor = ImageProcessor(md)
processor.source = Path(self.getConfig('source'))
processor.target = Path(self.getConfig('target'))
md.preprocessors.register(processor, 'multiline_image', 1)
# Add the postprocessor to handle placeholders after all processing is done
md.postprocessors.register(
ImagePlaceholderPostprocessor(md, processor.placeholders),
'image_placeholder_postprocessor',
100 # High priority to run at the end
)
def makeExtension(**kwargs): # pragma: no cover
return ImageExtension(**kwargs)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment