Module soitool.codebook_to_pdf
Contains functionality for generating codebook as PDF.
Expand source code
"""Contains functionality for generating codebook as PDF."""
from datetime import datetime
from reportlab.pdfgen import canvas
from reportlab.platypus import (
Table,
Paragraph,
Spacer,
PageBreak,
TableStyle,
SimpleDocTemplate,
)
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.enumerates import CodebookSort
A4_WIDTH, A4_HEIGHT = A4
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",
fontSize=20,
alignment=1,
underlineWidth=1.5,
)
PAGE_NUMBER_FONT = "Helvetica"
TABLE_STYLE = TableStyle(
[
("FONTSIZE", (0, 0), (-1, 0), 16), # Header-fontsize
("BOTTOMPADDING", (0, 0), (-1, 0), 10), # Header-padding bottom
(
"BACKGROUND",
(0, 0),
(-1, 0),
HEADER_BG_COLOR,
), # Header background-color
("ALIGN", (0, 0), (-1, 0), "CENTER"), # Header-text centered
("GRID", (0, 0), (-1, -1), 1, colors.black), # Border around cells
]
)
def generate_codebook_pdf(
database, small=False, page_size=A4, orientation=portrait
):
"""Generate PDF with data from database-table 'CodeBook'.
Parameters
----------
database : soitool.database.Database
Reference to database-instance.
small : bool, optional
Data is from full codebook if False (default),
from small codebook if True.
page_size : reportlab.lib.pagesizes, optional
Size of each page, by default A4
orientation : reportlab.lib.pagesizes portrait or landscape
Paper orientation, by default portrait
"""
# Set title/headline
if small:
title_code = Paragraph(TITLE_SMALL_CODE, TITLE_STYLE)
title_decode = Paragraph(TITLE_SMALL_DECODE, TITLE_STYLE)
else:
title_code = Paragraph(TITLE_FULL_CODE, TITLE_STYLE)
title_decode = Paragraph(TITLE_FULL_DECODE, TITLE_STYLE)
# Get data from database
data_code, data_decode = get_codebook_data(database, small)
# Create code- and decodebook-tables with predefined style
table_code = Table(data_code, style=TABLE_STYLE, repeatRows=1)
table_decode = Table(data_decode, style=TABLE_STYLE, repeatRows=1)
elements = []
# Add title, vertical spacer and table for codebook
elements.append(title_code)
elements.append(Spacer(0, 1 * cm))
elements.append(table_code)
# Double pagebreak to add separating page between code- and decodebook
elements.append(PageBreak())
elements.append(PageBreak())
# Add title, vertical spacer and table for decodebook
elements.append(title_decode)
elements.append(Spacer(0, 1 * cm))
elements.append(table_decode)
# Generate filename
file_name = generate_filename(small)
# Create document, add elements and save as PDF
doc = CodeAndDecodebookDocTemplate(
file_name, page_size=orientation(page_size), topMargin=30
)
doc.build(elements, canvasmaker=CodeAndDecodebookCanvas)
def generate_filename(small=False):
"""Generate filename with current date for PDF.
Parameters
----------
small : bool
Filename will contain 'Kodebok' if False (default),
'Kodebok_liten' if True.
Returns
-------
String
Filename for PDF.
'Kodebok_YYYY_mm_dd.pdf' or 'Kodebok_liten_YYYY_mm_dd.pdf'
"""
# Get date on format YYYY_mm_dd
date = datetime.now().strftime("%Y_%m_%d")
if small:
return f"Kodebok_liten_{date}.pdf"
return f"Kodebok_{date}.pdf"
def get_codebook_data(database, small=False):
"""Read and format codebook-data from database sorted by Word and Code.
Parameters
----------
database : soitool.database.Database
Reference to database-instance.
small : bool
Retrieves full codebook if False (default), small codebook if True.
Returns
-------
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_data_code = database.get_codebook(small, sort=CodebookSort.WORD)
db_data_decode = database.get_codebook(small, sort=CodebookSort.CODE)
# Lists to append column-headers and formatted data
data_code = []
data_decode = []
# Add column-headers
if small:
data_code.append([HEADERS[0], HEADERS[3], HEADERS[1]])
data_decode.append([HEADERS[3], HEADERS[0], HEADERS[1]])
else:
data_code.append([HEADERS[0], HEADERS[3], HEADERS[1], HEADERS[2]])
data_decode.append([HEADERS[3], HEADERS[0], HEADERS[1], HEADERS[2]])
# Add row data
for row in db_data_code:
if small:
data_code.append([row["word"], row["code"], row["category"]])
else:
data_code.append(
[row["word"], row["code"], row["category"], row["type"]]
)
for row in db_data_decode:
if small:
data_decode.append([row["code"], row["word"], row["category"]])
else:
data_decode.append(
[row["code"], row["word"], row["category"], row["type"]]
)
return data_code, data_decode
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.
The first blank page (2 * PageBreak) added will be marked as a separating
page, and will not contain 'Side x av y'.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pagebreak_counter = 0
def afterFlowable(self, flowable):
"""Reset pagenumber and mark blank first page as separating page."""
# If flowable is a Paragraph,
# it is the title on the first page of code- or decodebook
if isinstance(flowable, Paragraph):
# Save startpage-number to get correct total, individual pagecount
self.canv.decodebook_startpage = self.canv.getPageNumber() - 1
# Reset page number
self.canv.reset_page_number()
self.canv.draw_page_count = True
if isinstance(flowable, PageBreak):
self.pagebreak_counter += 1
# Mark first blank page
if self.pagebreak_counter == 1:
self.mark_separating_page()
self.canv.draw_page_count = False
super().afterFlowable(flowable)
def mark_separating_page(self):
"""Mark the page separating code- and decodebook.
Put solid, black squares in each corner and centered text
stating that the page is for separation only.
"""
# Draw solid squares in each corner
self.canv.rect(
0, A4_HEIGHT - 2.8 * cm, 80, 80, fill=True,
)
self.canv.rect(
A4_WIDTH - 2.8 * cm, A4_HEIGHT - 2.8 * cm, 80, 80, fill=True,
)
self.canv.rect(
0, 0, 80, 80, fill=True,
)
self.canv.rect(
A4_WIDTH - 2.8 * cm, 0, 80, 80, fill=True,
)
# Draw centered text
text = "SKILLEARK"
text_width = self.canv.stringWidth(text)
self.canv.drawString(
A4_WIDTH / 2 - text_width / 2, A4_HEIGHT / 2, text
)
# pylint: disable=W0223
# Disabling pylint-warning 'Method is abstract in class but is not overridden'.
# 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 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 reset_page_number(self):
"""Reset page-number (to 1)."""
self._pageNumber = 1
def showPage(self):
"""On a page break, add page data."""
self.pages.append(dict(self.__dict__))
self._startPage()
def save(self):
"""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)
self.draw_page_number(page_count)
canvas.Canvas.showPage(self)
canvas.Canvas.save(self)
def draw_page_number(self, page_count):
"""Draw 'Side x av y' at bottom of pages."""
if self.draw_page_count:
page = f"Side {self._pageNumber} av {page_count}"
self.setFont(PAGE_NUMBER_FONT, 10)
self.drawString(A4_WIDTH / 2 - 20, 25, page)
Functions
def generate_codebook_pdf(database, small=False, page_size=(595.2755905511812, 841.8897637795277), orientation=<function portrait>)
-
Generate PDF with data from database-table 'CodeBook'.
Parameters
database
:Database
- Reference to database-instance.
small
:bool
, optional- Data is from full codebook if False (default), from small codebook if True.
page_size
:reportlab.lib.pagesizes
, optional- Size of each page, by default A4
orientation
:reportlab.lib.pagesizes
portrait
orlandscape
- Paper orientation, by default portrait
Expand source code
def generate_codebook_pdf( database, small=False, page_size=A4, orientation=portrait ): """Generate PDF with data from database-table 'CodeBook'. Parameters ---------- database : soitool.database.Database Reference to database-instance. small : bool, optional Data is from full codebook if False (default), from small codebook if True. page_size : reportlab.lib.pagesizes, optional Size of each page, by default A4 orientation : reportlab.lib.pagesizes portrait or landscape Paper orientation, by default portrait """ # Set title/headline if small: title_code = Paragraph(TITLE_SMALL_CODE, TITLE_STYLE) title_decode = Paragraph(TITLE_SMALL_DECODE, TITLE_STYLE) else: title_code = Paragraph(TITLE_FULL_CODE, TITLE_STYLE) title_decode = Paragraph(TITLE_FULL_DECODE, TITLE_STYLE) # Get data from database data_code, data_decode = get_codebook_data(database, small) # Create code- and decodebook-tables with predefined style table_code = Table(data_code, style=TABLE_STYLE, repeatRows=1) table_decode = Table(data_decode, style=TABLE_STYLE, repeatRows=1) elements = [] # Add title, vertical spacer and table for codebook elements.append(title_code) elements.append(Spacer(0, 1 * cm)) elements.append(table_code) # Double pagebreak to add separating page between code- and decodebook elements.append(PageBreak()) elements.append(PageBreak()) # Add title, vertical spacer and table for decodebook elements.append(title_decode) elements.append(Spacer(0, 1 * cm)) elements.append(table_decode) # Generate filename file_name = generate_filename(small) # Create document, add elements and save as PDF doc = CodeAndDecodebookDocTemplate( file_name, page_size=orientation(page_size), topMargin=30 ) doc.build(elements, canvasmaker=CodeAndDecodebookCanvas)
def generate_filename(small=False)
-
Generate filename with current date for PDF.
Parameters
small
:bool
- Filename will contain 'Kodebok' if False (default), 'Kodebok_liten' if True.
Returns
String
- Filename for PDF. 'Kodebok_YYYY_mm_dd.pdf' or 'Kodebok_liten_YYYY_mm_dd.pdf'
Expand source code
def generate_filename(small=False): """Generate filename with current date for PDF. Parameters ---------- small : bool Filename will contain 'Kodebok' if False (default), 'Kodebok_liten' if True. Returns ------- String Filename for PDF. 'Kodebok_YYYY_mm_dd.pdf' or 'Kodebok_liten_YYYY_mm_dd.pdf' """ # Get date on format YYYY_mm_dd date = datetime.now().strftime("%Y_%m_%d") if small: return f"Kodebok_liten_{date}.pdf" return f"Kodebok_{date}.pdf"
def get_codebook_data(database, small=False)
-
Read and format codebook-data from database sorted by Word and Code.
Parameters
database
:Database
- Reference to database-instance.
small
:bool
- Retrieves full codebook if False (default), small codebook if True.
Returns
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).
Expand source code
def get_codebook_data(database, small=False): """Read and format codebook-data from database sorted by Word and Code. Parameters ---------- database : soitool.database.Database Reference to database-instance. small : bool Retrieves full codebook if False (default), small codebook if True. Returns ------- 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_data_code = database.get_codebook(small, sort=CodebookSort.WORD) db_data_decode = database.get_codebook(small, sort=CodebookSort.CODE) # Lists to append column-headers and formatted data data_code = [] data_decode = [] # Add column-headers if small: data_code.append([HEADERS[0], HEADERS[3], HEADERS[1]]) data_decode.append([HEADERS[3], HEADERS[0], HEADERS[1]]) else: data_code.append([HEADERS[0], HEADERS[3], HEADERS[1], HEADERS[2]]) data_decode.append([HEADERS[3], HEADERS[0], HEADERS[1], HEADERS[2]]) # Add row data for row in db_data_code: if small: data_code.append([row["word"], row["code"], row["category"]]) else: data_code.append( [row["word"], row["code"], row["category"], row["type"]] ) for row in db_data_decode: if small: data_decode.append([row["code"], row["word"], row["category"]]) else: data_decode.append( [row["code"], row["word"], row["category"], row["type"]] ) return data_code, data_decode
Classes
class CodeAndDecodebookCanvas (*args, **kwargs)
-
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".
Create a canvas of a given size. etc.
You may pass a file-like object to filename as an alternative to a string. For more information about the encrypt parameter refer to the setEncrypt method.
Most of the attributes are private - we will use set/get methods as the preferred interface. Default page size is A4. cropMarks may be True/False or an object with parameters borderWidth, markColor, markWidth and markLength
if enforceColorSpace is in ('cmyk', 'rgb', 'sep','sep_black','sep_cmyk') then one of the standard _PDFColorSetter callables will be used to enforce appropriate color settings. If it is a callable then that will be used.
Expand source code
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 reset_page_number(self): """Reset page-number (to 1).""" self._pageNumber = 1 def showPage(self): """On a page break, add page data.""" self.pages.append(dict(self.__dict__)) self._startPage() def save(self): """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) self.draw_page_number(page_count) canvas.Canvas.showPage(self) canvas.Canvas.save(self) def draw_page_number(self, page_count): """Draw 'Side x av y' at bottom of pages.""" if self.draw_page_count: page = f"Side {self._pageNumber} av {page_count}" self.setFont(PAGE_NUMBER_FONT, 10) self.drawString(A4_WIDTH / 2 - 20, 25, page)
Ancestors
- reportlab.pdfgen.canvas.Canvas
- reportlab.pdfgen.textobject._PDFColorSetter
Methods
def draw_page_number(self, page_count)
-
Draw 'Side x av y' at bottom of pages.
Expand source code
def draw_page_number(self, page_count): """Draw 'Side x av y' at bottom of pages.""" if self.draw_page_count: page = f"Side {self._pageNumber} av {page_count}" self.setFont(PAGE_NUMBER_FONT, 10) self.drawString(A4_WIDTH / 2 - 20, 25, page)
def reset_page_number(self)
-
Reset page-number (to 1).
Expand source code
def reset_page_number(self): """Reset page-number (to 1).""" self._pageNumber = 1
def save(self)
-
Add the page number (page x of y) to page before saving.
Expand source code
def save(self): """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) self.draw_page_number(page_count) canvas.Canvas.showPage(self) canvas.Canvas.save(self)
def showPage(self)
-
On a page break, add page data.
Expand source code
def showPage(self): """On a page break, add page data.""" self.pages.append(dict(self.__dict__)) self._startPage()
class CodeAndDecodebookDocTemplate (*args, **kwargs)
-
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.
The first blank page (2 * PageBreak) added will be marked as a separating page, and will not contain 'Side x av y'.
create a document template bound to a filename (see class documentation for keyword arguments)
Expand source code
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. The first blank page (2 * PageBreak) added will be marked as a separating page, and will not contain 'Side x av y'. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.pagebreak_counter = 0 def afterFlowable(self, flowable): """Reset pagenumber and mark blank first page as separating page.""" # If flowable is a Paragraph, # it is the title on the first page of code- or decodebook if isinstance(flowable, Paragraph): # Save startpage-number to get correct total, individual pagecount self.canv.decodebook_startpage = self.canv.getPageNumber() - 1 # Reset page number self.canv.reset_page_number() self.canv.draw_page_count = True if isinstance(flowable, PageBreak): self.pagebreak_counter += 1 # Mark first blank page if self.pagebreak_counter == 1: self.mark_separating_page() self.canv.draw_page_count = False super().afterFlowable(flowable) def mark_separating_page(self): """Mark the page separating code- and decodebook. Put solid, black squares in each corner and centered text stating that the page is for separation only. """ # Draw solid squares in each corner self.canv.rect( 0, A4_HEIGHT - 2.8 * cm, 80, 80, fill=True, ) self.canv.rect( A4_WIDTH - 2.8 * cm, A4_HEIGHT - 2.8 * cm, 80, 80, fill=True, ) self.canv.rect( 0, 0, 80, 80, fill=True, ) self.canv.rect( A4_WIDTH - 2.8 * cm, 0, 80, 80, fill=True, ) # Draw centered text text = "SKILLEARK" text_width = self.canv.stringWidth(text) self.canv.drawString( A4_WIDTH / 2 - text_width / 2, A4_HEIGHT / 2, text )
Ancestors
- reportlab.platypus.doctemplate.SimpleDocTemplate
- reportlab.platypus.doctemplate.BaseDocTemplate
Methods
def afterFlowable(self, flowable)
-
Reset pagenumber and mark blank first page as separating page.
Expand source code
def afterFlowable(self, flowable): """Reset pagenumber and mark blank first page as separating page.""" # If flowable is a Paragraph, # it is the title on the first page of code- or decodebook if isinstance(flowable, Paragraph): # Save startpage-number to get correct total, individual pagecount self.canv.decodebook_startpage = self.canv.getPageNumber() - 1 # Reset page number self.canv.reset_page_number() self.canv.draw_page_count = True if isinstance(flowable, PageBreak): self.pagebreak_counter += 1 # Mark first blank page if self.pagebreak_counter == 1: self.mark_separating_page() self.canv.draw_page_count = False super().afterFlowable(flowable)
def mark_separating_page(self)
-
Mark the page separating code- and decodebook.
Put solid, black squares in each corner and centered text stating that the page is for separation only.
Expand source code
def mark_separating_page(self): """Mark the page separating code- and decodebook. Put solid, black squares in each corner and centered text stating that the page is for separation only. """ # Draw solid squares in each corner self.canv.rect( 0, A4_HEIGHT - 2.8 * cm, 80, 80, fill=True, ) self.canv.rect( A4_WIDTH - 2.8 * cm, A4_HEIGHT - 2.8 * cm, 80, 80, fill=True, ) self.canv.rect( 0, 0, 80, 80, fill=True, ) self.canv.rect( A4_WIDTH - 2.8 * cm, 0, 80, 80, fill=True, ) # Draw centered text text = "SKILLEARK" text_width = self.canv.stringWidth(text) self.canv.drawString( A4_WIDTH / 2 - text_width / 2, A4_HEIGHT / 2, text )