""" 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