Compare commits

...

3 Commits

8 changed files with 269 additions and 317 deletions

3
.gitignore vendored
View File

@@ -1,5 +1,6 @@
label/
build/
dist/
logs/
pdf_backup/
conf/ghostscript/
@@ -9,4 +10,6 @@ __pycache__/
*.pyc
*.pyo
*.pyd
build_output.log
.vscode/

View File

@@ -23,12 +23,12 @@ if errorlevel 1 (
exit /b 1
)
echo [1/5] Checking Python installation...
echo [1/6] Checking Python installation...
python --version
echo.
REM Upgrade pip
echo [2/5] Upgrading pip, setuptools, and wheel...
echo [2/6] Upgrading pip, setuptools, and wheel...
python -m pip install --upgrade pip setuptools wheel
if errorlevel 1 (
echo ERROR: Failed to upgrade pip
@@ -50,7 +50,7 @@ echo.
REM Copy GhostScript binaries for bundling (optional but strongly recommended)
echo [4/6] Preparing GhostScript for bundling...
call prepare_ghostscript.bat
python prepare_ghostscript.py
echo.
REM Check that SumatraPDF.exe exists before building

View File

@@ -1,20 +1,110 @@
from reportlab.lib.pagesizes import landscape
from reportlab.lib.utils import ImageReader
from reportlab.pdfgen import canvas
"""
check_pdf_size.py verify that a PDF's page dimensions match 35 mm × 25 mm.
Usage:
python check_pdf_size.py [path/to/label.pdf]
If no path is given the script operates on every PDF found in pdf_backup/.
Exit code 0 = all OK, 1 = mismatch or error.
"""
import os
import sys
# Check the test PDF file
if os.path.exists('test_label.pdf'):
file_size = os.path.getsize('test_label.pdf')
print(f'test_label.pdf exists ({file_size} bytes)')
print(f'Expected: 35mm x 25mm landscape (99.2 x 70.9 points)')
print(f'')
print(f'Open test_label.pdf in a PDF viewer to verify:')
print(f' - Page size should be wider than tall')
print(f' - Content should be correctly oriented')
print(f'')
print(f'In Adobe Reader: File > Properties > Description')
print(f' Page size should show: 3.5 x 2.5 cm or 1.38 x 0.98 in')
else:
print('test_label.pdf not found')
# ── Target dimensions ────────────────────────────────────────────────────────
TARGET_W_MM = 35.0 # width (landscape, wider side)
TARGET_H_MM = 25.0 # height
TOLERANCE_MM = 0.5 # ± 0.5 mm is acceptable rounding from PDF viewers
PT_PER_MM = 72.0 / 25.4 # 1 mm in points
def read_page_size_pt(pdf_path):
"""
Return (width_pt, height_pt) of the first page of *pdf_path*.
Tries pypdf first, then pymupdf (fitz) as a fallback.
Raises RuntimeError if neither library is available.
"""
# ── pypdf ────────────────────────────────────────────────────────────────
try:
from pypdf import PdfReader # type: ignore
reader = PdfReader(pdf_path)
page = reader.pages[0]
w = float(page.mediabox.width)
h = float(page.mediabox.height)
return w, h
except ImportError:
pass
# ── pymupdf (fitz) ───────────────────────────────────────────────────────
try:
import fitz # type: ignore
doc = fitz.open(pdf_path)
rect = doc[0].rect
return rect.width, rect.height
except ImportError:
pass
raise RuntimeError(
"Install pypdf or pymupdf:\n"
" pip install pypdf\n"
" pip install pymupdf"
)
def check_file(pdf_path):
"""Print a pass/fail line for one PDF. Returns True if dimensions match."""
if not os.path.exists(pdf_path):
print(f" MISS {pdf_path} (file not found)")
return False
try:
w_pt, h_pt = read_page_size_pt(pdf_path)
except Exception as e:
print(f" ERR {pdf_path} ({e})")
return False
w_mm = w_pt / PT_PER_MM
h_mm = h_pt / PT_PER_MM
w_ok = abs(w_mm - TARGET_W_MM) <= TOLERANCE_MM
h_ok = abs(h_mm - TARGET_H_MM) <= TOLERANCE_MM
ok = w_ok and h_ok
status = "PASS" if ok else "FAIL"
print(
f" {status} {os.path.basename(pdf_path)}"
f" {w_mm:.2f}×{h_mm:.2f} mm"
f" (target {TARGET_W_MM}×{TARGET_H_MM} mm ±{TOLERANCE_MM} mm)"
)
return ok
def main():
targets = sys.argv[1:]
if not targets:
backup_dir = os.path.join(os.path.dirname(__file__), "pdf_backup")
if os.path.isdir(backup_dir):
targets = [
os.path.join(backup_dir, f)
for f in sorted(os.listdir(backup_dir))
if f.lower().endswith(".pdf")
]
if not targets:
# fall back to test_label.pdf in cwd
targets = ["test_label.pdf"]
print(f"Checking {len(targets)} PDF(s)…")
results = [check_file(p) for p in targets]
total = len(results)
passed = sum(results)
failed = total - passed
print(f"\n {passed}/{total} passed" + (f", {failed} FAILED" if failed else ""))
sys.exit(0 if failed == 0 else 1)
if __name__ == "__main__":
main()

View File

@@ -990,7 +990,7 @@ printer = PDF
f'Printing {idx+1} of {count}...'
), 0)
success = print_label_standalone(label_text, printer, preview=0, use_pdf=True, svg_template=template_path)
success = print_label_standalone(label_text, printer, preview=0, svg_template=template_path)
if not success:
all_success = False

View File

@@ -22,6 +22,10 @@ set GS_BIN_SRC=
set GS_LIB_SRC=
set GS_EXE=
REM Pre-assign ProgramFiles(x86) to avoid batch parser choking on the parentheses
set "PF64=%ProgramFiles%"
set "PF32=%ProgramFiles(x86)%"
echo.
echo ============================================================
echo GhostScript Bundle Preparation
@@ -29,7 +33,7 @@ echo ============================================================
echo.
REM ---- Search for GhostScript in both Program Files locations ----
for %%P in ("%ProgramFiles%" "%ProgramFiles(x86)%") do (
for %%P in ("%PF64%" "%PF32%") do (
if exist "%%~P\gs" (
for /d %%V in ("%%~P\gs\gs*") do (
if exist "%%~V\bin\gswin64c.exe" (

115
prepare_ghostscript.py Normal file
View File

@@ -0,0 +1,115 @@
"""
prepare_ghostscript.py
Finds the system GhostScript installation and copies the files needed for
bundling into conf/ghostscript/
Files copied:
conf/ghostscript/bin/gswin64c.exe (or gswin32c.exe)
conf/ghostscript/bin/gsdll64.dll (or gsdll32.dll)
conf/ghostscript/lib/*.ps (PostScript init files)
Run this ONCE before building the exe with build_windows.bat.
"""
import os
import shutil
import glob
import sys
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DEST_BIN = os.path.join(SCRIPT_DIR, "conf", "ghostscript", "bin")
DEST_LIB = os.path.join(SCRIPT_DIR, "conf", "ghostscript", "lib")
SEARCH_ROOTS = [
os.environ.get("ProgramFiles", r"C:\Program Files"),
os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)"),
os.environ.get("ProgramW6432", r"C:\Program Files"),
]
FLAVOURS = [
("gswin64c.exe", "gsdll64.dll"),
("gswin32c.exe", "gsdll32.dll"),
]
print()
print("=" * 60)
print(" GhostScript Bundle Preparation")
print("=" * 60)
print()
def find_ghostscript():
"""Return (bin_dir, lib_dir, exe_name, dll_name) or None."""
for root in dict.fromkeys(r for r in SEARCH_ROOTS if r): # unique, preserve order
gs_root = os.path.join(root, "gs")
if not os.path.isdir(gs_root):
continue
# Each versioned sub-folder, newest first
versions = sorted(
[d for d in glob.glob(os.path.join(gs_root, "gs*")) if os.path.isdir(d)],
reverse=True,
)
for ver_dir in versions:
bin_dir = os.path.join(ver_dir, "bin")
lib_dir = os.path.join(ver_dir, "lib")
for exe, dll in FLAVOURS:
if os.path.isfile(os.path.join(bin_dir, exe)):
print(f"Found GhostScript: {ver_dir}")
return bin_dir, lib_dir, exe, dll
return None
result = find_ghostscript()
if result is None:
print("WARNING: GhostScript is NOT installed on this machine.")
print()
print(" The exe will still build successfully, but GhostScript will")
print(" NOT be bundled. On the target machine the app will fall back")
print(" to SumatraPDF for printing (which may produce dotted output).")
print()
print(" To get sharp vector-quality printing, install GhostScript:")
print(" https://ghostscript.com/releases/gsdnld.html")
print(" Then re-run this script before building.")
print()
sys.exit(0)
src_bin, src_lib, exe_name, dll_name = result
# Create destination folders
os.makedirs(DEST_BIN, exist_ok=True)
os.makedirs(DEST_LIB, exist_ok=True)
# Copy executable and DLL
print("Copying GhostScript binaries...")
for fname in (exe_name, dll_name):
src = os.path.join(src_bin, fname)
dst = os.path.join(DEST_BIN, fname)
if os.path.isfile(src):
shutil.copy2(src, dst)
print(f" + {fname}")
else:
print(f" WARNING: {fname} not found in {src_bin}")
# Copy lib/ (PostScript init files .ps)
print()
print("Copying GhostScript lib (PostScript init files)...")
ps_files = glob.glob(os.path.join(src_lib, "*.ps"))
for ps in ps_files:
shutil.copy2(ps, DEST_LIB)
print(f" + {len(ps_files)} .ps files copied from {src_lib}")
# Report
bin_count = len(os.listdir(DEST_BIN))
lib_count = len(os.listdir(DEST_LIB))
print()
print("=" * 60)
print(" GhostScript prepared successfully in conf\\ghostscript\\")
print("=" * 60)
print()
print(f" bin\\ : {bin_count} file(s)")
print(f" lib\\ : {lib_count} file(s)")
print()
print("GhostScript will be embedded into LabelPrinter.exe at build time.")
print()

View File

@@ -1,6 +1,3 @@
from PIL import Image, ImageDraw, ImageFont
import barcode
from barcode.writer import ImageWriter
import time
import os
import sys
@@ -89,119 +86,6 @@ def get_available_printers():
return ["PDF"]
def create_label_image(text):
"""
Create a label image with 3 rows: label + barcode for each field.
Args:
text (str): Combined text in format "SAP|CANTITATE|LOT" or single value
Returns:
PIL.Image: The generated label image
"""
# Parse the text input
parts = text.split('|') if '|' in text else [text, '', '']
sap_nr = parts[0].strip() if len(parts) > 0 else ''
cantitate = parts[1].strip() if len(parts) > 1 else ''
lot_number = parts[2].strip() if len(parts) > 2 else ''
# Label dimensions (narrower, 3 rows)
label_width = 800 # 8 cm
label_height = 600 # 6 cm
# Create canvas
label_img = Image.new('RGB', (label_width, label_height), 'white')
draw = ImageDraw.Draw(label_img)
# Row setup - 3 equal rows
row_height = label_height // 3
left_margin = 15
row_spacing = 3
# Fonts
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
try:
label_font = ImageFont.truetype(font_path, 16)
value_font = ImageFont.truetype(font_path, 14)
except IOError:
label_font = ImageFont.load_default()
value_font = ImageFont.load_default()
# Data for 3 rows
rows_data = [
("SAP-Nr", sap_nr),
("Cantitate", cantitate),
("Lot Nr", lot_number),
]
# Generate barcodes first
CODE128 = barcode.get_barcode_class('code128')
writer_options = {
"write_text": False,
"module_width": 0.4,
"module_height": 8,
"quiet_zone": 2,
"font_size": 0
}
barcode_images = []
for _, value in rows_data:
if value:
try:
code = CODE128(value[:25], writer=ImageWriter())
filename = code.save('temp_barcode', options=writer_options)
barcode_img = Image.open(filename)
barcode_images.append(barcode_img)
except:
barcode_images.append(None)
else:
barcode_images.append(None)
# Draw each row with label and barcode
for idx, ((label_name, value), barcode_img) in enumerate(zip(rows_data, barcode_images)):
row_y = idx * row_height
# Draw label name
draw.text(
(left_margin, row_y + 3),
label_name,
fill='black',
font=label_font
)
# Draw barcode if available
if barcode_img:
# Resize barcode to fit in row width
barcode_width = label_width - left_margin - 10
barcode_height = row_height - 25
# Use high-quality resampling for crisp barcodes
try:
# Try newer Pillow API first
from PIL.Image import Resampling
barcode_resized = barcode_img.resize((barcode_width, barcode_height), Resampling.LANCZOS)
except (ImportError, AttributeError):
# Fallback for older Pillow versions
barcode_resized = barcode_img.resize((barcode_width, barcode_height), Image.LANCZOS)
label_img.paste(barcode_resized, (left_margin, row_y + 20))
else:
# Fallback: show value as text
draw.text(
(left_margin, row_y + 25),
value if value else "(empty)",
fill='black',
font=value_font
)
# Clean up temporary barcode files
try:
if os.path.exists('temp_barcode.png'):
os.remove('temp_barcode.png')
except:
pass
return label_img
def create_label_pdf(text, svg_template=None):
"""
Create a high-quality PDF label with 3 rows: label + barcode for each field.
@@ -259,94 +143,6 @@ def create_label_pdf(text, svg_template=None):
return generator.create_label_pdf(article, nr_art, serial, pdf_filename, image_path, selected_template)
def configure_printer_quality(printer_name, width_mm=35, height_mm=25):
"""
Configure printer for high quality label printing (Windows only).
Sets paper size, orientation, and QUALITY settings.
Args:
printer_name (str): Name of the printer
width_mm (int): Label width in millimeters (default 35)
height_mm (int): Label height in millimeters (default 25)
Returns:
bool: True if successful
"""
if SYSTEM != "Windows" or not WIN32_AVAILABLE:
return False
try:
import win32print
import pywintypes
hprinter = win32print.OpenPrinter(printer_name)
try:
# Get current printer properties
props = win32print.GetPrinter(hprinter, 2)
devmode = props.get("pDevMode")
if devmode is None:
print("Could not get printer DEVMODE")
return False
# CRITICAL: Set print quality to HIGHEST
# This prevents dotted/pixelated text
try:
devmode.PrintQuality = 600 # 600 DPI (high quality)
except:
try:
devmode.PrintQuality = 4 # DMRES_HIGH
except:
pass
# Set custom paper size
try:
devmode.PaperSize = 256 # DMPAPER_USER (custom size)
devmode.PaperLength = height_mm * 10 # Height in 0.1mm units
devmode.PaperWidth = width_mm * 10 # Width in 0.1mm units
except:
pass
# Set orientation to PORTRAIT (1 = no rotation).
# For a 35mm × 25mm thermal label, Portrait means "print across the
# 35mm print-head width without rotating". Landscape (2) would
# rotate the output 90° CCW, which is exactly the reported
# "rotated-left" symptom so we must NOT use Landscape here.
try:
devmode.Orientation = 1 # Portrait = no rotation
except:
pass
# Set additional quality settings
try:
devmode.Color = 1 # Monochrome for labels
except:
pass
try:
devmode.TTOption = 2 # DMTT_BITMAP - print TrueType as graphics (sharper)
except:
pass
# Apply settings
try:
props["pDevMode"] = devmode
win32print.SetPrinter(hprinter, 2, props, 0)
print(f"Printer configured: {width_mm}x{height_mm}mm @ HIGH QUALITY")
return True
except Exception as set_err:
print(f"Could not apply printer settings: {set_err}")
return False
finally:
win32print.ClosePrinter(hprinter)
except Exception as e:
print(f"Could not configure printer quality: {e}")
return False
def find_ghostscript():
"""
Find GhostScript executable. Search order:
@@ -403,10 +199,8 @@ def print_pdf_with_ghostscript(pdf_path, printer_name):
# -dTextAlphaBits=4 : anti-aliasing for text
# -dGraphicsAlphaBits=4
# -dNOSAFER : allow file access needed for fonts
# -dDEVICEWIDTHPOINTS \
# -dDEVICEHEIGHTPOINTS : explicit label size (35mm x 25mm in pts)
# -dFIXEDMEDIA : do not auto-scale/rotate to a different size
# 35mm = 35/25.4*72 ≈ 99.21 pt, 25mm = 25/25.4*72 ≈ 70.87 pt
# The printer paper size and orientation are already configured
# in the driver do not override them here.
cmd = [
gs_path,
"-dNOPAUSE",
@@ -416,9 +210,6 @@ def print_pdf_with_ghostscript(pdf_path, printer_name):
"-r600",
"-dTextAlphaBits=4",
"-dGraphicsAlphaBits=4",
"-dDEVICEWIDTHPOINTS=99.21", # 35 mm
"-dDEVICEHEIGHTPOINTS=70.87", # 25 mm
"-dFIXEDMEDIA",
f"-sOutputFile=%printer%{printer_name}",
pdf_path,
]
@@ -472,19 +263,10 @@ def print_to_printer(printer_name, file_path):
try:
if WIN32_AVAILABLE:
import win32print
import win32api
# Configure printer DEVMODE BEFORE sending any job.
# This is critical: it sets Portrait orientation (no rotation)
# and maximum print quality so the 35mm×25mm PDF maps
# directly to the physical label without being auto-rotated
# by the driver (which caused the 90° "rotated left" symptom).
configure_printer_quality(printer_name)
if file_path.endswith('.pdf'):
# Try silent printing methods (no viewer opens)
import os
import winreg
printed = False
@@ -536,14 +318,9 @@ def print_to_printer(printer_name, file_path):
try:
print(f"Using SumatraPDF: {sumatra_path}")
print(f"Sending to printer: {printer_name}")
# The printer DEVMODE has already been configured
# above (Portrait, 35mm×25mm, high quality).
# "noscale" tells SumatraPDF to send the PDF
# at its exact size without any shrink/fit.
# Do NOT add "landscape" here: the DEVMODE
# Portrait setting already matches the label
# orientation; adding landscape would tell the
# driver to rotate 90° again and undo the fix.
# "noscale" = print the PDF at its exact page size.
# Do NOT add "landscape" the printer driver
# already knows the orientation from its own settings.
result = subprocess.run([
sumatra_path,
"-print-to",
@@ -601,73 +378,41 @@ def print_to_printer(printer_name, file_path):
return True
def print_label_standalone(value, printer, preview=0, use_pdf=True, svg_template=None):
def print_label_standalone(value, printer, preview=0, svg_template=None):
"""
Print a label with the specified text on the specified printer.
Always generates a PDF backup in pdf_backup and prints that PDF.
Generate a PDF label, save it to pdf_backup/, and print it on the printer.
The printer's own paper size and orientation settings are used as-is.
Args:
value (str): The text to print on the label
printer (str): The name of the printer to use
preview (int): 0 = no preview, 1-3 = 3s preview, >3 = 5s preview
use_pdf (bool): False to also generate a PNG if PDF generation fails
svg_template (str): Path to specific SVG template to use (optional)
value (str): Label data in format "article;nr_art;serial;status"
printer (str): Printer name (or "PDF" to skip physical printing)
preview (int): 0 = print immediately; 1-3 = 3 s delay; >3 = 5 s delay
svg_template (str): Path to SVG template (optional; auto-selected if None)
Returns:
bool: True if printing was successful, False otherwise
bool: True if sending to printer succeeded, False otherwise
"""
# Track generated files
file_created = False
temp_file = None
pdf_file = None
try:
# Debug output
print(f"Preview value: {preview}")
print(f"Preview type: {type(preview)}")
print(f"Using format: {'PDF' if use_pdf else 'PNG'}")
# Always generate a PDF backup and print that PDF for verification
# Step 1 Generate and save PDF to pdf_backup/
try:
pdf_file = create_label_pdf(value, svg_template)
if pdf_file and os.path.exists(pdf_file):
print(f"PDF label created: {pdf_file}")
print(f"PDF backup saved to: {pdf_file}")
else:
print("PDF generation returned no file path")
print("PDF generation failed no output file")
return False
except Exception as pdf_err:
print(f"PDF generation failed: {pdf_err}")
# Optionally also create the label image (PNG)
if not pdf_file or not os.path.exists(pdf_file):
if not use_pdf:
label_img = create_label_image(value)
temp_file = 'final_label.png'
label_img.save(temp_file)
print(f"PNG label created: {temp_file}")
else:
temp_file = pdf_file
file_created = True
if not temp_file or not os.path.exists(temp_file):
print("No label file created for printing")
print(f"PDF generation error: {pdf_err}")
return False
# Convert preview to int if it's a string
# Step 2 Optional countdown before printing
if isinstance(preview, str):
preview = int(preview)
if preview > 0: # Any value above 0 shows a preview message
# Calculate preview duration in seconds
if 1 <= preview <= 3:
preview_sec = 3 # 3 seconds
else: # preview > 3
preview_sec = 5 # 5 seconds
print(f"Printing in {preview_sec} seconds... (Press Ctrl+C to cancel)")
# Simple countdown timer using time.sleep
if preview > 0:
preview_sec = 3 if 1 <= preview <= 3 else 5
print(f"Printing in {preview_sec} seconds… (Ctrl+C to cancel)")
try:
for i in range(preview_sec, 0, -1):
print(f" {i}...", end=" ", flush=True)
@@ -677,24 +422,19 @@ def print_label_standalone(value, printer, preview=0, use_pdf=True, svg_template
print("\nCancelled by user")
return False
# Print after preview
print("Sending to printer...")
return print_to_printer(printer, temp_file)
else:
print("Direct printing without preview...")
# Direct printing without preview (preview = 0)
return print_to_printer(printer, temp_file)
# Step 3 Send to printer
print("Sending to printer...")
return print_to_printer(printer, pdf_file)
except Exception as e:
print(f"Error printing label: {str(e)}")
return False
finally:
# This block always executes, ensuring cleanup
if pdf_file and os.path.exists(pdf_file):
print("Cleanup complete - PDF backup saved to pdf_backup folder")
print("Cleanup complete PDF backup saved to pdf_backup/")
else:
print("Cleanup complete - label file retained for reference")
print("Cleanup complete")
# Main code removed - import this module or run as part of the Kivy GUI application

View File

@@ -49,8 +49,8 @@ class PDFLabelGenerator:
"""
self.label_width = label_width * cm
self.label_height = label_height * cm
# Force landscape: ensure width > height
self.page_size = landscape((self.label_height, self.label_width)) if self.label_width > self.label_height else (self.label_width, self.label_height)
# label_width (3.5 cm) > label_height (2.5 cm) → page is already landscape
self.page_size = (self.label_width, self.label_height)
self.dpi = dpi
self.margin = 1 * mm # Minimal margin