Files
blechreiz-website/scoremanager/pdf_views.py

306 lines
9.5 KiB
Python

"""
PDF generation views for the score manager application.
"""
import os
from django.http import HttpResponse
from reportlab.lib import pagesizes, units, utils
from reportlab.lib.colors import Color
from reportlab.platypus import (
BaseDocTemplate,
Flowable,
Frame,
NextPageTemplate,
PageBreak,
PageTemplate,
Table,
)
from .models import Piece
class RepertoireDocTemplate(BaseDocTemplate):
"""Document template for repertoire PDFs with header and two-column layout."""
def __init__(self, *args, **kwargs):
BaseDocTemplate.__init__(self, *args, **kwargs)
self.pagesize = kwargs["pagesize"]
left_border = 1 * units.cm
right_border = 1 * units.cm
top_border = 3 * units.cm
bottom_border = 1 * units.cm
frame_width = (self.pagesize[0] - right_border - left_border) / 2
frame_height = self.pagesize[1] - top_border - bottom_border
left_column = Frame(
left_border, bottom_border, frame_width, frame_height, showBoundary=0
)
right_column = Frame(
left_border + frame_width,
bottom_border,
frame_width,
frame_height,
showBoundary=0,
)
toc_template = PageTemplate(
id="TOC",
frames=[left_column, right_column],
onPage=RepertoireDocTemplate._draw_header,
)
full_page_template = PageTemplate(
id="FullPage",
frames=[Frame(0, 0, self.pagesize[0], self.pagesize[1])],
)
self.addPageTemplates([toc_template, full_page_template])
@staticmethod
def _draw_header(canvas, document):
"""Draw the header with logo and gradient background."""
current_path = os.path.dirname(os.path.realpath(__file__))
logo_image_file = current_path + "/static/pdfImg/logo.png"
bg_image_file = current_path + "/static/pdfImg/body_bg.jpg"
# Gradient
title_area_height = 2 * units.cm
gradient_colors = [Color(0.1, 0.1, 0.1), Color(0.2, 0.2, 0.2)]
ps = document.pagesize
gradient_start = (ps[0] / 2, ps[1])
gradient_end = (ps[0] / 2, ps[1] - title_area_height)
canvas.linearGradient(
gradient_start[0],
gradient_start[1],
gradient_end[0],
gradient_end[1],
gradient_colors,
extend=False,
)
title_area_mid_y = 0.5 * (gradient_start[1] + gradient_end[1])
# Draw Logo
if os.path.exists(logo_image_file):
logo_img = utils.ImageReader(logo_image_file)
logo_size = logo_img.getSize()
logo_fraction = 0.6
logo_size = (logo_fraction * logo_size[0], logo_fraction * logo_size[1])
logo_position = (
ps[0] - 5 * units.cm,
title_area_mid_y - 0.5 * logo_size[1],
)
canvas.drawImage(
logo_img,
logo_position[0],
logo_position[1],
width=logo_size[0],
height=logo_size[1],
mask=Color(0, 0, 0),
)
# Draw Title
text = canvas.beginText()
text.setTextOrigin(1 * units.cm, title_area_mid_y - 25 / 2)
text.setFillColorRGB(0.95, 0.95, 0.95)
text.setFont("Helvetica", 25)
text.textLine("Inhaltsverzeichnis")
canvas.drawText(text)
# Draw Background
if os.path.exists(bg_image_file):
bg_image = utils.ImageReader(bg_image_file)
bg_image_size = bg_image.getSize()
cur_pos = [0, gradient_end[1] - bg_image_size[1]]
while cur_pos[1] > -bg_image_size[1]:
cur_pos[0] = 0
while cur_pos[0] < ps[0]:
canvas.drawImage(bg_image, cur_pos[0], cur_pos[1])
cur_pos[0] += bg_image_size[0]
cur_pos[1] -= bg_image_size[1]
class PdfImage(Flowable):
"""
PdfImage wraps the first page from a PDF file as a Flowable
which can be included into a ReportLab Platypus document.
Based on the vectorpdf extension in rst2pdf (http://code.google.com/p/rst2pdf/)
"""
def __init__(
self, filename_or_object, page=0, width=None, height=None, kind="direct"
):
Flowable.__init__(self)
try:
from pdfrw import PdfReader
from pdfrw.buildxobj import pagexobj
except ImportError:
raise ImportError(
"pdfrw is required for PDF embedding. Install it with: pip install pdfrw"
)
# If using StringIO buffer, set pointer to beginning
if hasattr(filename_or_object, "read"):
filename_or_object.seek(0)
pdf_pages = PdfReader(filename_or_object, decompress=False).pages
if page >= len(pdf_pages):
page = 0
self.xobj = pagexobj(pdf_pages[page])
self.imageWidth = width
self.imageHeight = height
x1, y1, x2, y2 = self.xobj.BBox
self._w, self._h = x2 - x1, y2 - y1
if not self.imageWidth:
self.imageWidth = self._w
if not self.imageHeight:
self.imageHeight = self._h
self.__ratio = float(self.imageWidth) / self.imageHeight
if kind in ["direct", "absolute"] or width is None or height is None:
self.drawWidth = width or self.imageWidth
self.drawHeight = height or self.imageHeight
elif kind in ["bound", "proportional"]:
factor = min(float(width) / self._w, float(height) / self._h)
self.drawWidth = self._w * factor
self.drawHeight = self._h * factor
def wrap(self, aW, aH):
return self.drawWidth, self.drawHeight
def drawOn(self, canv, x, y, _sW=0):
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
try:
from pdfrw.toreportlab import makerl
except ImportError:
raise ImportError("pdfrw is required for PDF embedding.")
if _sW > 0 and hasattr(self, "hAlign"):
a = self.hAlign
if a in ("CENTER", "CENTRE", TA_CENTER):
x += 0.5 * _sW
elif a in ("RIGHT", TA_RIGHT):
x += _sW
elif a not in ("LEFT", TA_LEFT):
raise ValueError("Bad hAlign value " + str(a))
xobj = self.xobj
xobj_name = makerl(canv._doc, xobj)
xscale = self.drawWidth / self._w
yscale = self.drawHeight / self._h
x -= xobj.BBox[0] * xscale
y -= xobj.BBox[1] * yscale
canv.saveState()
canv.translate(x, y)
canv.scale(xscale, yscale)
canv.doForm(xobj_name)
canv.restoreState()
class Bookmark(Flowable):
"""Utility class to display PDF bookmark."""
def __init__(self, title, key):
self.title = title
self.key = key
Flowable.__init__(self)
def wrap(self, availWidth, availHeight):
"""Doesn't take up any space."""
return (0, 0)
def draw(self):
# set the bookmark outline to show when the file's opened
self.canv.showOutline()
# step 1: put a bookmark on the page
self.canv.bookmarkPage(self.key)
# step 2: put an entry in the bookmark outline
self.canv.addOutlineEntry(self.title, self.key, 0, 0)
def repertoire_pdf(request):
"""Generate a PDF containing the full repertoire with scores."""
response = HttpResponse(content_type="application/pdf")
response["Content-Disposition"] = 'attachment; filename="Repertoire.pdf"'
# Create the PDF object, using the response object as its "file"
doc = RepertoireDocTemplate(response, pagesize=pagesizes.A4)
elements = []
elements.append(Bookmark("Inhaltsverzeichnis", "Contents"))
# Table of Contents
data = []
for piece in Piece.get_repertoire():
data.append([f"{piece.repertoire_nr} {piece.title}"])
if data:
table = Table(data)
table.hAlign = "LEFT"
elements.append(table)
elements.append(NextPageTemplate("FullPage"))
elements.append(PageBreak())
pagesize = pagesizes.A4
for piece in Piece.get_repertoire():
score = piece.get_score_for_user(request.user)
if score is None:
continue
filename = score.file
bookmark_title = (
f"{piece.repertoire_nr:02d} {piece.title} - {score.score_type}"
)
elements.append(Bookmark(bookmark_title, str(piece.id)))
try:
# TODO: support multiple pages
image_flowable = PdfImage(
filename, 0, width=pagesize[0] * 0.98, height=pagesize[1] * 0.98
)
image_flowable.hAlign = "CENTER"
elements.append(image_flowable)
except Exception as e:
# Skip this score if PDF processing fails
print(f"Error processing PDF for {piece.title}: {e}")
continue
doc.build(elements)
return response
def repertoire_toc(request):
"""Generate a PDF containing only the table of contents."""
response = HttpResponse(content_type="application/pdf")
response["Content-Disposition"] = 'attachment; filename="Inhaltsverzeichnis.pdf"'
# Create the PDF object, using the response object as its "file"
doc = RepertoireDocTemplate(response, pagesize=pagesizes.A4)
elements = []
data = []
for piece in Piece.get_repertoire():
data.append([f"{piece.repertoire_nr} {piece.title}"])
if data:
table = Table(data)
table.hAlign = "LEFT"
elements.append(table)
doc.build(elements)
return response