# Written by Jeremy Karst 2025 # This software is licensed under the MIT License with Additional Terms. # See LICENSE.md for more information. # Built-in Dependencies import base64 import time import datetime # External Dependencies from cryptography.hazmat.primitives import hashes import qrcode from qrcode.image.styledpil import StyledPilImage from qrcode.image.styles.moduledrawers.pil import CircleModuleDrawer from qrcode.image.styles.colormasks import RadialGradiantColorMask, SolidFillColorMask from PIL import Image, ImageDraw, ImageFont def hash_text(text: str) -> str: message = text.encode('utf-8') hash_class = hashes.Hash(hashes.SHA256()) hash_class.update(message) signature = hash_class.finalize() first_half = signature[:len(signature)//2] second_half = signature[len(signature)//2:] xored = bytes(a ^ b for a, b in zip(first_half, second_half)) b64bytes = base64.b64encode(xored).replace(b'+', b'-').replace(b'/', b'_').replace(b'=', b'') b64str = b64bytes.decode('utf-8') return b64str def generate_signature_image(sig_name: str, sig_time: int, sig_timezone: datetime.timezone, signature_url: str, sig_font_path: str, note_font_path: str, padding: int = 100, interspacing: int = 60, invert_colors: bool = False, gradient: bool = True): start_time = time.perf_counter() # Create a datetime object from the sig_time and sig_timezone sig_time = datetime.datetime.fromtimestamp(sig_time, sig_timezone) datetime_time = time.perf_counter() note_text = f"Digitally Signed by\n{sig_name}\n{sig_time.strftime('%Y-%m-%d %I:%M %p')}" # QR code generation timing # qr_start = time.perf_counter() qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=8, border=0, ) qr.add_data(signature_url) if invert_colors: if gradient: qr_img = qr.make_image(fit=True, image_factory=StyledPilImage, module_drawer=CircleModuleDrawer(), color_mask=RadialGradiantColorMask(back_color=(0, 0, 0, 128), center_color=(245, 245, 245, 255), edge_color=(250, 250, 250, 255))) else: qr_img = qr.make_image(fit=True, image_factory=StyledPilImage, module_drawer=CircleModuleDrawer(), color_mask=SolidFillColorMask(back_color=(0, 0, 0, 128), front_color=(250, 250, 250, 255))) else: if gradient: qr_img = qr.make_image(fit=True, image_factory=StyledPilImage, module_drawer=CircleModuleDrawer(), color_mask=RadialGradiantColorMask(back_color=(255, 255, 255, 128), center_color=(10, 10, 150, 255), edge_color=(5, 5, 20, 255))) else: qr_img = qr.make_image(fit=True, image_factory=StyledPilImage, module_drawer=CircleModuleDrawer()) qr_size = qr_img.pixel_size # qr_time = time.perf_counter() # Text measurement timing # text_start = time.perf_counter() test_image = Image.new("RGBA", (2000, 1000), (0, 0, 0, 0)) sig_font = ImageFont.truetype(sig_font_path, 350) sans_font = ImageFont.truetype(note_font_path, 80) draw = ImageDraw.Draw(test_image) sig_bbox = draw.textbbox((0, 0), sig_name, font=sig_font, anchor="ls") # (left, top, right, bottom) note_bbox = draw.multiline_textbbox((0, 0), note_text, font=sans_font, anchor="ls") # (left, top, right, bottom) # text_measure_time = time.perf_counter() sig_width = sig_bbox[2] - sig_bbox[0] sig_height = sig_bbox[3] - sig_bbox[1] note_width = note_bbox[2] - note_bbox[0] note_height = note_bbox[3] - note_bbox[1] total_image_width = padding + sig_width + interspacing + note_width + interspacing + qr_size + padding total_image_height = padding + max(sig_height, note_height) + padding v_anchor = total_image_height - sig_bbox[3] - padding if invert_colors: sig_color = "white" note_color = "white" background_color = (255, 255, 255, 0) else: sig_color = "black" note_color = "black" background_color = (0, 0, 0, 0) image = Image.new("RGBA", (total_image_width, total_image_height), background_color) draw = ImageDraw.Draw(image) draw.text((padding - sig_bbox[0], v_anchor), sig_name, font=sig_font, fill=sig_color, anchor="ls") draw.text((padding + sig_width + interspacing - note_bbox[0], v_anchor - note_bbox[3]), note_text, font=sans_font, fill=note_color, anchor="ls") image.paste(qr_img._img, (padding + sig_width + interspacing + note_width + interspacing, v_anchor - qr_size)) # final_time = time.perf_counter() # # Print timing information # print(f"Signature Image Generation Timing:") # print(f" DateTime processing: {(datetime_time - start_time)*1000:.2f}ms") # print(f" QR code generation: {(qr_time - qr_start)*1000:.2f}ms") # print(f" Text measurement: {(text_measure_time - text_start)*1000:.2f}ms") # print(f" Final image composition: {(final_time - text_measure_time)*1000:.2f}ms") # print(f" Total time: {(final_time - start_time)*1000:.2f}ms") return image if __name__ == "__main__": sig_name = "Yöùr Ñamé Herê" sig_font_path = "./web_content/fonts/HerrVonMuellerhoff.ttf" note_font_path = "./web_content/fonts/Steelfish.ttf" timestamp = int(time.time()) timezone = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo sig_text = f"{sig_name}{timestamp}" hash_id = hash_text(sig_text) signature_data = f"osg.jkdev.org/v/{hash_id}" image = generate_signature_image(sig_name, timestamp, timezone, signature_data, sig_font_path, note_font_path, invert_colors=True, gradient=True) image.save(f"Signature {hash_id} {timestamp}.png", optimize=True, compress_level=9)