diff --git a/soitool/codebook_to_pdf.py b/soitool/codebook_to_pdf.py index 4001e9037bd090723ad31a26c811a5192431faaf..ea3aa2222cb2446875b18bafe835877eb49c7479 100644 --- a/soitool/codebook_to_pdf.py +++ b/soitool/codebook_to_pdf.py @@ -1,50 +1,61 @@ """Contains functionality for generating codebook as PDF.""" from datetime import datetime -from enum import Enum from reportlab.pdfgen import canvas -from reportlab.platypus import Table, Paragraph, TableStyle, \ - SimpleDocTemplate, Spacer +from reportlab.platypus import ( + Table, + Paragraph, + TableStyle, + SimpleDocTemplate, + Spacer, + PageBreak, +) from reportlab.lib.styles import ParagraphStyle from reportlab.lib import colors from reportlab.lib.pagesizes import A4, portrait from reportlab.lib.units import cm from soitool.database import Database +from soitool.enumerates import CodebookSize, CodebookSort A4_WIDTH = A4[0] -TITLE_FULL = '<u>Kodebok</u>' -TITLE_SMALL = "<u>Liten Kodebok</u>" + +TITLE_FULL_CODE = "<u>Kodebok</u>" +TITLE_FULL_DECODE = "<u>Dekodebok</u>" +TITLE_SMALL_CODE = "<u>Liten Kodebok</u>" +TITLE_SMALL_DECODE = "<u>Liten Dekodebok</u>" + HEADERS = ["Ord/Uttrykk", "Kategori", "Type", "Kode"] HEADER_BG_COLOR = colors.HexColor("#a6a6a6") TITLE_STYLE = ParagraphStyle( - name='Title', - fontName='Helvetica', + name="Title", + fontName="Helvetica", fontSize=20, alignment=1, - underlineWidth=1.5 + underlineWidth=1.5, ) PAGE_NUMBER_FONT = "Helvetica" -TABLE_STYLE = TableStyle([ - ('FONTSIZE', (0, 0), (3, 0), 16), # Header-font - ('BOTTOMPADDING', (0, 0), (3, 0), 10), # Header-padding bottom - ("BACKGROUND", (0, 0), (3, 0), HEADER_BG_COLOR), # Header background-color - ("ALIGN", (0, 0), (3, 0), "CENTER"), # Header-text centered - ('GRID', (0, 0), (-1, -1), 1, colors.black), # Border around cells -]) - - -class CodebookSize(Enum): - """Enumerate with possible codebook-sizes.""" - - FULL = 0 - SMALL = 1 +TABLE_STYLE = TableStyle( + [ + ("FONTSIZE", (0, 0), (3, 0), 16), # Header-fontsize + ("BOTTOMPADDING", (0, 0), (3, 0), 10), # Header-padding bottom + ( + "BACKGROUND", + (0, 0), + (3, 0), + HEADER_BG_COLOR, + ), # Header background-color + ("ALIGN", (0, 0), (3, 0), "CENTER"), # Header-text centered + ("GRID", (0, 0), (-1, -1), 1, colors.black), # Border around cells + ] +) -def generate_codebook_pdf(codebook_size=CodebookSize.FULL, - page_size=A4, orientation=portrait): +def generate_codebook_pdf( + codebook_size=CodebookSize.FULL, page_size=A4, orientation=portrait +): """Generate PDF with data from database-table 'CodeBook'. Parameters @@ -59,30 +70,42 @@ def generate_codebook_pdf(codebook_size=CodebookSize.FULL, """ # Set title/headline if codebook_size == CodebookSize.FULL: - title = Paragraph(TITLE_FULL, TITLE_STYLE) + title_code = Paragraph(TITLE_FULL_CODE, TITLE_STYLE) + title_decode = Paragraph(TITLE_FULL_DECODE, TITLE_STYLE) else: - title = Paragraph(TITLE_SMALL, TITLE_STYLE) + title_code = Paragraph(TITLE_SMALL_CODE, TITLE_STYLE) + title_decode = Paragraph(TITLE_SMALL_DECODE, TITLE_STYLE) # Get data from database data = get_codebook_data(codebook_size) - # Create Table with data and predefined style - table = Table(data, repeatRows=1) - table.setStyle(TABLE_STYLE) + # Create Tables with data sorted by Word/Code and set predefined style + table_code = Table(data[0], repeatRows=1) + table_decode = Table(data[1], repeatRows=1) + table_code.setStyle(TABLE_STYLE) + table_decode.setStyle(TABLE_STYLE) - # Add title, vertical spacer and table to element-list + # Add title, vertical spacer and table for codebook elements = [] - elements.append(title) + elements.append(title_code) elements.append(Spacer(1, 1 * cm)) - elements.append(table) + elements.append(table_code) + elements.append(PageBreak()) + # Add blank page to separate code- and decodebook + elements.append(PageBreak()) + # Add title, vertical spacer and table for decodebook + elements.append(title_decode) + elements.append(Spacer(1, 1 * cm)) + elements.append(table_decode) # Generate filename file_name = generate_filename(codebook_size) # Create document, add elements and save as PDF - doc = SimpleDocTemplate(file_name, pagesize=orientation(page_size), - topMargin=30) - doc.build(elements, canvasmaker=PageNumCanvas) + doc = CodeAndDecodebookDocTemplate( + file_name, page_size=orientation(page_size), topMargin=30 + ) + doc.build(elements, canvasmaker=CodeAndDecodebookCanvas) def generate_filename(codebook_size): @@ -120,26 +143,65 @@ def get_codebook_data(codebook_size=CodebookSize.FULL): Returns ------- - 2D List of strings - Data from codebook. + Tuple of two 2D-lists with data from db-table 'CodeBook'. + The first list is a codebook (sorted by Word), + the second list is a decodebook (sorted by Code). """ + # Get data from CodeBook-table db = Database() + db_data_code = db.get_codebook(codebook_size, sort=CodebookSort.WORD) + db_data_decode = db.get_codebook(codebook_size, sort=CodebookSort.CODE) - # Get data from CodeBook-table - if codebook_size == CodebookSize.FULL: - db_data = db.get_codebook(small=False) - else: - db_data = db.get_codebook(small=True) + # Lists to append column-headers and formatted data + data_code = [] + data_decode = [] - data = [] # Add column-headers - data.append([HEADERS[0], HEADERS[1], HEADERS[2], HEADERS[3]]) + data_code.append([HEADERS[0], HEADERS[1], HEADERS[2], HEADERS[3]]) + data_decode.append([HEADERS[0], HEADERS[1], HEADERS[2], HEADERS[3]]) # Add row data - for row in db_data: - data.append([row["word"], row["category"], row["type"], row["code"]]) + for row in db_data_code: + data_code.append( + [row["word"], row["category"], row["type"], row["code"]] + ) + for row in db_data_decode: + data_decode.append( + [row["word"], row["category"], row["type"], row["code"]] + ) + + return data_code, data_decode - return data + +class CodeAndDecodebookDocTemplate(SimpleDocTemplate): + """DocTemplate for adding individual 'Side x av y' to code- and decodebook. + + SimpleDocTemplate (super) method 'build' needs to be used with + CodeAndDecodebookCanvas as canvasmaker. + + If code- and decodebook use 10 pages each, the total page count will be 20, + but this class will draw 'Side 1 av 10' through 'Side 10 av 10' for both. + + 'Side x av y' is not drawn on blank pages. + """ + + def afterFlowable(self, flowable): + """Reset pagenumber and don't write pagecount on blank page.""" + # If flowable is a Paragraph, + # it is the title on the first page of decodebook. + if isinstance(flowable, Paragraph): + # Save startpage-number to get correct total, individual pagecount. + # pylint: disable=W0212 + self.canv.decodebook_startpage = self.canv._pageNumber - 1 + + # Reset page number + self.canv._pageNumber = 1 # pylint: disable=W0212 + self.canv.draw_page_count = True + + if isinstance(flowable, PageBreak): + self.canv.draw_page_count = False + + super().afterFlowable(flowable) # pylint: disable=W0223 @@ -147,16 +209,34 @@ def get_codebook_data(codebook_size=CodebookSize.FULL): # The methods are 'inkAnnotation' and 'inkAnnotation0', and they are supposed # to add PDF-annotations. PDF-annotations enable PDF-editing such as forms, # text highlighting etc, and are not needed in the generated codebook-PDF. -class PageNumCanvas(canvas.Canvas): - """Subclassed Canvas for adding 'page x of y' at bottom of all pages. +class CodeAndDecodebookCanvas(canvas.Canvas): + """Canvas for adding individual 'Side x av y' to codebook and decodebook. + + This class will add 'Side x av y', where y is total PDF page count, + unless attribute decodebook_startpage is set and _pageNumber reset. + + This class is meant to be used with CodeAndDecodeBookDocTemplate, + which sets the attributes mentioned above. Modified version of: http://www.blog.pythonlibrary.org/2013/08/12/reportlab-how-to-add-page-numbers/ + + Attributes + ---------- + draw_page_count : bool + Draws page count on pages while True, which is the default value. + The bool can be updated from outside this class. + decodebook_startpage : int + Page number where decodebook starts, value is set outside this class. + Is used to reset page count so the first page + of decodebook shows "Side 1 av y". """ def __init__(self, *args, **kwargs): canvas.Canvas.__init__(self, *args, **kwargs) self.pages = [] + self.draw_page_count = True + self.decodebook_startpage = 0 def showPage(self): """On a page break, add page data.""" @@ -164,8 +244,8 @@ class PageNumCanvas(canvas.Canvas): self._startPage() def save(self): - """Add the page number to pages (page x of y) before saving.""" - page_count = len(self.pages) + """Add the page number (page x of y) to page before saving.""" + page_count = len(self.pages) - self.decodebook_startpage for page in self.pages: self.__dict__.update(page) @@ -175,7 +255,8 @@ class PageNumCanvas(canvas.Canvas): canvas.Canvas.save(self) def draw_page_number(self, page_count): - """Add the page number.""" - page = "Side %s av %s" % (self._pageNumber, page_count) - self.setFont(PAGE_NUMBER_FONT, 10) - self.drawString(A4_WIDTH / 2 - 20, 25, page) + """Draw 'Side x av y' at bottom of pages.""" + if self.draw_page_count: + page = "Side %s av %s" % (self._pageNumber, page_count) + self.setFont(PAGE_NUMBER_FONT, 10) + self.drawString(A4_WIDTH / 2 - 20, 25, page)