183 lines
7.1 KiB
Python
183 lines
7.1 KiB
Python
from typing import Optional, Union, Literal
|
|
import os
|
|
import aiohttp
|
|
import cups
|
|
from pathlib import Path
|
|
from pdf2image import convert_from_path
|
|
from brother_ql.conversion import convert
|
|
from brother_ql.raster import BrotherQLRaster
|
|
import logging
|
|
import asyncio
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class PrintService:
|
|
def __init__(self, printer_name: Optional[str] = None, printer_api_url: str = "http://localhost:8000/print",
|
|
min_print_interval: int = 30):
|
|
"""Initialize the print service.
|
|
|
|
Args:
|
|
printer_name: Name of the printer to use. If None, will use default printer.
|
|
printer_api_url: URL of the printer API endpoint
|
|
min_print_interval: Minimum time in seconds between print requests for label printer
|
|
"""
|
|
self.printer_name = printer_name
|
|
self.printer_api_url = printer_api_url
|
|
self.status_url = printer_api_url.replace('/print', '/status')
|
|
self.conn = cups.Connection()
|
|
self.cache_dir = Path("app/data/cache/prints")
|
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Rate limiting and coordination
|
|
self.min_print_interval = min_print_interval
|
|
self._last_print_time = None
|
|
self._print_lock = asyncio.Lock()
|
|
|
|
async def _wait_for_printer_ready(self, max_wait: int = 300) -> bool:
|
|
"""Wait for the printer to be ready.
|
|
|
|
Args:
|
|
max_wait: Maximum time to wait in seconds
|
|
|
|
Returns:
|
|
bool: True if printer is ready, False if timeout
|
|
"""
|
|
start_time = time.time()
|
|
while time.time() - start_time < max_wait:
|
|
try:
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.get(self.status_url) as response:
|
|
if response.status == 200:
|
|
data = await response.json()
|
|
if data.get('status') == 'ready':
|
|
return True
|
|
except Exception as e:
|
|
logger.warning(f"Error checking printer status: {e}")
|
|
|
|
await asyncio.sleep(1)
|
|
|
|
logger.error("Timeout waiting for printer to be ready")
|
|
return False
|
|
|
|
async def print_file(self, file_path: Union[str, Path], printer_type: Literal["regular", "label"] = "regular", label_type: Optional[Literal["dk1201", "dk1241"]] = None) -> bool:
|
|
"""Print a PDF or PNG file.
|
|
|
|
Args:
|
|
file_path: Path to the PDF or PNG file
|
|
printer_type: Type of printer ("regular" or "label")
|
|
label_type: Type of label to use ("dk1201" or "dk1241"). Only used when printer_type is "label".
|
|
|
|
Returns:
|
|
bool: True if print was successful, False otherwise
|
|
"""
|
|
try:
|
|
file_path = Path(file_path)
|
|
if not file_path.exists():
|
|
logger.error(f"File not found: {file_path}")
|
|
return False
|
|
|
|
if printer_type == "regular":
|
|
# For regular printers, use CUPS
|
|
printer = self.printer_name or self.conn.getDefault()
|
|
if not printer:
|
|
logger.error("No default printer found")
|
|
return False
|
|
|
|
job_id = self.conn.printFile(
|
|
printer,
|
|
str(file_path),
|
|
f"{file_path.suffix.upper()} Print",
|
|
{}
|
|
)
|
|
logger.info(f"Print job {job_id} submitted to printer {printer}")
|
|
return True
|
|
else:
|
|
# For label printers, we need to coordinate requests
|
|
if label_type is None:
|
|
logger.error("label_type must be specified when printing to label printer")
|
|
return False
|
|
|
|
# Wait for printer to be ready
|
|
if not await self._wait_for_printer_ready():
|
|
logger.error("Printer not ready after waiting")
|
|
return False
|
|
|
|
if file_path.suffix.lower() == '.pdf':
|
|
# Convert PDF to image first
|
|
images = convert_from_path(file_path)
|
|
if not images:
|
|
logger.error(f"No images could be extracted from {file_path}")
|
|
return False
|
|
image = images[0] # Only use first page
|
|
else:
|
|
# For PNG files, we can use them directly
|
|
from PIL import Image
|
|
image = Image.open(file_path)
|
|
|
|
# Convert to label format
|
|
qlr = BrotherQLRaster("QL-1100")
|
|
qlr.exception_on_warning = True
|
|
|
|
# Get label size based on type
|
|
label_size = "29x90" if label_type == "dk1201" else "102x152"
|
|
|
|
converted_image = convert(
|
|
qlr=qlr,
|
|
images=[image],
|
|
label=label_size,
|
|
rotate="0",
|
|
threshold=70.0,
|
|
dither=False,
|
|
compress=False,
|
|
red=False,
|
|
dpi_600=False,
|
|
hq=True,
|
|
cut=True
|
|
)
|
|
|
|
# Cache the converted binary data
|
|
cache_path = self.cache_dir / f"{file_path.stem}_{label_type}_converted.bin"
|
|
with open(cache_path, "wb") as f:
|
|
f.write(converted_image)
|
|
|
|
# Send to API
|
|
return await self._send_print_request(cache_path)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error printing file {file_path}: {str(e)}")
|
|
return False
|
|
|
|
async def _send_print_request(self, file_path: Union[str, Path]) -> bool:
|
|
"""Send print data to printer API.
|
|
|
|
Args:
|
|
file_path: Path to the binary data file to send to the printer
|
|
|
|
Returns:
|
|
bool: True if request was successful, False otherwise
|
|
"""
|
|
try:
|
|
# Read the binary data from the cache file
|
|
with open(file_path, "rb") as f:
|
|
print_data = f.read()
|
|
|
|
# Send the request to the printer API using aiohttp
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.post(
|
|
self.printer_api_url,
|
|
data=print_data,
|
|
timeout=30
|
|
) as response:
|
|
if response.status == 200:
|
|
return True
|
|
else:
|
|
response_text = await response.text()
|
|
logger.error(f"Print request failed with status {response.status}: {response_text}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error sending print request: {str(e)}")
|
|
return False
|