This commit is contained in:
Hendrik Schutter 2025-06-08 20:21:42 +02:00
parent 63ba1e8d1d
commit 7b4dfebbdc
2 changed files with 214 additions and 150 deletions

View File

@ -1,215 +1,279 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" Author: Hendrik Schutter, localhorst@mosad.xyz """
Date of creation: 2022/11/16 Author: Hendrik Schutter, localhorst@mosad.xyz
Date of last modification: 2022/11/23 Date of creation: 2022/11/16
Date of last modification: 2025/06/08
""" """
import re import re
import dataclasses import dataclasses
import glob import glob
import datetime import datetime
import json import json
import logging
import qrcode import qrcode
from PIL import Image from PIL import Image, ImageFont, ImageDraw
from PIL import ImageFont
from PIL import ImageDraw # Constants
FONT_PATH = "/usr/share/fonts"
DEFAULT_FONT_REGULAR = "DejaVuSans.ttf"
DEFAULT_FONT_BOLD = "DejaVuSans-Bold.ttf"
OUTPUT_WIDTH = 696 # px
OUTPUT_HEIGHT = 300 # px
TEXT_X_OFFSET = 300 # px
QR_CODE_SIZE = 289 # px
# Configure logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
)
@dataclasses.dataclass @dataclasses.dataclass
class DriveData: class DriveData:
drive_index: int drive_index: int
drive_state: str #none, deleted, shredded drive_state: str
modelfamiliy: str modelfamily: str
modelname: str modelname: str
capacity: int #in bytes capacity: int
serialnumber: str serialnumber: str
power_on_hours: int #in hours power_on_hours: int
power_cycle: int power_cycle: int
smart_error_count: int smart_error_count: int
shred_timestamp: int #unix timestamp shred_timestamp: int
shred_duration: int #in seconds shred_duration: int
@dataclasses.dataclass @dataclasses.dataclass
class DriveDataPrintable: class DriveDataPrintable:
modelfamiliy: str #max lenght 25 modelfamily: str
modelname: str #max lenght 25 modelname: str
capacity: str #max lenght 25, in human-readable format with unit (GB/TB) capacity: str
serialnumber: str #max lenght 25 serialnumber: str
power_on_hours: str #max lenght 25, in hours and days and years power_on_hours: str
power_cycle: str #max lenght 25 power_cycle: str
smart_error_count: str #max lenght 25 smart_error_count: str
shred_timestamp: str #max lenght 25, human-readable shred_timestamp: str
shred_duration: str #max lenght 25, human-readable shred_duration: str
@dataclasses.dataclass @dataclasses.dataclass
class ReHddInfo: class ReHddInfo:
link: str link: str
version: str version: str
@dataclasses.dataclass @dataclasses.dataclass
class DriveDataJson: class DriveDataJson:
drive: DriveData drive: DriveData
rehdd: ReHddInfo rehdd: ReHddInfo
def get_font_path_regular():
path = "/usr/share/fonts"
files = glob.glob(path + "/**/DejaVuSans.ttf", recursive = True)
return files[0]
def get_font_path_bold(): def find_font_path(font_name):
path = "/usr/share/fonts" """Finds the full path of the specified font."""
files = glob.glob(path + "/**/DejaVuSans-Bold.ttf", recursive = True) try:
return files[0] files = glob.glob(f"{FONT_PATH}/**/{font_name}", recursive=True)
return files[0] if files else None
except Exception as e:
logging.error(f"Error locating font {font_name}: {e}")
return None
def human_readable_capacity_1024(size, decimal_places=0):
for unit in ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB']:
if size < 1024.0 or unit == 'PiB':
break
size /= 1024.0
return f"{size:.{decimal_places}f} {unit}"
def human_readable_capacity_1000(size, decimal_places=0): def human_readable_capacity(size, decimal_places=0, base=1000):
for unit in ['B', 'KB', 'MB', 'GB', 'TB', 'PB']: """Converts bytes to a human-readable string."""
if size < 1000.0 or unit == 'PB': units = (
break ["B", "KB", "MB", "GB", "TB", "PB"]
size /= 1000.0 if base == 1000
return f"{size:.{decimal_places}f} {unit}" else ["B", "KiB", "MiB", "GiB", "TiB", "PiB"]
)
for unit in units:
if size < base or unit == units[-1]:
return f"{size:.{decimal_places}f} {unit}"
size /= base
def human_readable_power_on_hours(hours):
"""Converts hours to human-readable string in hours, days, and years."""
return (
str(hours)
+ "h or "
+ str(int(hours / 24))
+ "d or "
+ str("{:.2f}".format(float(hours / 24 / 365)))
+ "y"
)
def cut_string(max_length, data, direction="end"):
"""Trims a string to the maximum length, adding ellipses if necessary."""
if len(data) > max_length:
return (
f"{data[:max_length-4]} ..."
if direction == "end"
else f"... {data[-(max_length-4):]}"
)
return data
def human_readable_power_on_hours(hours, decimal_places=2):
return str(hours) + "h or " + str(int(hours/24)) + "d or " + str("{:.2f}".format(float(hours/24/365))) + "y"
def cut_string(max_lenght, data, direction):
if (len(data) > max_lenght):
if (direction == "end"):
return data[0:(max_lenght-4)] + " ..."
elif (direction == "start"):
return "... " + data[(len(data)-max_lenght+4):]
else:
return cut_string(max_lenght, data, "end")
else:
return data
def format_to_printable(drive): def format_to_printable(drive):
return DriveDataPrintable( return DriveDataPrintable(
cut_string(20, re.sub(r"[^a-zA-Z0-9. ]", "", drive.modelfamiliy), "end"),\ cut_string(20, re.sub(r"[^a-zA-Z0-9. ]", "", drive.modelfamily), "end"),
cut_string(20, re.sub(r"[^a-zA-Z0-9. ]", "", drive.modelname), "end"),\ cut_string(20, re.sub(r"[^a-zA-Z0-9. ]", "", drive.modelname), "end"),
cut_string(20, human_readable_capacity_1000(drive.capacity), "end"),\ cut_string(20, human_readable_capacity(drive.capacity), "end"),
cut_string(16, re.sub(r"[^a-zA-Z0-9.-_]", "", drive.serialnumber), "start"),\ cut_string(16, re.sub(r"[^a-zA-Z0-9.-_]", "", drive.serialnumber), "start"),
cut_string(30, human_readable_power_on_hours(drive.power_on_hours), "end"),\ cut_string(30, human_readable_power_on_hours(drive.power_on_hours), "end"),
cut_string(10, str(drive.power_cycle), "end"),\ cut_string(10, str(drive.power_cycle), "end"),
cut_string(10, str(drive.smart_error_count), "end"),\ cut_string(10, str(drive.smart_error_count), "end"),
cut_string(30, datetime.datetime.utcfromtimestamp(drive.shred_timestamp).strftime('%Y-%m-%d %H:%M:%S'), "end"),\ cut_string(
cut_string(30, str(datetime.timedelta(seconds = drive.shred_duration)), "end")) 30,
datetime.datetime.fromtimestamp(
drive.shred_timestamp, datetime.UTC
).strftime("%Y-%m-%d %H:%M:%S"),
"end",
),
cut_string(30, str(datetime.timedelta(seconds=drive.shred_duration)), "end"),
)
def draw_text(drawable, printable_data, text_x_offset):
try: def draw_text(drawable, printable_data, font_regular, font_bold, font_bold_bigger):
font_file_regular = get_font_path_regular() """Draws the formatted text onto the image."""
font_file_bold = get_font_path_bold()
except Exception as ex:
print("unable to find font: " + str(ex))
return
font_size = 20
text_y_offset = 10 text_y_offset = 10
text_y_offset_increment = 25 value_column_x_offset = 120
value_colum_x_offset = 120 font_size = 20
drawable.text((text_x_offset, text_y_offset), printable_data.serialnumber,(0),font=ImageFont.truetype(font_file_bold, 30)) drawable.text(
text_y_offset += 40 (TEXT_X_OFFSET, text_y_offset), printable_data.serialnumber, (0), font=font_bold
)
text_y_offset += 25
drawable.text((text_x_offset, text_y_offset), "Familiy: ",(0),font=ImageFont.truetype(font_file_bold, font_size)) fields = [
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.modelfamiliy,(0),font=ImageFont.truetype(font_file_regular, font_size)) ("Family:", printable_data.modelfamily, font_regular),
text_y_offset += text_y_offset_increment ("Model:", printable_data.modelname, font_regular),
drawable.text((text_x_offset, text_y_offset), "Model: ",(0),font=ImageFont.truetype(font_file_bold, font_size)) ("Hours:", printable_data.power_on_hours, font_regular),
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.modelname,(0),font=ImageFont.truetype(font_file_regular, font_size)) ("Cycles:", printable_data.power_cycle, font_regular),
text_y_offset += text_y_offset_increment ("Errors:", printable_data.smart_error_count, font_regular),
drawable.text((text_x_offset, text_y_offset), "Hours: " ,(0),font=ImageFont.truetype(font_file_bold, font_size)) ("Shred on:", printable_data.shred_timestamp, font_regular),
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.power_on_hours,(0),font=ImageFont.truetype(font_file_regular, font_size)) ("Duration:", printable_data.shred_duration, font_regular),
text_y_offset += text_y_offset_increment ]
drawable.text((text_x_offset, text_y_offset), "Cycles: ",(0),font=ImageFont.truetype(font_file_bold, font_size))
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.power_cycle,(0),font=ImageFont.truetype(font_file_regular, font_size))
text_y_offset += text_y_offset_increment
drawable.text((text_x_offset, text_y_offset), "Errors: ", (0),font=ImageFont.truetype(font_file_bold, font_size))
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.smart_error_count,(0),font=ImageFont.truetype(font_file_regular, font_size))
text_y_offset += text_y_offset_increment
drawable.text((text_x_offset, text_y_offset), "Shred on: ",(0),font=ImageFont.truetype(font_file_bold, font_size))
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.shred_timestamp,(0),font=ImageFont.truetype(font_file_regular, font_size))
text_y_offset += text_y_offset_increment
drawable.text((text_x_offset, text_y_offset), "Duration: " ,(0),font=ImageFont.truetype(font_file_bold, font_size))
drawable.text((text_x_offset+value_colum_x_offset, text_y_offset), printable_data.shred_duration,(0),font=ImageFont.truetype(font_file_regular, font_size))
text_y_offset += text_y_offset_increment
drawable.text((text_x_offset, text_y_offset), printable_data.capacity,(0),font=ImageFont.truetype(font_file_bold, font_size*3)) for label, value, font in fields:
drawable.text((TEXT_X_OFFSET, text_y_offset), label, fill=0, font=font_bold)
drawable.text(
(TEXT_X_OFFSET + value_column_x_offset, text_y_offset),
value,
fill=0,
font=font,
)
text_y_offset += 25
drawable.text(
(TEXT_X_OFFSET, text_y_offset),
printable_data.capacity,
(0),
font=font_bold_bigger,
)
def draw_outline(drawable, margin, width, output_width, output_height):
#upper
drawable.line([(margin,margin), (output_width -margin ,margin)], fill=None, width=width, joint=None)
#left
drawable.line([(margin,margin), (margin ,output_height-margin)], fill=None, width=width, joint=None)
#right
drawable.line([(output_width-margin,margin), (output_width-margin ,output_height-margin)], fill=None, width=width, joint=None)
#lower
drawable.line([(margin,output_height - margin), (output_width-margin,output_height -margin)], fill=None, width=width, joint=None)
def draw_qr_code(image, data): def draw_qr_code(image, data):
"""
Draws a QR code on the provided image at a specific location.
Parameters:
image (Image.Image): The target image.
data (str): The data to encode in the QR code.
"""
# Generate the QR code
qr_img = qrcode.make(data) qr_img = qrcode.make(data)
qr_img.thumbnail((291, 291), Image.Resampling.LANCZOS) qr_img = qr_img.convert("1") # Ensure QR code is in binary (black/white)
image.paste(qr_img, (7, 7))
# Remove white border
bbox = qr_img.getbbox() # Get the bounding box of the non-white area
qr_img = qr_img.crop(bbox)
# Resize to desired size
qr_img = qr_img.resize((QR_CODE_SIZE, QR_CODE_SIZE), Image.Resampling.LANCZOS)
# Paste the QR code onto the image
region = (5, 5, 5 + QR_CODE_SIZE, 5 + QR_CODE_SIZE)
image.paste(qr_img, box=region)
def draw_outline(drawable, margin, width, output_width, output_height):
"""
Draws a rectangular outline on the drawable canvas.
Parameters:
drawable (ImageDraw.Draw): The drawable canvas.
margin (int): The margin from the edges of the image.
width (int): The width of the outline.
output_width (int): The total width of the image.
output_height (int): The total height of the image.
"""
# Define the four corners of the rectangle, adjusting for line width
top_left = (margin, margin)
top_right = (output_width - margin - width, margin)
bottom_left = (margin, output_height - margin - width)
bottom_right = (output_width - margin - width, output_height - margin - width)
# Draw the outline lines with adjusted coordinates
lines = [
(top_left, top_right), # Top edge
(top_left, bottom_left), # Left edge
(top_right, bottom_right), # Right edge
(bottom_left, bottom_right), # Bottom edge
]
for line in lines:
drawable.line(line, fill=0, width=width)
def generate_image(drive, rehdd_info, output_file): def generate_image(drive, rehdd_info, output_file):
output_width = 696 #in px set by used paper """Generates an image containing drive data and a QR code."""
output_height = 300 #in px
text_x_offset= 300 #in px
qr_data = DriveDataJson(drive, rehdd_info)
try:
json_qr_daten = json.dumps(dataclasses.asdict(qr_data))
except Exception as ex:
print("unable to generate json: " + str(ex))
return
try: try:
qr_data = json.dumps(dataclasses.asdict(DriveDataJson(drive, rehdd_info)))
printable_data = format_to_printable(drive) printable_data = format_to_printable(drive)
except Exception as ex: except Exception as e:
print("unable to format data: " + str(ex)) logging.error(f"Error preparing data: {e}")
return return
#print(printable_data.serialnumber)
#create black and white (binary) image with white background output_image = Image.new("1", (OUTPUT_WIDTH, OUTPUT_HEIGHT), "white")
output_image = Image.new('1', (output_width, output_height), "white")
#create draw pane
draw = ImageDraw.Draw(output_image) draw = ImageDraw.Draw(output_image)
draw_outline(draw, 1, 4, output_width, output_height) font_regular = ImageFont.truetype(find_font_path(DEFAULT_FONT_REGULAR), 20)
draw_text(draw, printable_data, text_x_offset) font_bold = ImageFont.truetype(find_font_path(DEFAULT_FONT_BOLD), 20)
draw_qr_code(output_image, str(json_qr_daten).replace(" ", "")) font_bold_bigger = ImageFont.truetype(find_font_path(DEFAULT_FONT_BOLD), 60)
output_image.save(output_file) draw_outline(draw, 1, 4, OUTPUT_WIDTH, OUTPUT_HEIGHT)
draw_text(draw, printable_data, font_regular, font_bold, font_bold_bigger)
draw_qr_code(output_image, qr_data)
try:
output_image.save(output_file)
logging.info(f"Image saved to {output_file}")
except Exception as e:
logging.error(f"Error saving image: {e}")
def main(): def main():
rehdd_info = ReHddInfo("https://git.mosad.xyz/localhorst/reHDD", "bV0.2.2")
rehdd_info = ReHddInfo("https://git.mosad.xyz/localhorst/reHDD", "bV0.2.2") # read this from rehdd process
temp_drive = DriveData( temp_drive = DriveData(
drive_index=0,\ drive_index=0,
drive_state="shredded",\ drive_state="shredded",
modelfamiliy="Toshiba 2.5\\ HDD MK..65GSSX",\ modelfamily='Toshiba 2.5" HDD MK..65GSSX',
modelname="TOSHIBA MK3265GSDX",\ modelname="TOSHIBA MK3265GSDX",
capacity=343597383680,\ capacity=343597383680,
serialnumber="YG6742U56UDRL123456789ABCDEFGJKL",\ serialnumber="YG6742U56UDRL123456789ABCDEFGJKL",
power_on_hours=7074,\ power_on_hours=7074,
power_cycle=4792,\ power_cycle=4792,
smart_error_count=1,\ smart_error_count=1,
shred_timestamp=1647937421,\ shred_timestamp=datetime.datetime.now(datetime.timezone.utc).timestamp(),
shred_duration=81718) shred_duration=81718,
)
generate_image(temp_drive, rehdd_info, "output.png") generate_image(temp_drive, rehdd_info, "output.png")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB