feat: v4.1 - Czech diacritics in PDF (Liberation fonts), hour/min duration fields, day select in modal
Some checks failed
Build & Push Docker / build (push) Has been cancelled
Some checks failed
Build & Push Docker / build (push) Has been cancelled
This commit is contained in:
@@ -7,15 +7,62 @@ Always exactly one page, A4 landscape.
|
||||
from io import BytesIO
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
import os
|
||||
import logging
|
||||
|
||||
from reportlab.lib.pagesizes import A4, landscape
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.pdfgen import canvas as rl_canvas
|
||||
import logging
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
|
||||
from .validator import ScenarsError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ── Font registration ─────────────────────────────────────────────────────────
|
||||
# LiberationSans supports Czech diacritics. Fallback to Helvetica if not found.
|
||||
_FONT_REGULAR = 'Helvetica'
|
||||
_FONT_BOLD = 'Helvetica-Bold'
|
||||
_FONT_ITALIC = 'Helvetica-Oblique'
|
||||
|
||||
_LIBERATION_PATHS = [
|
||||
'/usr/share/fonts/truetype/liberation',
|
||||
'/usr/share/fonts/liberation',
|
||||
'/usr/share/fonts/truetype',
|
||||
]
|
||||
|
||||
|
||||
def _find_font(filename: str):
|
||||
for base in _LIBERATION_PATHS:
|
||||
path = os.path.join(base, filename)
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def _register_fonts():
|
||||
global _FONT_REGULAR, _FONT_BOLD, _FONT_ITALIC
|
||||
regular = _find_font('LiberationSans-Regular.ttf')
|
||||
bold = _find_font('LiberationSans-Bold.ttf')
|
||||
italic = _find_font('LiberationSans-Italic.ttf')
|
||||
if regular and bold and italic:
|
||||
try:
|
||||
pdfmetrics.registerFont(TTFont('LiberationSans', regular))
|
||||
pdfmetrics.registerFont(TTFont('LiberationSans-Bold', bold))
|
||||
pdfmetrics.registerFont(TTFont('LiberationSans-Italic', italic))
|
||||
_FONT_REGULAR = 'LiberationSans'
|
||||
_FONT_BOLD = 'LiberationSans-Bold'
|
||||
_FONT_ITALIC = 'LiberationSans-Italic'
|
||||
logger.info('PDF: Using LiberationSans (Czech diacritics supported)')
|
||||
except Exception as e:
|
||||
logger.warning(f'PDF: Font registration failed: {e}')
|
||||
else:
|
||||
logger.warning('PDF: LiberationSans not found, Czech diacritics may be broken')
|
||||
|
||||
|
||||
_register_fonts()
|
||||
|
||||
PAGE_W, PAGE_H = landscape(A4)
|
||||
MARGIN = 10 * mm
|
||||
|
||||
@@ -186,13 +233,13 @@ def generate_pdf(doc) -> bytes:
|
||||
|
||||
# ── Draw header ───────────────────────────────────────────────────
|
||||
y = y_top
|
||||
c.setFont('Helvetica-Bold', TITLE_SIZE)
|
||||
c.setFont(_FONT_BOLD, TITLE_SIZE)
|
||||
set_fill(c, HEADER_BG)
|
||||
c.drawString(x0, y - TITLE_SIZE, doc.event.title)
|
||||
y -= TITLE_SIZE + 5
|
||||
|
||||
if doc.event.subtitle:
|
||||
c.setFont('Helvetica', SUB_SIZE)
|
||||
c.setFont(_FONT_REGULAR, SUB_SIZE)
|
||||
set_fill(c, (0.4, 0.4, 0.4))
|
||||
c.drawString(x0, y - SUB_SIZE, doc.event.subtitle)
|
||||
y -= SUB_SIZE + 3
|
||||
@@ -208,7 +255,7 @@ def generate_pdf(doc) -> bytes:
|
||||
parts.append(f'Datum: {date_display}')
|
||||
if doc.event.location:
|
||||
parts.append(f'Místo: {doc.event.location}')
|
||||
c.setFont('Helvetica', INFO_SIZE)
|
||||
c.setFont(_FONT_REGULAR, INFO_SIZE)
|
||||
set_fill(c, (0.5, 0.5, 0.5))
|
||||
c.drawString(x0, y - INFO_SIZE, ' | '.join(parts))
|
||||
y -= INFO_SIZE + 3
|
||||
@@ -230,7 +277,7 @@ def generate_pdf(doc) -> bytes:
|
||||
c.line(tx, table_top, tx, table_top + TIME_AXIS_H)
|
||||
# label
|
||||
label = fmt_time(m)
|
||||
c.setFont('Helvetica', time_axis_font)
|
||||
c.setFont(_FONT_REGULAR, time_axis_font)
|
||||
set_fill(c, AXIS_TEXT)
|
||||
c.drawCentredString(tx, table_top + (TIME_AXIS_H - time_axis_font) / 2, label)
|
||||
|
||||
@@ -244,7 +291,7 @@ def generate_pdf(doc) -> bytes:
|
||||
for m in range(t_start, t_end + 1, 60):
|
||||
slot_idx = (m - t_start) // 15
|
||||
tx = x0 + DATE_COL_W + slot_idx * slot_w
|
||||
c.setFont('Helvetica', time_axis_font)
|
||||
c.setFont(_FONT_REGULAR, time_axis_font)
|
||||
set_fill(c, AXIS_TEXT)
|
||||
c.drawCentredString(tx, table_top + (TIME_AXIS_H - time_axis_font) / 2, fmt_time(m))
|
||||
|
||||
@@ -263,7 +310,7 @@ def generate_pdf(doc) -> bytes:
|
||||
# Date label cell
|
||||
fill_rect(c, x0, row_y, DATE_COL_W, row_h, AXIS_BG, BORDER, 0.4)
|
||||
draw_clipped_text(c, format_date_cs(date_key), x0, row_y, DATE_COL_W, row_h,
|
||||
'Helvetica-Bold', date_font, AXIS_TEXT, 'center')
|
||||
_FONT_BOLD, date_font, AXIS_TEXT, 'center')
|
||||
|
||||
# Vertical grid lines (15-min slots, hour lines darker)
|
||||
for slot_i in range(num_15min_slots + 1):
|
||||
@@ -315,15 +362,15 @@ def generate_pdf(doc) -> bytes:
|
||||
title_line = block.title + (' →' if overnight else '')
|
||||
if row_h > block_title_font * 2.2 and block.responsible:
|
||||
# Two lines: title + responsible
|
||||
c.setFont('Helvetica-Bold', block_title_font)
|
||||
c.setFont(_FONT_BOLD, block_title_font)
|
||||
c.drawCentredString(bx + bw / 2, row_y + row_h / 2 + 1, title_line)
|
||||
c.setFont('Helvetica', block_time_font)
|
||||
c.setFont(_FONT_REGULAR, block_time_font)
|
||||
set_fill(c, (text_rgb[0] * 0.8, text_rgb[1] * 0.8, text_rgb[2] * 0.8)
|
||||
if is_light(pt.color) else (0.85, 0.85, 0.85))
|
||||
c.drawCentredString(bx + bw / 2, row_y + row_h / 2 - block_title_font + 1,
|
||||
block.responsible)
|
||||
else:
|
||||
c.setFont('Helvetica-Bold', block_title_font)
|
||||
c.setFont(_FONT_BOLD, block_title_font)
|
||||
c.drawCentredString(bx + bw / 2, row_y + (row_h - block_title_font) / 2, title_line)
|
||||
|
||||
c.restoreState()
|
||||
@@ -331,7 +378,7 @@ def generate_pdf(doc) -> bytes:
|
||||
# ── Legend ────────────────────────────────────────────────────────
|
||||
legend_y_top = table_top - num_days * row_h - 6
|
||||
|
||||
c.setFont('Helvetica-Bold', 7)
|
||||
c.setFont(_FONT_BOLD, 7)
|
||||
set_fill(c, HEADER_BG)
|
||||
c.drawString(x0, legend_y_top, 'Legenda:')
|
||||
legend_y_top -= LEGEND_ITEM_H
|
||||
@@ -349,7 +396,7 @@ def generate_pdf(doc) -> bytes:
|
||||
fill_rgb, BORDER, 0.3)
|
||||
|
||||
# Type name NEXT TO the square
|
||||
c.setFont('Helvetica', 7)
|
||||
c.setFont(_FONT_REGULAR, 7)
|
||||
set_fill(c, (0.15, 0.15, 0.15))
|
||||
c.drawString(lx + LEGEND_BOX_W + 3,
|
||||
ly - LEGEND_ITEM_H + 2 + (LEGEND_ITEM_H - 2 - 7) / 2,
|
||||
@@ -357,7 +404,7 @@ def generate_pdf(doc) -> bytes:
|
||||
|
||||
# ── Footer ────────────────────────────────────────────────────────
|
||||
gen_date = datetime.now().strftime('%d.%m.%Y %H:%M')
|
||||
c.setFont('Helvetica-Oblique', 6.5)
|
||||
c.setFont(_FONT_ITALIC, 6.5)
|
||||
set_fill(c, FOOTER_TEXT)
|
||||
c.drawCentredString(PAGE_W / 2, MARGIN - 2, f'Vygenerováno Scenár Creatorem v4 | {gen_date}')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user