diff --git a/.gitignore b/.gitignore index d8ebaa0..ae5d1d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ # .gitignore +# Python stuff +*.pyc + # Ignore log files etc makeBook.log bakeBook.log diff --git a/utilities/bakeBook.py b/utilities/bakeBook.py index 87cd3e6..99608d7 100644 --- a/utilities/bakeBook.py +++ b/utilities/bakeBook.py @@ -10,7 +10,7 @@ # Version: 2024-01-18 - skip code blocks when fixing citations # Version: 2024-01-29 - use dedicated pdf directory # Version: 2024-04-02 - copy image files to pdf directory -# Version: 2024-04-04 - change image citations to suit pandoc +# # Version: 2024-04-04 - change image citations to suit pandoc # Version: 2024-04-07 - deprecate SVG to suit pandoc # - added pagebreak hack # Version: 2024-04-08 - cosmetic fix @@ -63,96 +63,14 @@ # ######################################################## -from tkinter import Tk -from tkinter.filedialog import askdirectory -from tkinter.messagebox import askokcancel, askyesno, showinfo - -import time import os -import sys -import subprocess -import shutil - - -def show(msg): - """Show a message""" - global T, cmd_line - if cmd_line: - print(msg) - else: - showinfo(title=T, message=msg) - - -def logit(msg): - """Add a message to the log file""" - global flog, printing - flog.write(msg + "\n") - if printing: - print(msg) - - -def logitw(msg): - """Add a warning message to the log file""" - global warnings - logit("WARNING: " + msg) - warnings += 1 - - -def dprint(*msg): - """Diagnostic print""" - global printing - if printing: - print(*msg) - - -def crash(msg): - """Log and crash""" - global printing - printing = True - logit("CRASH " + msg) - flog.close() - exit() - - -def rf(f): - """Return a file as a list of lower case strings""" - file = open(f, "r", encoding="utf-8", errors="replace") - l = file.readlines() - file.close() - return l - - -def wf(f, l): - """Write list of strings to file""" - global written - file = open(f, "w", encoding="utf-8") - for line in l: - file.write(line) - file.close() - logit("'" + f + "' written") - - -def cmd(command): - """Execute system command""" - do_cmd = subprocess.Popen( - command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - _out, _err = do_cmd.communicate() - if _err: - logitw(_err.decode("utf-8").strip()) - - -## if _out: -## _out = _out.decode('utf-8').strip() -## print(_out) +import time -def uncase(l): - """Return lower case version of a list of strings""" - u = [] - for s in l: - u.append(s.lower()) - return u +# ---------------------------------------------------------------------- +# ----------------Utility functions for baking the book----------------- +# ---------------------------------------------------------------------- +page_break = "\nbackslashpagebreak\n" def fix_section(raw, epub=False): @@ -255,181 +173,138 @@ def fix_section(raw, epub=False): return new -def imgcopy(dirname): - """Duplicate any image files in the pdf directory""" - for f in os.listdir(dirname): - ftype = os.path.splitext(f)[1] - if ftype == ".svg": - logitw("SVG image found, probable pandoc failure") - if ftype in [".svg", ".jpg", ".jpeg", ".png", ".gif"]: - shutil.copy(dirname + "/" + f, "pdf/" + f) - - -page_break = "\nbackslashpagebreak\n" - -######### Startup - -# Define some globals - -printing = False # True for extra diagnostic prints -warnings = 0 - -cmd_line = False -if len(sys.argv) > 1: - # user provided directory name? - if os.path.isdir(sys.argv[1]): - # assume user has provided directory - # and set all options to defaults - os.chdir(sys.argv[1]) - cmd_line = True - -utility_dir = os.path.dirname(os.path.realpath(__file__)) - -# Announce -if not cmd_line: - Tk().withdraw() # we don't want a full GUI - - T = "Book baker." - - printing = askyesno(title=T, message="Diagnostic printing?") - - os.chdir(askdirectory(title="Select main book directory")) - -# Open log file - -flog = open("bakeBook.log", "w", encoding="utf-8") -timestamp = time.strftime("%Y-%m-%d %H:%M:%S UTC%z", time.localtime()) -logit("bakeBook run at " + timestamp) - -logit("Running in directory " + os.getcwd()) - - -show("Will read current book6 text.\nTouch no files until done!") - - -######### Create empty output - -baked = [] -epub_backed = [] - -######### Title page - -title = rf("Title.md") -title.append("\nVersion captured at " + timestamp + "\n") -epub_backed += title -title.append(page_break) -baked += title -del title - -######### Contents - -# Read raw contents - -contents = rf("Contents.md") - -# Make list of section files to be baked, copy image files - -preamble = True -fns = [] -for line in contents: - if line.startswith("[1. Introduction]"): - preamble = False - if preamble: - continue # ignore preamble - if not line.strip(): - continue # ignore blank - if line.startswith("["): - # directory name - _, tail = line.split("](") - dirname, filename = tail.split("/") - dirname = dirname.replace("%20", " ") - imgcopy(dirname) # copy any image files - filename = filename.replace("%20", " ").replace(")", "").replace("\n", "") - elif line.startswith("* ["): - # strip link - filename, _ = line[3:].split("]", maxsplit=1) - filename += ".md" - elif line.startswith("*"): - # old format (plain name) - filename = line.replace("* ", "").replace("%20", " ").replace("\n", "") + ".md" - fns.append(dirname + "/" + filename) - -# Pre-bake contents - -contents = fix_section(contents) - -baked += contents - -######### Main text - -for fn in fns: - baked += fix_section(rf(fn)) - epub_backed += fix_section(rf(fn), True) - - -######### Indexes - -baked += fix_section(rf("Index.md")) -baked += fix_section(rf("Citex.md")) -epub_backed += fix_section(rf("Index.md"), True) -epub_backed += fix_section(rf("Citex.md"), True) - -######### Write the baked file - -wf("pdf/baked.md", baked) -wf("pdf/baked_epub.md", epub_backed) - -######### Attempt LaTeX and PDF conversion -logit("Attempting LaTeX conversion") -try: - - # Call pandoc to make LaTeX file - cmd( - "pandoc pdf/baked.md -f gfm+implicit_figures -t latex -s -o pdf/baked.tex -V colorlinks=true" - ) - - # Fix up LaTeX - latex = rf("pdf/baked.tex") - for i in range(len(latex)): - if "backslashpagebreak" in latex[i]: - latex[i] = "\\pagebreak\n" - wf("pdf/baked.tex", latex) - - # Convert LaTeX to PDF - logit("Attempting PDF conversion (slow)") - # Must switch to PDF directory - os.chdir("pdf") - cmd("pdflatex baked.tex") - # 2nd run to fix citations - cmd("pdflatex baked.tex") - logit("Exiting PDF conversion - check baked.pdf") - -except Exception as e: - logitw("PDF conversion failure: " + str(e)) - logitw("Manual PDF conversion needed.") - -######### Attempt EPUB conversion -logit("Attempting EPUB conversion (slow)") -try: - metadata_file = os.path.join(utility_dir, "epubMetadata.yaml") - css_file = os.path.join(utility_dir, "epub.css") - font_file = os.path.join(utility_dir, "NotoSansMono-Regular.ttf") - cmd( - f"pandoc baked_epub.md -f gfm --toc=true -t epub3 -o baked.epub -V colorlinks=true --epub-title-page=false --metadata-file={metadata_file} --css {css_file} --epub-embed-font={font_file}" - ) - logit("Exiting EPUB conversion - check baked.epub") - -except Exception as e: - logitw("EPUB conversion failure: " + str(e)) - logitw("Manual EPUB conversion needed.") - -######### Close log and exit - -flog.close() - -if warnings: - warn = str(warnings) + " warning(s)\n" -else: - warn = "" - -show(warn + "Check bakeBook.log.") +# ---------------------------------------------------------------------- +# ---------------------------------------------------------------------- +# ---------------------------------------------------------------------- + + +# ---------------------------------------------------------------------- +# ------------------------------Main baking job------------------------- +# ---------------------------------------------------------------------- + + +def bake_book_process(book_dir, log, utils): + """Main logic to bake the book.""" + + # Initialization + utility_dir = os.path.dirname(os.path.realpath(__file__)) + pdf_dir = os.path.join(book_dir, "pdf") + baked = [] + epub_backed = [] + timestamp = time.strftime("%Y-%m-%d %H:%M:%S UTC%z", time.localtime()) + + ## Title page + title = utils.read_file("Title.md") + title.append("\nVersion captured at " + timestamp + "\n") + epub_backed += title + title.append(page_break) + baked += title + + ######### Contents + + # Read raw contents + + contents = utils.read_file("Contents.md") + + # Make list of section files to be baked, copy image files + + preamble = True + sections = [] + for line in contents: + if line.startswith("[1. Introduction]"): + preamble = False + if preamble: + continue # ignore preamble + if not line.strip(): + continue # ignore blank + if line.startswith("["): + # directory name + _, tail = line.split("](") + dirname, filename = tail.split("/") + dirname = dirname.replace("%20", " ") + utils.imgcopy(dirname) # copy any image files + filename = filename.replace("%20", " ").replace(")", "").replace("\n", "") + elif line.startswith("* ["): + # strip link + filename, _ = line[3:].split("]", maxsplit=1) + filename += ".md" + elif line.startswith("*"): + # old format (plain name) + filename = line.replace("* ", "").replace("%20", " ").replace("\n", "") + ".md" + sections.append(dirname + "/" + filename) + + # Pre-bake contents + + contents = fix_section(contents) + + baked += contents + + ######### Main text + + for section in sections: + section_content = utils.read_file(section) + baked += fix_section(section_content) + epub_backed += fix_section(section_content, True) + + + ######### Indexes + index_content = utils.read_file("Index.md") + citex_content = utils.read_file("Citex.md") + baked += fix_section(index_content) + baked += fix_section(citex_content) + epub_backed += fix_section(index_content, True) + epub_backed += fix_section(citex_content, True) + + ######### Write the baked file + + baked_path = utils.write_file("pdf/baked.md", baked) + epub_baked_path = utils.write_file("pdf/baked_epub.md", epub_backed) + + ######### Attempt LaTeX and PDF conversion + log.logit("Attempting LaTeX conversion") + try: + # Call pandoc to make LaTeX file + baked_tex_path = os.path.join(pdf_dir, "baked.tex") + utils.cmd( + f"pandoc {baked_path} -f gfm+implicit_figures -t latex -s -o {baked_tex_path} -V colorlinks=true", + pdf_dir + ) + + # Fix up LaTeX + latex = utils.read_file("pdf/baked.tex") + for i in range(len(latex)): + if "backslashpagebreak" in latex[i]: + latex[i] = "\\pagebreak\n" + utils.write_file("pdf/baked.tex", latex) + + # Convert LaTeX to PDF + log.logit("Attempting PDF conversion (slow)") + # Must switch to PDF directory + utils.set_new_base_dir = os.path.join(book_dir, "/pdf") + utils.cmd(f"pdflatex {baked_tex_path}", pdf_dir) + # 2nd run to fix citations + utils.cmd(f"pdflatex {baked_tex_path}", pdf_dir) + log.logit("Exiting PDF conversion - check baked.pdf") + + except Exception as e: + log.logitw("PDF conversion failure: " + str(e)) + log.logitw("Manual PDF conversion needed.") + + ######### Attempt EPUB conversion + log.logit("Attempting EPUB conversion (slow)") + try: + metadata_file = os.path.join(utility_dir, "epubMetadata.yaml") + css_file = os.path.join(utility_dir, "epub.css") + font_file = os.path.join(utility_dir, "NotoSansMono-Regular.ttf") + epub_export_path = os.path.join(pdf_dir, "baked_epub.md") + utils.cmd( + f"pandoc {epub_baked_path} -f gfm --toc=true -t epub3" + f" -o {epub_export_path} -V colorlinks=true --epub-title-page=false" + f" --metadata-file={metadata_file} --css {css_file} --epub-embed-font={font_file}", + pdf_dir + ) + log.logit("Exiting EPUB conversion - check baked.epub") + + except Exception as e: + log.logitw("EPUB conversion failure: " + str(e)) + log.logitw("Manual EPUB conversion needed.") diff --git a/utilities/gui_requirements.txt b/utilities/gui_requirements.txt new file mode 100644 index 0000000..6a6ac56 --- /dev/null +++ b/utilities/gui_requirements.txt @@ -0,0 +1 @@ +Tk diff --git a/utilities/run.py b/utilities/run.py new file mode 100644 index 0000000..7c111b8 --- /dev/null +++ b/utilities/run.py @@ -0,0 +1,46 @@ +import os +import time +import sys + +import utils +from bakeBook import bake_book_process + + +def pandoc_checks(log_instance): + _out, _err = utils.execute_command("pandoc --version") + if _err: + # Pandoc not installed, we won't be able to do much... + log_instance.crash("pandoc is not installed!") + + +def bake_book(book_dir, debug=True, gui_instance=None): + script_location = os.path.dirname(os.path.realpath(__file__)) + log_path = os.path.join(script_location, "bakeBook.log") + + log_instance = utils.Logging(log_path, + printing=debug, + gui_instance=gui_instance) + pandoc_checks(log_instance) + + utility_instance = utils.Utilities(log_instance, book_dir) + + timestamp = time.strftime("%Y-%m-%d %H:%M:%S UTC%z", time.localtime()) + log_instance.logit("bakeBook run at " + timestamp) + log_instance.logit("Book directory " + book_dir) + + bake_book_process(book_dir, log_instance, utility_instance) + + +if __name__ == "__main__": + # Command line mode + if len(sys.argv) <= 1: + print("Please provide a path for the book dir or use run_gui.py") + else: + # user provided directory name? + if os.path.isdir(sys.argv[1]): + # assume user has provided directory + # and set all options to defaults + bake_book(sys.argv[1]) + else: + print("Error : " + "provided path is either inaccessible or innexistant") diff --git a/utilities/run_gui.py b/utilities/run_gui.py new file mode 100644 index 0000000..a9994ba --- /dev/null +++ b/utilities/run_gui.py @@ -0,0 +1,177 @@ +from tkinter import (Tk, Frame, BooleanVar, Text, + Checkbutton, Label, Button, + StringVar, Entry, Scrollbar, + END) +from tkinter.filedialog import askdirectory +from tkinter.messagebox import showinfo +import os +import run + + +class SelectFrame(Frame): + def __init__(self, parent, controller): + # default dir + Frame.__init__(self, parent) + + self.controller = controller + + default_book_dir = os.path.dirname( + os.path.dirname( + os.path.realpath(__file__) + ) + ) + # Variables + self.do_bake_book = BooleanVar(value=True) + self.printing = BooleanVar() + self.book_dir = StringVar(value=default_book_dir) + + # Labels + self.tasks_label = Label(self, text="Tasks to be performed:") + self.debug_label = Label(self, text="Debug options:") + + # Buttons + self.bake_book_check_button = Checkbutton(self, + text="Bake book", + variable=self.do_bake_book, + onvalue=True, offvalue=False) + self.printing_button = Checkbutton(self, + text="Enable diagnostic printing", + variable=self.printing, + onvalue=True, offvalue=False) + self.change_book_dir = Button(self, + text="Change book dir", + command=self.change_dir) + self.start_button = Button(self, + text="Start selected jobs", + command=self.start_jobs) + + # Text + self.book_path_text = Entry(self, + wrap=None, + textvariable=self.book_dir + ) + self.book_path_text.xview_moveto(1) + + self.tasks_label.pack() + self.bake_book_check_button.pack() + self.book_path_text.pack() + self.change_book_dir.pack() + self.debug_label.pack() + self.printing_button.pack() + self.start_button.pack() + + def start_jobs(self): + # checking that the selected dir exists + if not os.path.isdir(self.book_dir.get()): + showinfo(title="Error", + message="The selected dir doesn't exist !") + return + + steps = [] + + if self.do_bake_book.get(): + steps.append("BakeBookStep") + + self.controller.start_jobs(steps, + self.book_dir.get(), + self.printing.get()) + + def change_dir(self): + _book_dir = askdirectory( + initialdir=self.book_dir.get(), + title="Select the book directory") + self.book_dir.set(_book_dir) + + +class JobStep(Frame): + def __init__(self, parent, controller): + Frame.__init__(self, parent) + + self.controller = controller + + self.main_label = Label(self) + self.main_label.pack() + + self.printing_text = None + + def add_printing(self, text): + if self.printing_text is None: + self.printing_text = Text(self, height=100) + scroll = Scrollbar(self) + self.printing_text.configure(yscrollcommand=scroll.set) + self.printing_text.pack() + self.printing_text.insert(END, f"\n{text}") + self.printing_text.see("end") + self.update() + + +class BakeBookStep(JobStep): + def __init__(self, parent, controller): + super().__init__(parent, controller) + + self.main_label.config( + text="Will read current book6 text.\n" + "Touch no files until done!" + ) + + +class Window(Tk): + def __init__(self, *args, **kwargs): + super().__init__() + # Adding a title to the window + self.title("Book6 utilities") + self.geometry('500x200') + self.resizable(False, False) + + # the container is where we'll stack a bunch of frames + # on top of each other, then the one we want visible + # will be raised above the others + container = Frame(self) + container.pack(side="top", fill="both", expand=True) + container.grid_rowconfigure(0, weight=1) + container.grid_columnconfigure(0, weight=1) + + self.current_frame = None + + self.frames = {} + for F in (SelectFrame, BakeBookStep): + page_name = F.__name__ + frame = F(parent=container, controller=self) + self.frames[page_name] = frame + # put all of the pages in the same location; + # the one on the top of the stacking order + # will be the one that is visible. + frame.grid(row=0, column=0, sticky="nsew") + + self.show_frame("SelectFrame") + + def show_frame(self, page_name): + '''Show a frame for the given page name''' + frame = self.frames[page_name] + self.current_frame = frame + frame.tkraise() + self.update() + + def print_info(self, message): + self.current_frame.add_printing(message) + + def start_jobs(self, steps, book_dir, printing): + for step in steps: + if step == "BakeBookStep": + self.show_frame("BakeBookStep") + run.bake_book( + book_dir, + printing, + self) + showinfo(title="Bake book", + message="Bake book step complete") + showinfo(title="Finished", + message="All task are done") + self.show_frame("SelectFrame") + + +if __name__ == "__main__": + window = Window() + window.mainloop() + + diff --git a/utilities/utils.py b/utilities/utils.py new file mode 100644 index 0000000..2f76f82 --- /dev/null +++ b/utilities/utils.py @@ -0,0 +1,104 @@ +import subprocess +import os +import shutil + + +def execute_command(command, cwd=None): + if cwd is None: + cwd = os.path.dirname(os.path.realpath(__file__)) + do_cmd = subprocess.Popen( + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd + ) + return do_cmd.communicate() + + +class Utilities: + + def __init__(self, log_instance, book_dir): + self.log_instance = log_instance + self.book_dir = book_dir + self.base_dir = book_dir + + def set_new_working_dir(self, new_base_dir): + self.base_dir = new_base_dir + + def read_file(self, target_file_path): + """Return a file as a list of lower case strings""" + complete_target_file_path = os.path.join(self.base_dir, + target_file_path) + with open(complete_target_file_path, + "r", encoding="utf-8", errors="replace") as f: + return f.readlines() + + def write_file(self, target_file_path, data): + """Write list of strings to file""" + complete_target_file_path = os.path.join(self.base_dir, + target_file_path) + with open(complete_target_file_path, + "w", encoding="utf-8") as f: + for line in data: + f.write(line) + self.log_instance.logit(f"{complete_target_file_path} written") + return complete_target_file_path + + def cmd(self, command, cwd=None): + """Execute system command""" + _out, _err = execute_command(command, cwd) + if _err: + self.log_instance.logitw(_err.decode("utf-8").strip()) + pass + + def uncase(string_list): + """Return lower case version of a list of strings""" + uncased_string = [] + for string in string_list: + uncased_string.append(string.lower()) + return uncased_string + + def imgcopy(self, dirname): + """Duplicate any image files in the pdf directory""" + target_dir = os.path.join(self.base_dir, dirname) + for f in os.listdir(target_dir): + ftype = os.path.splitext(f)[1] + if ftype == ".svg": + self.log_instance.logit("SVG image found, probable pandoc failure") + if ftype in [".svg", ".jpg", ".jpeg", ".png", ".gif"]: + shutil.copy(os.path.join(target_dir, f), + os.path.join(self.book_dir, "pdf", f)) + + +class Logging: + + def __init__(self, log_path, printing=False, gui_instance=None): + self.log_path = log_path + self.printing = printing + self.warnings = 0 + self.gui_instance = gui_instance + # Deleting old log if present + if os.path.isfile(log_path): + os.remove(log_path) + + def logit(self, msg): + """Add a message to the log file""" + with open(self.log_path, "a", encoding="utf-8") as f: + f.write(msg + "\n") + if self.printing: + print(msg) + if self.gui_instance is not None: + self.gui_instance.print_info(msg) + + def logitw(self, msg): + """Add a warning message to the log file""" + self.logit("WARNING: " + msg) + self.warnings += 1 + + def dprint(self, *msg): + """Diagnostic print""" + if self.printing: + print(*msg) + + def crash(self, msg): + """Log and crash""" + self.printing = True + self.logit("CRASH " + msg) + sys.exit(1)