2022-11-16 21:37:01 +01:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
2025-06-08 20:21:42 +02:00
"""
Author : Hendrik Schutter , localhorst @mosad.xyz
Date of creation : 2022 / 11 / 16
Date of last modification : 2025 / 06 / 08
2022-11-16 21:37:01 +01:00
"""
import re
2022-11-22 20:10:45 +01:00
import dataclasses
2022-11-16 21:37:01 +01:00
import glob
2025-06-08 20:21:42 +02:00
import datetime
2022-11-22 20:10:45 +01:00
import json
2025-06-08 20:21:42 +02:00
import logging
2022-11-22 20:10:45 +01:00
import qrcode
2025-06-08 20:21:42 +02:00
from PIL import Image , ImageFont , ImageDraw
# Constants
FONT_PATH = " /usr/share/fonts "
DEFAULT_FONT_REGULAR = " DejaVuSans.ttf "
DEFAULT_FONT_BOLD = " DejaVuSans-Bold.ttf "
OUTPUT_WIDTH = 696 # px
2025-06-22 12:54:48 +02:00
OUTPUT_HEIGHT = 190 # px
TEXT_X_OFFSET = 190 # px
QR_CODE_SIZE = 179 # px
2025-06-08 20:21:42 +02:00
# Configure logging
logging . basicConfig (
level = logging . INFO , format = " %(asctime)s - %(levelname)s - %(message)s "
)
2025-06-22 13:32:56 +02:00
2022-11-22 20:10:45 +01:00
@dataclasses.dataclass
2022-11-16 21:37:01 +01:00
class DriveData :
drive_index : int
2025-06-08 20:21:42 +02:00
drive_state : str
2025-12-05 21:21:18 +01:00
drive_connection_type : str
2025-06-08 20:21:42 +02:00
modelfamily : str
modelname : str
capacity : int
2022-11-16 21:37:01 +01:00
serialnumber : str
2025-06-08 20:21:42 +02:00
power_on_hours : int
2022-11-16 21:37:01 +01:00
power_cycle : int
smart_error_count : int
2025-06-08 20:21:42 +02:00
shred_timestamp : int
shred_duration : int
2025-06-22 13:32:56 +02:00
@dataclasses.dataclass
class DriveDataJson :
state : str
2025-12-05 21:41:52 +01:00
contype : str
2025-06-22 13:32:56 +02:00
fam : str
name : str
cap : int
sn : str
poh : int
pc : int
err : int
time : int
dur : int
2022-11-22 20:10:45 +01:00
@dataclasses.dataclass
2022-11-16 21:37:01 +01:00
class DriveDataPrintable :
2025-12-05 21:21:18 +01:00
connectiontype : str
2025-06-08 20:21:42 +02:00
modelfamily : str
modelname : str
capacity : str
serialnumber : str
power_on_hours : str
power_cycle : str
smart_error_count : str
shred_timestamp : str
shred_duration : str
2022-11-16 21:37:01 +01:00
2022-11-22 20:10:45 +01:00
@dataclasses.dataclass
class ReHddInfo :
2025-06-22 15:08:54 +02:00
link : str
version : str
2022-11-22 20:10:45 +01:00
2025-06-08 20:21:42 +02:00
2022-11-22 20:10:45 +01:00
@dataclasses.dataclass
2025-06-22 13:32:56 +02:00
class QrDataJson :
drive : DriveDataJson
2022-11-22 20:10:45 +01:00
rehdd : ReHddInfo
2022-11-16 21:37:01 +01:00
2025-06-08 20:21:42 +02:00
def find_font_path ( font_name ) :
""" Finds the full path of the specified font. """
try :
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 ( size , decimal_places = 0 , base = 1000 ) :
""" Converts bytes to a human-readable string. """
units = (
[ " B " , " KB " , " MB " , " GB " , " TB " , " PB " ]
if base == 1000
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
2022-11-16 21:37:01 +01:00
def format_to_printable ( drive ) :
2022-11-22 20:54:11 +01:00
return DriveDataPrintable (
2025-12-05 21:21:18 +01:00
drive . drive_connection_type ,
2025-06-08 20:21:42 +02:00
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 , human_readable_capacity ( drive . capacity ) , " end " ) ,
2025-06-22 12:43:13 +02:00
cut_string ( 20 , re . sub ( r " [^a-zA-Z0-9.-_] " , " " , drive . serialnumber ) , " start " ) ,
2025-06-08 20:21:42 +02:00
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 . smart_error_count ) , " end " ) ,
cut_string (
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 , font_regular , font_bold , font_bold_bigger ) :
2025-06-22 12:43:13 +02:00
""" Draws formatted text with Cycles and Errors on one row. """
2025-12-05 21:41:52 +01:00
y_start = 4
2025-06-22 12:43:13 +02:00
line_height = 26
label_x = TEXT_X_OFFSET
value_offset = 115
right_field_spacing = 200 # Horizontal gap between Cycles and Errors
2025-06-22 12:56:40 +02:00
x_capacity = 520
2025-06-22 12:54:48 +02:00
y_capacity = 142
2025-12-05 21:41:52 +01:00
x_connection_type = 600
y_connection_type = y_start
y_spacing_connection_type = 25
2025-06-22 12:43:13 +02:00
# Serial Number
drawable . text ( ( label_x , y_start ) , " Serial: " , fill = 0 , font = font_bold )
drawable . text (
( label_x + value_offset , y_start ) ,
printable_data . serialnumber ,
fill = 0 ,
font = font_bold ,
)
y1 = y_start + line_height
y2 = y1 + line_height
y3 = y2 + line_height
y4 = y3 + line_height
y5 = y4 + line_height
y6 = y5 + line_height
2025-06-08 20:21:42 +02:00
2025-06-22 12:43:13 +02:00
# Left-Aligned Fields (One per row)
drawable . text ( ( label_x , y1 ) , " Family: " , fill = 0 , font = font_bold )
2025-06-08 20:21:42 +02:00
drawable . text (
2025-06-22 12:43:13 +02:00
( label_x + value_offset , y1 ) ,
printable_data . modelfamily ,
fill = 0 ,
font = font_regular ,
2025-06-08 20:21:42 +02:00
)
2025-06-22 12:43:13 +02:00
drawable . text ( ( label_x , y2 ) , " Model: " , fill = 0 , font = font_bold )
drawable . text (
( label_x + value_offset , y2 ) ,
printable_data . modelname ,
fill = 0 ,
font = font_regular ,
)
drawable . text ( ( label_x , y3 ) , " Hours: " , fill = 0 , font = font_bold )
drawable . text (
( label_x + value_offset , y3 ) ,
printable_data . power_on_hours ,
fill = 0 ,
font = font_regular ,
)
# Cycles and Errors on the same line
drawable . text ( ( label_x , y4 ) , " Cycles: " , fill = 0 , font = font_bold )
drawable . text (
( label_x + value_offset , y4 ) ,
printable_data . power_cycle ,
fill = 0 ,
font = font_regular ,
)
2025-06-08 20:21:42 +02:00
drawable . text (
2025-06-22 12:43:13 +02:00
( label_x + right_field_spacing , y4 ) , " Errors: " , fill = 0 , font = font_bold
)
drawable . text (
( label_x + right_field_spacing + value_offset , y4 ) ,
printable_data . smart_error_count ,
fill = 0 ,
font = font_regular ,
)
# Continue remaining fields
drawable . text ( ( label_x , y5 ) , " Shred on: " , fill = 0 , font = font_bold )
drawable . text (
( label_x + value_offset , y5 ) ,
printable_data . shred_timestamp ,
fill = 0 ,
font = font_regular ,
)
drawable . text ( ( label_x , y6 ) , " Duration: " , fill = 0 , font = font_bold )
drawable . text (
( label_x + value_offset , y6 ) ,
printable_data . shred_duration ,
fill = 0 ,
font = font_regular ,
)
# Capacity at the bottom
drawable . text (
( x_capacity , y_capacity ) ,
2025-06-08 20:21:42 +02:00
printable_data . capacity ,
2025-06-22 12:43:13 +02:00
fill = 0 ,
2025-06-08 20:21:42 +02:00
font = font_bold_bigger ,
)
2022-11-20 21:13:44 +01:00
2025-12-05 21:41:52 +01:00
if ( printable_data . connectiontype == " sata " ) :
drawable . text (
( x_connection_type , y_connection_type ) ,
" ⬤ SATA " ,
fill = 0 ,
font = font_regular ,
)
drawable . text (
( x_connection_type , y_connection_type + y_spacing_connection_type ) ,
" ◯ NVME " ,
fill = 0 ,
font = font_regular ,
)
elif ( printable_data . connectiontype == " nvme " ) :
drawable . text (
( x_connection_type , y_connection_type ) ,
" ◯ SATA " ,
fill = 0 ,
font = font_regular ,
)
drawable . text (
( x_connection_type , y_connection_type + y_spacing_connection_type ) ,
" ⬤ NVME " ,
fill = 0 ,
font = font_regular ,
)
else :
drawable . text (
( x_connection_type , y_connection_type ) ,
" ◯ SATA " ,
fill = 0 ,
font = font_regular ,
)
drawable . text (
( x_connection_type , y_connection_type + y_spacing_connection_type ) ,
" ◯ NVME " ,
fill = 0 ,
font = font_regular ,
)
2022-11-20 21:13:44 +01:00
2022-11-22 20:10:45 +01:00
def draw_qr_code ( image , data ) :
2025-06-08 20:21:42 +02:00
"""
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
2022-11-22 20:10:45 +01:00
qr_img = qrcode . make ( data )
2025-06-08 20:21:42 +02:00
qr_img = qr_img . convert ( " 1 " ) # Ensure QR code is in binary (black/white)
2022-11-20 21:13:44 +01:00
2025-06-08 20:21:42 +02:00
# Remove white border
bbox = qr_img . getbbox ( ) # Get the bounding box of the non-white area
qr_img = qr_img . crop ( bbox )
2022-11-16 21:37:01 +01:00
2025-06-08 20:21:42 +02:00
# Resize to desired size
qr_img = qr_img . resize ( ( QR_CODE_SIZE , QR_CODE_SIZE ) , Image . Resampling . LANCZOS )
2022-11-16 21:37:01 +01:00
2025-06-08 20:21:42 +02:00
# 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 )
2022-11-16 21:37:01 +01:00
2025-06-08 20:21:42 +02:00
def generate_image ( drive , rehdd_info , output_file ) :
""" Generates an image containing drive data and a QR code. """
2022-11-23 18:38:27 +01:00
try :
2025-06-22 13:32:56 +02:00
drive_json = DriveDataJson (
state = drive . drive_state ,
2025-12-05 21:41:52 +01:00
contype = drive . drive_connection_type ,
2025-06-22 13:32:56 +02:00
fam = drive . modelfamily ,
name = drive . modelname ,
cap = drive . capacity ,
sn = drive . serialnumber ,
poh = drive . power_on_hours ,
pc = drive . power_cycle ,
err = drive . smart_error_count ,
time = int ( drive . shred_timestamp ) ,
dur = drive . shred_duration ,
)
qr_data = json . dumps ( dataclasses . asdict ( QrDataJson ( drive_json , rehdd_info ) ) )
2022-11-23 18:38:27 +01:00
printable_data = format_to_printable ( drive )
2025-06-08 20:21:42 +02:00
except Exception as e :
logging . error ( f " Error preparing data: { e } " )
2022-11-23 18:38:27 +01:00
return
2022-11-22 20:10:45 +01:00
2025-06-08 20:21:42 +02:00
output_image = Image . new ( " 1 " , ( OUTPUT_WIDTH , OUTPUT_HEIGHT ) , " white " )
2022-11-16 21:37:01 +01:00
draw = ImageDraw . Draw ( output_image )
2025-06-22 12:56:40 +02:00
font_regular = ImageFont . truetype ( find_font_path ( DEFAULT_FONT_REGULAR ) , 20 )
font_bold = ImageFont . truetype ( find_font_path ( DEFAULT_FONT_BOLD ) , 20 )
2025-06-22 12:54:48 +02:00
font_bold_bigger = ImageFont . truetype ( find_font_path ( DEFAULT_FONT_BOLD ) , 42 )
2022-11-16 21:37:01 +01:00
2025-06-22 13:32:56 +02:00
draw_outline ( draw , 0 , 3 , OUTPUT_WIDTH + 1 , OUTPUT_HEIGHT + 1 )
2025-06-08 20:21:42 +02:00
draw_text ( draw , printable_data , font_regular , font_bold , font_bold_bigger )
draw_qr_code ( output_image , qr_data )
2022-11-23 18:38:27 +01:00
2025-06-08 20:21:42 +02:00
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 } " )
2022-11-23 18:38:27 +01:00
2025-06-08 20:21:42 +02:00
def main ( ) :
rehdd_info = ReHddInfo ( " https://git.mosad.xyz/localhorst/reHDD " , " bV0.2.2 " )
2022-11-23 18:38:27 +01:00
temp_drive = DriveData (
2025-06-08 20:21:42 +02:00
drive_index = 0 ,
2025-12-05 21:21:18 +01:00
drive_connection_type = " sata " ,
2025-06-08 20:21:42 +02:00
drive_state = " shredded " ,
modelfamily = ' Toshiba 2.5 " HDD MK..65GSSX ' ,
modelname = " TOSHIBA MK3265GSDX " ,
2025-06-22 12:56:40 +02:00
capacity = 343597383000 ,
2025-06-08 20:21:42 +02:00
serialnumber = " YG6742U56UDRL123456789ABCDEFGJKL " ,
power_on_hours = 7074 ,
power_cycle = 4792 ,
smart_error_count = 1 ,
shred_timestamp = datetime . datetime . now ( datetime . timezone . utc ) . timestamp ( ) ,
shred_duration = 81718 ,
)
2022-11-23 18:38:27 +01:00
generate_image ( temp_drive , rehdd_info , " output.png " )
2022-11-16 21:37:01 +01:00
2025-06-22 12:43:13 +02:00
2022-11-16 21:37:01 +01:00
if __name__ == " __main__ " :
2025-06-08 20:21:42 +02:00
main ( )