diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a4d411a --- /dev/null +++ b/.gitignore @@ -0,0 +1,170 @@ +# Test Input/Output +*.in +*.out + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +*.pdf +.vscode/ +.devcontainer/ \ No newline at end of file diff --git a/build_my_notebook.py b/build_my_notebook.py deleted file mode 100644 index 743c197..0000000 --- a/build_my_notebook.py +++ /dev/null @@ -1,94 +0,0 @@ -from notebook_builder import Doc,Page,LinearLinks -from datetime import date,timedelta -import sys - -sunday=date.fromisoformat('2021-08-01') -thisweek=f"Week {sunday.strftime('%U, %Y')}" - -lastSunday=sunday+timedelta(days=-7) -lastweek=f"Week {lastSunday.strftime('%U, %Y')}" - - - -doc=Doc("templates/pagetemplate.pdf") - -# links between top level weekly pages -weeklyLinks=LinearLinks(left=10,top=110) - -# links down to daily goal pages from weekly pages -dailyGoalsLinks=LinearLinks(right=-10,top=110) - -# Build top level weekly pages, with templates, and give them their outbound links -# Note the daily goal pages do not exist yet, but that's ok! -weeklyRetro=doc.addPage(title=f"Retro for {lastweek}",basepdfname="templates/weeklyRetroTemplate.pdf",toclevel=1) -weeklyRetro.addLinks(weeklyLinks,dailyGoalsLinks) - -weeklyPlanner=doc.addPage(title=f"Planner for {thisweek}",basepdfname="templates/weeklyPlannerTemplate.pdf",toclevel=1) -weeklyPlanner.addLinks(weeklyLinks,dailyGoalsLinks) - -weeklyDump1=doc.addPage(title=f"Dump 1 for {thisweek}",toclevel=1) -weeklyDump1.addLinks(weeklyLinks,dailyGoalsLinks) - -weeklyDump2=doc.addPage(title=f"Dump 2 for {thisweek}",toclevel=1) -weeklyDump2.addLinks(weeklyLinks,dailyGoalsLinks) - -weeklyGoals=doc.addPage(title=f"Goals for {thisweek}",basepdfname="templates/weeklyGoalsTemplate.pdf",toclevel=1) -weeklyGoals.addLinks(weeklyLinks,dailyGoalsLinks) - - -# Link top level pages to each other -weeklyLinks.addLink(weeklyRetro,"R"); -weeklyLinks.addLink(weeklyPlanner,"P"); -weeklyLinks.addLink(weeklyDump1,"D1"); -weeklyLinks.addLink(weeklyDump2,"D2"); -weeklyLinks.addLink(weeklyGoals,"G"); - -# Build one goals page and 9 notes pages for each day of the week -days=[] -for x in range(1,6): - tempdate=sunday+timedelta(days=x) - days.append(tempdate) - - -for daydate in days: - day=daydate.strftime('%a %-d %b %Y') - # Each set of daily notes has links to each other - dailyNotesLinks=LinearLinks(bottom=-500,right=-5,flowdirection="down") - - # First page is a goals page. - # Each of the daily pages links back up to the weekly pages, to the other days in the week, and to each other on this day - dailyGoals=doc.addPage(title=f"{day}: Daily Goals",basepdfname="templates/dailyGoalsTemplate.pdf",toclevel=1) - dailyGoals.addLinks(weeklyLinks,dailyGoalsLinks,dailyNotesLinks) - - # This gets linked "down to" from the weekly pages above - dailyGoalsLinks.addLink(dailyGoals,f"{day[0]}") - - # It is also linked by each other page on this day - dailyNotesLinks.addLink(dailyGoals,"G") - - - #Add index page - dailyNote=doc.addPage(title=f"{day}: Notes Index") - dailyNote.addLinks(weeklyLinks,dailyGoalsLinks,dailyNotesLinks) - - dailyNotesLinks.addLink(dailyNote,f"I") - - for pageno in range(2,10): - # These are the individual note pages for a given day - dailyNote=doc.addPage(title=f"{day}: Notes {pageno}") - dailyNote.addLinks(weeklyLinks,dailyGoalsLinks,dailyNotesLinks) - dailyNotesLinks.addLink(dailyNote,f"{pageno}") - - -peopleLinks=LinearLinks(left=10,top=110,width=300) -projectLinks=LinearLinks(left=310,top=110,width=300) - - - -doc.render(sys.argv[1]) - - - - - - diff --git a/diary_generator/__main__.py b/diary_generator/__main__.py new file mode 100644 index 0000000..59820ca --- /dev/null +++ b/diary_generator/__main__.py @@ -0,0 +1,11 @@ +import sys + +from diary_generator.example_diary import create_example_diary + + +def main(): + create_example_diary(sys.argv[1]) + + +if __name__ == "__main__": + main() diff --git a/diary_generator/classes/doc.py b/diary_generator/classes/doc.py new file mode 100644 index 0000000..7cb47f6 --- /dev/null +++ b/diary_generator/classes/doc.py @@ -0,0 +1,84 @@ +from typing import Optional + +import fitz + +from diary_generator.classes.page import Page +from diary_generator.classes.table_of_content_entry import TableOfContentEntry + + +class Doc: + # Represents the output document + # This class exists primarily to collect the invidual pages, in the correct order. + # As the intra pdf linking scheme relies on page number, all pages must be known + # before links are created. + # + # Requires the path to a tempate pdf file when created: + # the first page of this file will be used as the default + # template for each page created, if the page itself does + # not have a dedicated template. + pages: list[Page] + fitz_doc: fitz.Document + toc: list[TableOfContentEntry] + + def __init__(self, base_pdf_name: str): + self.pages = [] + self.fitz_doc = fitz.open() + self.base_pdf_name = base_pdf_name + self.toc = [] + + def add_page( + self, + base_pdf_name: Optional[str] = None, + title="", + title_x=20, + title_y=250, + title_size=50, + toc_level=0, + ): + base_pdf = self._get_base_pdf_for_page(base_pdf_name) + page = Page( + base_pdf, + title=title, + title_x=title_x, + title_y=title_y, + title_size=title_size, + toc_level=toc_level, + ) + self.addPages(page) + return page + + def _get_base_pdf_for_page(self, base_pdf_name: Optional[str]) -> fitz.Document: + if base_pdf_name is None: + return fitz.open(self.base_pdf_name) + else: + return fitz.open(base_pdf_name) + + # Add one or more pages into the document. + # This method will create fitz pages for each doc, however rendering of content + # is done as a separate pass + def addPages(self, *pages: Page): + for page in pages: + self.pages.append(page) + page.page_number = len(self.pages) - 1 + # copy tempate into new doc + self.fitz_doc.insert_pdf( + page.base_pdf, + from_page=0, + to_page=0, + start_at=-1, + rotate=-1, + links=True, + annots=True, + show_progress=0, + final=1, + ) + if page.toc_level != 0: + self.toc.append((page.toc_level, page.title, page.page_number)) + + # Ask each page to render their own conent- this is done once all pages are added + # After rendering the document is saved. + def render(self, output_file_name: str): + for page in self.pages: + page.render(self.fitz_doc) + self.fitz_doc.set_toc(self.toc, collapse=1) # type: ignore + self.fitz_doc.save(output_file_name) diff --git a/diary_generator/classes/linear_links.py b/diary_generator/classes/linear_links.py new file mode 100644 index 0000000..b6b693b --- /dev/null +++ b/diary_generator/classes/linear_links.py @@ -0,0 +1,153 @@ +from typing import Literal + +import fitz + +from diary_generator.classes.links import Links +from diary_generator.classes.page import Page + + +class LinearLinks(Links): + # Renders a set of links left to right, or top to bottom, + # as a set of boxes, containing centered label text. + # + # Expects one of left / right to be passed in constructor + # Expects one of top/bottom to be passed in constructor + # + # Starting from the point defined by the two elements above, + # boxes will be laid out either from the left (if "left" is passed) + # or from the right (if "right" is passed). + # + # If right is passed, it can either be expressed as an positive number, + # in which case it is considered as an absolute x coordinate, or as + # a negative number, in which case it is considered as an offset from + # the right edge of the document. + # + # Similarly if bottom is passed and is positive, it is considered as + # an absolute y cooridinate, but if negative it is considered as + # an offset from the page bottom. + # + # The size of each box can be modified by the width and height args- + # Note that is the label text is too large it will just not rendered + # + # Use flowdirection="right" to choose left to right, + # and flowdirection="down" to choose top to bottom + # The fontsize is also controllable. + + horizontal_anchor: Literal["left", "right"] + horizontal_anchor_offset: int + vertical_achor: Literal["top", "bottom"] + vertical_achor_offset: int + width: int + height: int + flow_direction: Literal["right", "down"] + font_size: int + + pages: list[Page] + labels: dict[Page, str] + + def __init__( + self, + horizontal_anchor: Literal["left", "right"], + horizontal_anchor_offset: int, + vertical_achor: Literal["top", "bottom"], + vertical_achor_offset: int, + width=80, + height=80, + flow_direction: Literal["right", "down"] = "right", + font_size=30, + ): + + self.horizontal_anchor = horizontal_anchor + self.horizontal_anchor_offset = horizontal_anchor_offset + self.vertical_achor = vertical_achor + self.vertical_achor_offset = vertical_achor_offset + + self.flow_direction = flow_direction + + self.width = width + self.height = height + self.font_size = font_size + + self.labels = dict() + self.pages = list() + + def add_link(self, page: Page, label: str): + self.labels[page] = label + self.pages.append(page) + + # Render this set of link boxes onto the passed page + # This method will automatically display a box as inverse color + # if the link points back to itself. + def render(self, page: fitz.Page, page_number: int): + if self.flow_direction == "right": + if self.horizontal_anchor == "left": + l = self.horizontal_anchor_offset + else: + l = ( + page.rect.x1 + + self.horizontal_anchor_offset + - len(self.pages) * self.width + ) + + r = l + self.width + + if self.vertical_achor == "top": + t = self.vertical_achor_offset + else: + t = page.rect.y1 + self.vertical_achor_offset - self.height + + b = t + self.height + else: # flow down from top + if self.horizontal_anchor == "left": + l = self.horizontal_anchor_offset + else: + l = page.rect.x1 + self.horizontal_anchor_offset - self.width + + r = l + self.width + + if self.vertical_achor == "top": + t = self.vertical_achor_offset + else: + t = ( + page.rect.y1 + + self.horizontal_anchor_offset + - self.height * len(self.pages) + ) + + b = t + self.height + + boxcol = fitz.utils.getColor("black") + + for target in self.pages: + + if target.page_number == page_number: + textcol = fitz.utils.getColor("white") + backcol = fitz.utils.getColor("black") + else: + textcol = fitz.utils.getColor("black") + backcol = fitz.utils.getColor("white") + r1 = fitz.Rect(l, t, r, b) + textrect = fitz.Rect( + l, t + (self.height / 2) - (self.font_size / 2 * 1.33), r, b + ) + page.draw_rect(r1, color=boxcol, fill=backcol, overlay=True) # type: ignore + + if self.flow_direction == "right": + r = r + self.width + l = l + self.width + else: + t = t + self.height + b = b + self.height + + linkdict = {"kind": 1, "from": r1, "page": target.page_number} + page.insert_link(linkdict) # type: ignore + + # this line should use link text + page.insert_textbox( # type: ignore + textrect, + f"{self.labels[target]}", + color=textcol, + overlay=True, + align=1, + fontsize=self.font_size, + ) diff --git a/diary_generator/classes/links.py b/diary_generator/classes/links.py new file mode 100644 index 0000000..4208513 --- /dev/null +++ b/diary_generator/classes/links.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod + +import fitz + + +class Links(ABC): + + @abstractmethod + def render(self, page: fitz.Page, page_number: int): + pass diff --git a/diary_generator/classes/page.py b/diary_generator/classes/page.py new file mode 100644 index 0000000..f89a1ed --- /dev/null +++ b/diary_generator/classes/page.py @@ -0,0 +1,51 @@ +import fitz + +from diary_generator.classes.links import Links + + +class Page: + title: str + title_x: int + title_y: int + title_size: int + toc_level: int + link_sets: list[Links] + base_pdf: fitz.Document + + def __init__( + self, + base_pdf: fitz.Document, + title="", + title_x=20, + title_y=250, + title_size=50, + toc_level=0, + ): + self.base_pdf = base_pdf + self.title = title + self.link_sets = [] + self.page_number = 0 + self.title_col = fitz.utils.getColor("black") + self.title_size = title_size + self.title_x = title_x + self.title_y = title_y + self.toc_level = toc_level + + def add_links(self, *linksets: Links): + for linkset in linksets: + self.link_sets.append(linkset) + return self + + def render(self, fitzdoc: fitz.Document): + fitzpage = fitzdoc[self.page_number] + # render outbound links + for linkset in self.link_sets: + linkset.render(fitzpage, self.page_number) + # render title + fitzpage.insert_text( # type: ignore + (self.title_x, self.title_y), + self.title, + color=self.title_col, + overlay=True, + fontsize=self.title_size, + ) diff --git a/diary_generator/classes/table_of_content_entry.py b/diary_generator/classes/table_of_content_entry.py new file mode 100644 index 0000000..5bdc1fb --- /dev/null +++ b/diary_generator/classes/table_of_content_entry.py @@ -0,0 +1 @@ +TableOfContentEntry = tuple[int, str, int] diff --git a/diary_generator/example_diary.py b/diary_generator/example_diary.py new file mode 100644 index 0000000..e3adea2 --- /dev/null +++ b/diary_generator/example_diary.py @@ -0,0 +1,112 @@ +from datetime import date, timedelta + +from diary_generator.classes.doc import Doc +from diary_generator.classes.linear_links import LinearLinks + +sunday = date.fromisoformat("2021-08-01") +thisweek = f"Week {sunday.strftime('%U, %Y')}" + +lastSunday = sunday + timedelta(days=-7) +lastweek = f"Week {lastSunday.strftime('%U, %Y')}" + + +def create_example_diary(output_file_name: str): + doc = Doc("templates/pagetemplate.pdf") + + # links between top level weekly pages + weekly_links = LinearLinks( + horizontal_anchor="left", + horizontal_anchor_offset=10, + vertical_achor="top", + vertical_achor_offset=110, + ) + + # links down to daily goal pages from weekly pages + daily_goals_links = LinearLinks( + horizontal_anchor="right", + horizontal_anchor_offset=-10, + vertical_achor="top", + vertical_achor_offset=110, + ) + + # Build top level weekly pages, with templates, and give them their outbound links + # Note the daily goal pages do not exist yet, but that's ok! + weekly_retro = doc.add_page( + title=f"Retro for {lastweek}", + base_pdf_name="templates/weeklyRetroTemplate.pdf", + toc_level=1, + ) + weekly_retro.add_links(weekly_links, daily_goals_links) + + weekly_planner = doc.add_page( + title=f"Planner for {thisweek}", + base_pdf_name="templates/weeklyPlannerTemplate.pdf", + toc_level=1, + ) + weekly_planner.add_links(weekly_links, daily_goals_links) + + weekly_dump_1 = doc.add_page(title=f"Dump 1 for {thisweek}", toc_level=1) + weekly_dump_1.add_links(weekly_links, daily_goals_links) + + weekly_dump_2 = doc.add_page(title=f"Dump 2 for {thisweek}", toc_level=1) + weekly_dump_2.add_links(weekly_links, daily_goals_links) + + weekly_goals = doc.add_page( + title=f"Goals for {thisweek}", + base_pdf_name="templates/weeklyGoalsTemplate.pdf", + toc_level=1, + ) + weekly_goals.add_links(weekly_links, daily_goals_links) + + # Link top level pages to each other + weekly_links.add_link(weekly_retro, "R") + weekly_links.add_link(weekly_planner, "P") + weekly_links.add_link(weekly_dump_1, "D1") + weekly_links.add_link(weekly_dump_2, "D2") + weekly_links.add_link(weekly_goals, "G") + + # Build one goals page and 9 notes pages for each day of the week + days = [] + for x in range(1, 6): + day_of_the_week_date = sunday + timedelta(days=x) + days.append(day_of_the_week_date) + + for day_date in days: + day = day_date.strftime("%a %-d %b %Y") + # Each set of daily notes has links to each other + daily_notes_links = LinearLinks( + horizontal_anchor="right", + horizontal_anchor_offset=-5, + vertical_achor="bottom", + vertical_achor_offset=-500, + flow_direction="down", + ) + + # First page is a goals page. + # Each of the daily pages links back up to the weekly pages, to the other days in the week, and to each other on this day + daily_goals = doc.add_page( + title=f"{day}: Daily Goals", + base_pdf_name="templates/dailyGoalsTemplate.pdf", + toc_level=1, + ) + daily_goals.add_links(weekly_links, daily_goals_links, daily_notes_links) + + # This gets linked "down to" from the weekly pages above + daily_goals_links.add_link(daily_goals, f"{day[0]}") + + # It is also linked by each other page on this day + daily_notes_links.add_link(daily_goals, "G") + + # Add index page + daily_note = doc.add_page(title=f"{day}: Notes Index") + daily_note.add_links(weekly_links, daily_goals_links, daily_notes_links) + + daily_notes_links.add_link(daily_note, f"I") + + for pageno in range(2, 10): + # These are the individual note pages for a given day + daily_note = doc.add_page(title=f"{day}: Notes {pageno}") + daily_note.add_links(weekly_links, daily_goals_links, daily_notes_links) + daily_notes_links.add_link(daily_note, f"{pageno}") + + doc.render(output_file_name) diff --git a/notebook_builder.py b/notebook_builder.py deleted file mode 100644 index 7abf3c9..0000000 --- a/notebook_builder.py +++ /dev/null @@ -1,233 +0,0 @@ -import fitz -from datetime import date,timedelta - -class Page: - def __init__(self, title="",titlex=20,titley=250,titlesize=50,basepdfname="",toclevel=0,links=[]): - self.title = title - self.linksets=[] - self.pageno=0 - self.fitzpage="" - self.titlecol=fitz.utils.getColor("black") - self.titlesize=titlesize - self.titlex=titlex - self.titley=titley - self.basepdfname=basepdfname - self.toclevel=toclevel - - - def addLinks(self,*linksets): - for linkset in linksets: - self.linksets.append(linkset) - return self - - - def render(self,fitzdoc): - self.fitzpage=fitzdoc[self.pageno] - #render outbound links - for linkset in self.linksets: - linkset.render(self) - #render title - self.fitzpage.insert_text((self.titlex,self.titley), self.title,color=self.titlecol, overlay=True,fontsize=self.titlesize) - - - - - -class Links: - #An abstract class to represent a set of outbound links. - # - #Primary responsibilities are: - #1. hold the details of a set of target pages, with text labels for each link - #2. render these links when asked on one or more source pages - # - #This set of links are generally logically grouped- eg - #you might have a set of links to pages for each day of the week. - #This set of links can be rendered on more than one page. - def __init__(self): - self.pages=[]; - self.labels={} - - - def addLink(self,page,label): - self.labels[page]=label - self.pages.append(page); - - -class LinearLinks(Links): - # Renders a set of links left to right, or top to bottom, - # as a set of boxes, containing centered label text. - # - # Expects one of left / right to be passed in constructor - # Expects one of top/bottom to be passed in constructor - # - # Starting from the point defined by the two elements above, - # boxes will be laid out either from the left (if "left" is passed) - # or from the right (if "right" is passed). - # - # If right is passed, it can either be expressed as an positive number, - # in which case it is considered as an absolute x coordinate, or as - # a negative number, in which case it is considered as an offset from - # the right edge of the document. - # - # Similarly if bottom is passed and is positive, it is considered as - # an absolute y cooridinate, but if negative it is considered as - # an offset from the page bottom. - # - # The size of each box can be modified by the width and height args- - # Note that is the label text is too large it will just not rendered - # - # Use flowdirection="right" to choose left to right, - # and flowdirection="down" to choose top to bottom - # The fontsize is also controllable. - def __init__(self,width=80,height=80,left="",top="",right="",bottom="",flowdirection="right",fontsize=30): - super().__init__() - - self.left=left - self.top=top - self.right=right - self.bottom=bottom - self.flowdirection=flowdirection - - if (left=="" and right==""): - raise Exception("You must set either left or right starting points") - if (left!="" and right!=""): - raise Exception("You cannot set both left and right starting points. Choose one") - if (top=="" and bottom==""): - raise Exception("You must set either top or bottom starting points") - if (top!="" and bottom!=""): - raise Exception("You cannot set both top and bottom starting points. Choose one") - - self.width=width - self.height=height - self.fontsize=fontsize - - - # Render this set of link boxes onto the passed page - # This method will automatically display a box as inverse color - # if the link points back to itself. - def render(self,page): - if self.flowdirection=="right": - - if (self.left!=""): - l=self.left - else: - if (self.right <= 0): #right is expressed as negative offset from right edge - #take the right edge, substract the passed offset, then start - l=page.fitzpage.rect.x1+self.right -len(self.pages)*self.width - - r=l+self.width - - if (self.top!=""): - t=self.top - else: - if (self.bottom <= 0): #bottom is expressed as negative offset from bottom edge - #self.bottom=page.fitzpage.rect.y1+self.bottom - t=page.fitzpage.rect.y1+self.bottom -self.height - b=t+self.height - else: #flow down from top - if (self.left!=""): - l=self.left - else: - if (self.right <= 0): #right is expressed as negative offset from right edge - #take the right edge, substract the passed offset, then start - l=page.fitzpage.rect.x1+self.right -self.width - - r=l+self.width - - if (self.top!=""): - t=self.top - else: - if (self.bottom <= 0): #bottom is expressed as negative offset from bottom edge - #self.bottom=page.fitzpage.rect.y1+self.bottom - t=page.fitzpage.rect.y1+self.bottom -self.height*len(self.pages) - b=t+self.height - - - - - - boxcol=fitz.utils.getColor("black") - - for target in self.pages : - - if (target.pageno==page.pageno): - textcol=fitz.utils.getColor("white") - backcol=fitz.utils.getColor("black") - else: - textcol=fitz.utils.getColor("black") - backcol=fitz.utils.getColor("white") - r1 = fitz.Rect(l, t, r, b) - textrect = fitz.Rect(l, t+(self.height/2)-(self.fontsize/2*1.33), r, b) - page.fitzpage.draw_rect(r1,color=boxcol, fill=backcol, overlay=True) - - if (self.flowdirection=="right"): - r=r+self.width - l=l+self.width - else: - t=t+self.height - b=b+self.height - - - - linkdict = { - "kind": 1, - "from": r1, - "page": target.pageno - } - page.fitzpage.insert_link(linkdict) - - # this line should use link text - page.fitzpage.insert_textbox(textrect, f"{self.labels[target]}",color=textcol, overlay=True,align=1,fontsize=self.fontsize) - - - -class Doc: - # Represents the output document - # This class exists primarily to collect the invidual pages, in the correct order. - # As the intra pdf linking scheme relies on page number, all pages must be known - # before links are created. - # - # Requires the path to a tempate pdf file when created: - # the first page of this file will be used as the default - # template for each page created, if the page itself does - # not have a dedicated template. - - def __init__(self,basepdfname): - self.pages=[] - self.fitzdoc = fitz.open() - self.basepdfname=basepdfname - self.toc=[] - - def addPage( self, title="",titlex=20,titley=250,titlesize=50,basepdfname="",toclevel=0,links=[]): - page=Page(title=title,titlex=titlex,titley=titley,titlesize=titlesize,basepdfname=basepdfname,toclevel=toclevel,links=links) - self.addPages(page) - return page - - # Add one or more pages into the document. - # This method will create fitz pages for each doc, however rendering of content - # is done as a separate pass - def addPages(self,*pages): - for page in pages: - self.pages.append(page) - page.pageno=len(self.pages)-1 - if (page.basepdfname==""): - page.basepdf=fitz.open(self.basepdfname) - else: - page.basepdf=fitz.open(page.basepdfname) - #copy tempate into new doc - self.fitzdoc.insert_pdf(page.basepdf, from_page=0, to_page=0,start_at=-1, rotate=-1, links=True, annots=True, show_progress=0, final=1) - if page.toclevel!=0: - self.toc.append([page.toclevel,page.title,page.pageno]) - - - - - # Ask each page to render their own conent- this is done once all pages are added - # After rendering the document is saved. - def render(self,outputfilename): - for page in self.pages: - page.render(self.fitzdoc); - self.fitzdoc.set_toc(self.toc, collapse=1) - self.fitzdoc.save(outputfilename) - - diff --git a/readme.md b/readme.md index 2ad5de6..c7b8d13 100644 --- a/readme.md +++ b/readme.md @@ -1,88 +1,86 @@ -# A Hyperlinked PDF Notebook Generator for EInk Tablets - -## Introduction and Functionality -This is a simple python library that allows you to generate custom hyperlinked pdf notebook for use on eink tablets such as the Supernote A5X, Remarkable 1/2, Boox Note Air, etc. You will need some level of python to be able to use, but you should be able to get away with hacking the example. - -**For a sample of the kinds of documents this allows you to generate, see the bottom of this page, or look at the sample [here](https://github.com/jacrify/diaryGenerator/raw/main/assets/sample.pdf).** - -Currently it supports the following functionality: -- Define a background PDF page used for all pages. You can make these on the Supernote itself if you wish, including with handwriting, and/or edit using a pdf editor (I used PDFPen) -- Override this PDF for specific pages if you wish -- Print a title on each page, if you wish (or embed it in the template PDF) -- Define a set of links to appear on one of more pages, linking to other pages -- Link labels are configurable -- Link sets are currently linear, and can be vertical or horizontal -- Link sets can be positioned relative to any edge of the page (top bottom left right) -- A linkset will indicate if the current page is selected by using inverse colors -- Pages can be added to PDF table of content -- Font size and size of link boxes is configurable -- For the Supernote, setting the output directory to Dropbox allow you to automatically push the resulting file to your Supernote. Your Supernote handwriting will not be impacted and will appear on top of the template, as long as you don't change the order of the pages. (This is awesome) - -## Installing -If you want to use this code, you will need to do the following: - -- Install Python3 for your platform -- Clone the git repository -- You'll need the pymupdf library to do the heavy lifting (details (here)[https://pymupdf.readthedocs.io/en/latest/]. To install this run the shell command: -`pip3 install fitz` -- Then cd to the directory where the code is, and run - `python3 build_my_notebook.py "testout.pdf"` - This should generate the default testout.pdf file. - - Now you can edit "build_my_notebook.py" and make it do what you want. This contains the code for stiching together the notebook pages. - - The file notebook_builder.py, on the other hand, contains the core classes and documentation on how to use them. - - - ## Key Concepts - Doc represents that output document. - - When created, pass the default template name. - - Page represents one page in the output doc. - - Add pages one by one by calling doc.addPage. Order matters: the doc will be built from start to finish in the order the pages are added - - When adding a page you can set title, specific template name, and toclevel. - - Set toclevel to 1 to show top level of TOC, 2 to show next level, etc. Leave out to not show page in TOC - -Links represents a set of links to be rendered on one or more pages, linking those pages to other pages. -- A single linkset can be dropped onto multiple pages to save time -- The only (current) implementation of links is LinearLinks, which draw links as a line of boxes with text inside them -- LinearLinks can be vertical (created with `flowdirecton=down`) or horizontal (created with `flowdirection=right`) -- Links can be offset from the left, right, top, and bottom edges. One offset for horizontal and vertical axes must be specified so the code knows where to draw. -- Right and bottom offsets are expressed as negative numbers - `right=-10` draws boxes finishing 10px from right edge of page. `bottom=-10` draws them finishing 10px from bottom -- `left=10` and `top=10` draw boxes 10 px from left and top edges, respectively -- Boxes have a sensible default width, which can be overridden with `width=80` -- Fontsize also has a sensible default, which can be overridden with `fontsize=20` -- Target pages can be added to the Links object, along with the label for the outbound link, using `addLink(dailyGoals,"G")` -- When rendering a set of links, if the current page is in the link set the link box will be reverse colour. This is useful for navigation. - -Links and Pages can be put together in any order. Ie you can create a page, create a Links object, add it to the page, then add some pages to the Links object. - -When you have finished creating all your pages and links, call `doc.render(outputFilename)` to create the output doc. - - -## Why does this exist? -I recently purchased a Supernote A5x. This is one of a (relatively) new class of eink notebook devices, like a kindle but with a stylus pen for writing notes on. I love it- there are kinds of work that are best done on paper, with pen in hand, but actual paper has never really worked for me. I love the simplicity of erasing, of being able to lasso text and move it. I also love a couple of special features of the A5X: the ability to draw a star anywhere in any doc and then search for all stars, the ability to build dynamic tables of contents, and the ability to take take handwritten notes on book highlights. - -However once I'd been using the device for a week I knew there were more things I wanted to make it do. Specifically I wanted some basic template notes to guide me through my weeks and days, and I wanted to be able to easily navigate between various portions of my notebooks. Supernote allows you to follow local links within PDF documents, so I looked into how difficult it would be to generate a my own hyperlinked pdf planner. - -The general structure I wanted was as follows: -- At the start of each week I do a simple retro of the previous week. 3 things that went well, 3 things to improve. I want a page for that with a simple template. ![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223640.png) -- Then I look at my calendar and pull out significant events on each day that I need to prepare for. I want a page for that also, with a section for each weekday to take notes in. ![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223702.png) -- I then do a brain dump of everything that is going on in my world. This is pretty freeform but normally takes a couple of pages. ![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223724.png) -- Swapping back and forth between the events and the dump pages, I then try and sketch out 3 significant outcomes I want to achieve in the week. I want a template page for this. ![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223751.png) - -Then I get on with my week. For each day of the week: -- I start setting goals for the day with a single templated page. I may refer back to the dumps and weekly goals when doing this.![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223820.png) -- As my day progresses I write notes on my meetings and tasks, in a fairly linear fashion. So I need around 9 pages of space each day just for these, with an index page ![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223908.png) -![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223934.png) - -The key things I wanted were: -- I wanted to be able to jump around within the notes very easily, especially between weekly and daily goals, and the various weekly planning pages. -- If I decided the change the system, I wanted to be able to do this with a minimum of effort. Ideally I wanted to be able to make minor changes to the templates mid week without breaking the whole notebook. Changes from week to week I wasn't so worried about, as I will generate a new doc each week. -- I wanted room for future growth. There are a few pdf planners kicking around with hyperlinked monthly and yearly planners. If I decide not to generate a doc each week, I wanted to be able to add functionality as required. - - -The full doc I generate is available [here](https://github.com/jacrify/diaryGenerator/raw/main/assets/sample.pdf). - - -Caveats: I'm not a seasoned Pythonista and the code is not especially good idiomatic pythhon. There are no automated tests. Your computer might catch fire. The earth will one day be torched to a cinder as it slowly spirals down into the heart of the sun, followed eons later by the terrible gloom of the stars themselves winking out one by one, and if that happens because you run this code it's on you dude not me. +# A Hyperlinked PDF Notebook Generator for EInk Tablets + +## Introduction and Functionality +This is a simple python library that allows you to generate custom hyperlinked pdf notebook for use on eink tablets such as the Supernote A5X, Remarkable 1/2, Boox Note Air, etc. You will need some level of python to be able to use, but you should be able to get away with hacking the example. + +**For a sample of the kinds of documents this allows you to generate, see the bottom of this page, or look at the sample [here](https://github.com/jacrify/diaryGenerator/raw/main/assets/sample.pdf).** + +Currently it supports the following functionality: +- Define a background PDF page used for all pages. You can make these on the Supernote itself if you wish, including with handwriting, and/or edit using a pdf editor (I used PDFPen) +- Override this PDF for specific pages if you wish +- Print a title on each page, if you wish (or embed it in the template PDF) +- Define a set of links to appear on one of more pages, linking to other pages +- Link labels are configurable +- Link sets are currently linear, and can be vertical or horizontal +- Link sets can be positioned relative to any edge of the page (top bottom left right) +- A linkset will indicate if the current page is selected by using inverse colors +- Pages can be added to PDF table of content +- Font size and size of link boxes is configurable +- For the Supernote, setting the output directory to Dropbox allow you to automatically push the resulting file to your Supernote. Your Supernote handwriting will not be impacted and will appear on top of the template, as long as you don't change the order of the pages. (This is awesome) + +## Installing +If you want to use this code, you will need to do the following: + +- Install Python3 for your platform +- Clone the git repository +- To run this project you'll need to install the dependencies defined in `requirements.txt`, depending on your system `pip install -r requirements.txt` will do this. +- Then cd to the directory where the code is, and run + `python3 -m diary_generator "testout.pdf"` + This should generate the default testout.pdf file. + + Now you can edit "__main__.py" and make it do what you want. This contains the code for stiching together the notebook pages. + + + + ## Key Concepts + Doc represents that output document. + - When created, pass the default template name. + + Page represents one page in the output doc. + - Add pages one by one by calling doc.addPage. Order matters: the doc will be built from start to finish in the order the pages are added + - When adding a page you can set title, specific template name, and toclevel. + - Set toclevel to 1 to show top level of TOC, 2 to show next level, etc. Leave out to not show page in TOC + +Links represents a set of links to be rendered on one or more pages, linking those pages to other pages. +- A single linkset can be dropped onto multiple pages to save time +- The only (current) implementation of links is LinearLinks, which draw links as a line of boxes with text inside them +- LinearLinks can be vertical (created with `flowdirecton=down`) or horizontal (created with `flowdirection=right`) +- Links can be offset from the left, right, top, and bottom edges. One offset for horizontal and vertical axes must be specified so the code knows where to draw. +- Right and bottom offsets are expressed as negative numbers - `right=-10` draws boxes finishing 10px from right edge of page. `bottom=-10` draws them finishing 10px from bottom +- `left=10` and `top=10` draw boxes 10 px from left and top edges, respectively +- Boxes have a sensible default width, which can be overridden with `width=80` +- Fontsize also has a sensible default, which can be overridden with `fontsize=20` +- Target pages can be added to the Links object, along with the label for the outbound link, using `addLink(dailyGoals,"G")` +- When rendering a set of links, if the current page is in the link set the link box will be reverse colour. This is useful for navigation. + +Links and Pages can be put together in any order. Ie you can create a page, create a Links object, add it to the page, then add some pages to the Links object. + +When you have finished creating all your pages and links, call `doc.render(outputFilename)` to create the output doc. + + +## Why does this exist? +I recently purchased a Supernote A5x. This is one of a (relatively) new class of eink notebook devices, like a kindle but with a stylus pen for writing notes on. I love it- there are kinds of work that are best done on paper, with pen in hand, but actual paper has never really worked for me. I love the simplicity of erasing, of being able to lasso text and move it. I also love a couple of special features of the A5X: the ability to draw a star anywhere in any doc and then search for all stars, the ability to build dynamic tables of contents, and the ability to take take handwritten notes on book highlights. + +However once I'd been using the device for a week I knew there were more things I wanted to make it do. Specifically I wanted some basic template notes to guide me through my weeks and days, and I wanted to be able to easily navigate between various portions of my notebooks. Supernote allows you to follow local links within PDF documents, so I looked into how difficult it would be to generate a my own hyperlinked pdf planner. + +The general structure I wanted was as follows: +- At the start of each week I do a simple retro of the previous week. 3 things that went well, 3 things to improve. I want a page for that with a simple template. ![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223640.png) +- Then I look at my calendar and pull out significant events on each day that I need to prepare for. I want a page for that also, with a section for each weekday to take notes in. ![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223702.png) +- I then do a brain dump of everything that is going on in my world. This is pretty freeform but normally takes a couple of pages. ![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223724.png) +- Swapping back and forth between the events and the dump pages, I then try and sketch out 3 significant outcomes I want to achieve in the week. I want a template page for this. ![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223751.png) + +Then I get on with my week. For each day of the week: +- I start setting goals for the day with a single templated page. I may refer back to the dumps and weekly goals when doing this.![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223820.png) +- As my day progresses I write notes on my meetings and tasks, in a fairly linear fashion. So I need around 9 pages of space each day just for these, with an index page ![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223908.png) +![](https://github.com/jacrify/diaryGenerator/raw/main/assets/20210804223934.png) + +The key things I wanted were: +- I wanted to be able to jump around within the notes very easily, especially between weekly and daily goals, and the various weekly planning pages. +- If I decided the change the system, I wanted to be able to do this with a minimum of effort. Ideally I wanted to be able to make minor changes to the templates mid week without breaking the whole notebook. Changes from week to week I wasn't so worried about, as I will generate a new doc each week. +- I wanted room for future growth. There are a few pdf planners kicking around with hyperlinked monthly and yearly planners. If I decide not to generate a doc each week, I wanted to be able to add functionality as required. + + +The full doc I generate is available [here](https://github.com/jacrify/diaryGenerator/raw/main/assets/sample.pdf). + + +Caveats: I'm not a seasoned Pythonista and the code is not especially good idiomatic pythhon. There are no automated tests. Your computer might catch fire. The earth will one day be torched to a cinder as it slowly spirals down into the heart of the sun, followed eons later by the terrible gloom of the stars themselves winking out one by one, and if that happens because you run this code it's on you dude not me. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7284a0d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +fitz +pymupdf \ No newline at end of file