129 lines
No EOL
5.6 KiB
Python
129 lines
No EOL
5.6 KiB
Python
# 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) |