open-signature-generator/sha_sign.py

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)