from PIL import Image, ImageDraw, ImageFont import sys, json, os config = json.loads(sys.argv[1]) # Parse configuration language = config.get('language', 'english') format_config = config.get('format', {}) layout = config.get('layout', 'standard') fonts_config = config.get('fonts', {}) images_config = config.get('images', {}) content = config.get('content', {}) # Page dimensions (A4 at 204x196 dpi) W, H = 1728, 2291 img = Image.new('L', (W, H), 255) draw = ImageDraw.Draw(img) # Font fallback chains FONT_FALLBACKS = { 'english': [ '/System/Library/Fonts/Helvetica.ttc', '/System/Library/Fonts/Supplemental/Arial Unicode.ttf', ], 'chinese': [ '/System/Library/AssetsV2/com_apple_MobileAsset_Font8/86ba2c91f017a3749571a82f2c6d890ac7ffb2fb.asset/AssetData/PingFang.ttc', '/System/Library/Fonts/STHeiti Medium.ttc', '/System/Library/Fonts/Supplemental/Arial Unicode.ttf', ], 'japanese': [ '/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc', '/System/Library/Fonts/Supplemental/Arial Unicode.ttf', ], 'korean': [ '/System/Library/Fonts/AppleSDGothicNeo.ttc', '/System/Library/Fonts/Supplemental/Arial Unicode.ttf', ], 'german': [ '/System/Library/Fonts/Helvetica.ttc', '/System/Library/Fonts/Supplemental/Arial Unicode.ttf', ], 'french': [ '/System/Library/Fonts/Helvetica.ttc', '/System/Library/Fonts/Supplemental/Arial Unicode.ttf', ], 'spanish': [ '/System/Library/Fonts/Helvetica.ttc', '/System/Library/Fonts/Supplemental/Arial Unicode.ttf', ], } # Language-specific labels LABELS = { 'english': { 'title': 'FACSIMILE', 'to': 'TO:', 'from': 'FROM:', 'date': 'DATE:', 'pages': 'PAGES:', 'subject': 'SUBJECT:', 'notes': 'NOTES:', 'urgency': { 'normal': 'NORMAL', 'urgent': 'URGENT', 'very_urgent': 'VERY URGENT', 'fyi': 'FOR YOUR INFORMATION', 'reply': 'PLEASE REPLY', }, }, 'chinese': { 'title': '傳真', 'to': '收件人:', 'from': '發件人:', 'date': '日期:', 'pages': '頁數:', 'subject': '主題:', 'notes': '備註:', 'urgency': { 'normal': '普通', 'urgent': '緊急', 'very_urgent': '非常緊急', 'fyi': '參考', 'reply': '請回覆', }, }, 'japanese': { 'title': 'FAX', 'to': '宛先:', 'from': '送信者:', 'date': '日付:', 'pages': 'ページ:', 'subject': '件名:', 'notes': '備考:', 'urgency': { 'normal': '通常', 'urgent': '緊急', 'very_urgent': '至急', 'fyi': '参考', 'reply': '返信願', }, }, 'korean': { 'title': '팩스', 'to': '수신:', 'from': '발신:', 'date': '날짜:', 'pages': '페이지:', 'subject': '제목:', 'notes': '참고:', 'urgency': { 'normal': '일반', 'urgent': '긴급', 'very_urgent': '매우 긴급', 'fyi': '참고', 'reply': '회신 요망', }, }, 'german': { 'title': 'FAX', 'to': 'AN:', 'from': 'VON:', 'date': 'DATUM:', 'pages': 'SEITEN:', 'subject': 'BETREFF:', 'notes': 'NOTIZEN:', 'urgency': { 'normal': 'NORMAL', 'urgent': 'DRINGEND', 'very_urgent': 'SEHR DRINGEND', 'fyi': 'ZUR INFORMATION', 'reply': 'BITTE ANTWORT', }, }, 'french': { 'title': 'FAX', 'to': 'À:', 'from': 'DE:', 'date': 'DATE:', 'pages': 'PAGES:', 'subject': 'OBJET:', 'notes': 'NOTES:', 'urgency': { 'normal': 'NORMAL', 'urgent': 'URGENT', 'very_urgent': 'TRÈS URGENT', 'fyi': 'POUR INFORMATION', 'reply': 'MERCI DE RÉPONDRE', }, }, 'spanish': { 'title': 'FAX', 'to': 'PARA:', 'from': 'DE:', 'date': 'FECHA:', 'pages': 'PÁGINAS:', 'subject': 'ASUNTO:', 'notes': 'NOTAS:', 'urgency': { 'normal': 'NORMAL', 'urgent': 'URGENTE', 'very_urgent': 'MUY URGENTE', 'fyi': 'PARA SU INFORMACIÓN', 'reply': 'POR FAVOR RESPONDA', }, }, } def load_font(lang, size, index=0): """Load font with fallback chain""" fallbacks = FONT_FALLBACKS.get(lang, FONT_FALLBACKS['english']) for font_path in fallbacks: if os.path.exists(font_path): try: if 'PingFang' in font_path: # PingFang font indices: 2=Light, 6=Medium, 10=Semibold if size >= 60: return ImageFont.truetype(font_path, size, index=10) elif size >= 40: return ImageFont.truetype(font_path, size, index=6) else: return ImageFont.truetype(font_path, size, index=2) else: return ImageFont.truetype(font_path, size, index=index) except Exception as e: continue return ImageFont.load_default() def load_image(path): """Load image in various formats (PNG, JPEG, TIFF, BMP, GIF, WebP)""" try: img = Image.open(path) # Convert to grayscale if needed if img.mode not in ('L', 'RGB'): img = img.convert('RGB') return img except Exception as e: print(json.dumps({"ok": False, "error": f"Failed to load image {path}: {str(e)}"})) return None def add_image_with_opacity(draw, img, position, size, opacity): """Add image with specified opacity""" if img is None: return # Resize if size specified if size: img = img.resize((size['width'], size['height']), Image.LANCZOS) # Apply opacity if opacity < 1.0: # Create alpha mask alpha = img.split()[-1] if img.mode == 'RGBA' else Image.new('L', img.size, 255) alpha = alpha.point(lambda p: int(p * opacity)) if img.mode != 'RGBA': img = img.convert('RGBA') img.putalpha(alpha) # Paste onto base image if img.mode == 'RGBA': base.paste(img, position, img) else: base.paste(img, position) # Load fonts font_size_title = fonts_config.get('title', 72) font_size_heading = fonts_config.get('heading', 44) font_size_body = fonts_config.get('body', 44) font_size_small = fonts_config.get('small', 36) font_title = load_font(language, font_size_title) font_bold = load_font(language, font_size_heading) font_normal = load_font(language, font_size_body) font_small = load_font(language, font_size_small) # Get labels for language labels = LABELS.get(language, LABELS['english']) # Add background image if 'background' in images_config: bg_config = images_config['background'] bg_img = load_image(bg_config['path']) if bg_img: opacity = bg_config.get('opacity', 0.3) add_image_with_opacity(draw, bg_img, (0, 0), None, opacity) # Add logo if 'logo' in images_config: logo_config = images_config['logo'] logo_img = load_image(logo_config['path']) if logo_img: pos = logo_config.get('position', {'x': 50, 'y': 50}) size = logo_config.get('size') opacity = logo_config.get('opacity', 1.0) # Convert percentage to pixels x = int(W * pos['x'] / 100) y = int(H * pos['y'] / 100) add_image_with_opacity(draw, logo_img, (x, y), size, opacity) # Draw footer bar draw.rectangle([(0, 2030), (W, 2230)], fill=0) # Draw title title = content.get('title', labels['title']) _, _, tw, _ = draw.textbbox((0, 0), title, font=font_title) draw.text(((W - tw) / 2, 2080), title, fill=255, font=font_title) # Draw line separator draw.line([(100, 1980), (W - 100, 1980)], fill=0, width=3) # Date string from datetime import datetime date_str = content.get('date', datetime.now().strftime('%Y/%m/%d %H:%M')) # Content fields fields = [ (labels['to'], content.get('to', '')), (labels['from'], content.get('from', '')), (labels['date'], date_str), (labels['pages'], str(content.get('total_pages', 1))), (labels['subject'], content.get('subject', '')), ] y = 1880 for label, value in fields: draw.text((100, y), label, fill=0, font=font_bold) draw.text((300, y), value, fill=0, font=font_normal) y -= 90 # Draw urgency if specified if 'urgency' in content: urgency = content['urgency'] urgency_label = labels['urgency'].get(urgency, urgency) # Draw urgency box box_width = 300 box_x = W - box_width - 100 draw.rectangle([(box_x, y - 30), (W - 100, y + 30)], outline=0, width=3) _, _, tw, th = draw.textbbox((0, 0), urgency_label, font=font_bold) draw.text((box_x + (box_width - tw) / 2, y - th / 2), urgency_label, fill=0, font=font_bold) y -= 100 # Draw line separator draw.line([(100, y), (W - 100, y)], fill=0, width=3) y -= 70 # Draw notes notes = content.get('notes', '') if notes: draw.text((100, y), labels['notes'], fill=0, font=font_bold) y -= 50 for line in notes.split('\n'): draw.text((100, y), line, fill=0, font=font_small) y -= 40 # Add watermark if 'watermark' in images_config: wm_config = images_config['watermark'] wm_img = load_image(wm_config['path']) if wm_img: pos = wm_config.get('position', {'x': 50, 'y': 50}) size = wm_config.get('size') opacity = wm_config.get('opacity', 0.1) # Convert percentage to pixels x = int(W * pos['x'] / 100) y = int(H * pos['y'] / 100) add_image_with_opacity(draw, wm_img, (x, y), size, opacity) # Save output output_path = content.get('output', '/tmp/cover.tif') img.save(output_path, dpi=(204, 196)) print(json.dumps({"ok": True, "output": output_path}))