306 lines
9.5 KiB
Python
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
|