diff --git a/plugins/python/comics_project_management_tools/comics_exporter.py b/plugins/python/comics_project_management_tools/comics_exporter.py index fa5efa2f8f..9a62b1077c 100644 --- a/plugins/python/comics_project_management_tools/comics_exporter.py +++ b/plugins/python/comics_project_management_tools/comics_exporter.py @@ -1,647 +1,647 @@ """ Copyright (c) 2017 Wolthera van Hövell tot Westerflier This file is part of the Comics Project Management Tools(CPMT). CPMT is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. CPMT is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with the CPMT. If not, see . """ """ An exporter that take the comicsConfig and uses it to generate several files. """ import sys from pathlib import Path import zipfile from xml.dom import minidom from xml.etree import ElementTree as ET import types import re from PyQt5.QtWidgets import QLabel, QProgressDialog, QMessageBox, qApp # For the progress dialog. from PyQt5.QtCore import QElapsedTimer, QLocale, Qt, QRectF, QPointF from PyQt5.QtGui import QImage, QTransform, QPainterPath, QFontMetrics, QFont from krita import * from . import exporters """ The sizesCalculator is a convenience class for interpretting the resize configuration from the export settings dialog. It is also used for batch resize. """ class sizesCalculator(): def __init__(self): pass def get_scale_from_resize_config(self, config, listSizes): listScaleTo = listSizes oldWidth = listSizes[0] oldHeight = listSizes[1] oldXDPI = listSizes[2] oldYDPI = listSizes[3] if "Method" in config.keys(): method = config["Method"] if method is 0: # percentage percentage = config["Percentage"] / 100 listScaleTo[0] = round(oldWidth * percentage) listScaleTo[1] = round(oldHeight * percentage) if method is 1: # dpi DPI = config["DPI"] listScaleTo[0] = round((oldWidth / oldXDPI) * DPI) listScaleTo[1] = round((oldHeight / oldYDPI) * DPI) listScaleTo[2] = DPI listScaleTo[3] = DPI if method is 2: # maximum width width = config["Width"] listScaleTo[0] = width listScaleTo[1] = round((oldHeight / oldWidth) * width) if method is 3: # maximum height height = config["Height"] listScaleTo[1] = height listScaleTo[0] = round((oldWidth / oldHeight) * height) return listScaleTo """ The comicsExporter is a class that batch exports to all the requested formats. Make it, set_config with the right data, and then call up "export". The majority of the functions are meta-data encoding functions. """ class comicsExporter(): acbfLocation = str() acbfPageData = [] cometLocation = str() comicRackInfo = str() pagesLocationList = {} # set of keys used to define specific export behaviour for this page. - pageKeys = ["acbf_title", "acbf_none", "acbf_fade", "acbf_blend", "acbf_horizontal", "acbf_vertical"] + pageKeys = ["acbf_title", "acbf_none", "acbf_fade", "acbf_blend", "acbf_horizontal", "acbf_vertical", "epub_spread"] def __init__(self): pass """ The configuration of the exporter. @param config: A dictionary containing all the config. @param projectUrl: the main location of the project folder. """ def set_config(self, config, projectURL): self.configDictionary = config self.projectURL = projectURL self.pagesLocationList = {} self.acbfLocation = str() self.acbfPageData = [] self.cometLocation = str() self.comicRackInfo = str() """ Export everything according to config and get yourself a coffee. This won't work if the config hasn't been set. """ def export(self): export_success = False path = Path(self.projectURL) if path.exists(): # Make a meta-data folder so we keep the export folder nice and clean. exportPath = path / self.configDictionary["exportLocation"] if Path(exportPath / "metadata").exists() is False: Path(exportPath / "metadata").mkdir() # Get to which formats to export, and set the sizeslist. lengthProcess = len(self.configDictionary["pages"]) sizesList = {} if "CBZ" in self.configDictionary.keys(): if self.configDictionary["CBZactive"]: lengthProcess += 5 sizesList["CBZ"] = self.configDictionary["CBZ"] if "EPUB" in self.configDictionary.keys(): if self.configDictionary["EPUBactive"]: lengthProcess += 1 sizesList["EPUB"] = self.configDictionary["EPUB"] if "TIFF" in self.configDictionary.keys(): if self.configDictionary["TIFFactive"]: sizesList["TIFF"] = self.configDictionary["TIFF"] # Export the pngs according to the sizeslist. # Create a progress dialog. self.progress = QProgressDialog(i18n("Preparing export."), str(), 0, lengthProcess) self.progress.setWindowTitle(i18n("Exporting Comic...")) self.progress.setCancelButton(None) self.timer = QElapsedTimer() self.timer.start() self.progress.show() qApp.processEvents() export_success = self.save_out_pngs(sizesList) # Export acbf metadata. if export_success: if "CBZ" in sizesList.keys(): title = self.configDictionary["projectName"] if "title" in self.configDictionary.keys(): title = str(self.configDictionary["title"]).replace(" ", "_") self.acbfLocation = str(exportPath / "metadata" / str(title + ".acbf")) locationStandAlone = str(exportPath / str(title + ".acbf")) self.progress.setLabelText(i18n("Saving out ACBF and\nACBF standalone")) self.progress.setValue(self.progress.value()+2) export_success = exporters.ACBF.write_xml(self.configDictionary, self.acbfPageData, self.pagesLocationList["CBZ"], self.acbfLocation, locationStandAlone, self.projectURL) print("CPMT: Exported to ACBF", export_success) # Export and package CBZ and Epub. if export_success: if "CBZ" in sizesList.keys(): export_success = self.export_to_cbz(exportPath) print("CPMT: Exported to CBZ", export_success) if "EPUB" in sizesList.keys(): self.progress.setLabelText(i18n("Saving out EPUB")) self.progress.setValue(self.progress.value()+1) export_success = exporters.EPUB.export(self.configDictionary, self.projectURL, self.pagesLocationList["EPUB"], self.acbfPageData) print("CPMT: Exported to EPUB", export_success) else: QMessageBox.warning(self, i18n("Export not Possible"), i18n("Nothing to export, URL not set."), QMessageBox.Ok) print("CPMT: Nothing to export, url not set.") return export_success """ This calls up all the functions necessary for making a cbz. """ def export_to_cbz(self, exportPath): title = self.configDictionary["projectName"] if "title" in self.configDictionary.keys(): title = str(self.configDictionary["title"]).replace(" ", "_") self.progress.setLabelText(i18n("Saving out CoMet\nmetadata file")) self.progress.setValue(self.progress.value()+1) self.cometLocation = str(exportPath / "metadata" / str(title + " CoMet.xml")) export_success = exporters.CoMet.write_xml(self.configDictionary, self.pagesLocationList["CBZ"], self.cometLocation) self.comicRackInfo = str(exportPath / "metadata" / "ComicInfo.xml") self.progress.setLabelText(i18n("Saving out Comicrack\nmetadata file")) self.progress.setValue(self.progress.value()+1) export_success = exporters.comic_rack_xml.write_xml(self.configDictionary, self.pagesLocationList["CBZ"], self.comicRackInfo) self.package_cbz(exportPath) return export_success def save_out_pngs(self, sizesList): # A small fix to ensure crop to guides is set. if "cropToGuides" not in self.configDictionary.keys(): self.configDictionary["cropToGuides"] = False # Check if we have pages at all... if "pages" in self.configDictionary.keys(): # Check if there's export methods, and if so make sure the appropriate dictionaries are initialised. if len(sizesList.keys()) < 1: print("CPMT: Export failed because there's no export methods set.") return False else: for key in sizesList.keys(): self.pagesLocationList[key] = [] # Get the appropriate paths. path = Path(self.projectURL) exportPath = path / self.configDictionary["exportLocation"] pagesList = self.configDictionary["pages"] fileName = str(exportPath) """ Mini function to handle the setup of this string. """ def timeString(timePassed, timeEstimated): return str(i18n("Time passed: {passedString}\n Estimated: {estimated}")).format(passedString=timePassed, estimated=timeEstimated) for p in range(0, len(pagesList)): pagesDone = str(i18n("{pages} of {pagesTotal} done.")).format(pages=p, pagesTotal=len(pagesList)) # Update the label in the progress dialog. self.progress.setValue(p) timePassed = self.timer.elapsed() if p > 0: timeEstimated = (len(pagesList) - p) * (timePassed / p) estimatedString = self.parseTime(timeEstimated) else: estimatedString = str(u"\u221E") passedString = self.parseTime(timePassed) self.progress.setLabelText("\n".join([pagesDone, timeString(passedString, estimatedString), i18n("Opening next page")])) qApp.processEvents() # Get the appropriate url and open the page. url = str(Path(self.projectURL) / pagesList[p]) page = Application.openDocument(url) page.waitForDone() # Update the progress bar a little self.progress.setLabelText("\n".join([pagesDone, timeString(self.parseTime(self.timer.elapsed()), estimatedString), i18n("Cleaning up page")])) # remove layers and flatten. labelList = self.configDictionary["labelsToRemove"] panelsAndText = [] # These three lines are what is causing the page not to close. root = page.rootNode() self.getPanelsAndText(root, panelsAndText) self.removeLayers(labelList, root) page.refreshProjection() # We'll need the offset and scale for aligning the panels and text correctly. We're getting this from the CBZ pageData = {} pageData["vector"] = panelsAndText tree = ET.fromstring(page.documentInfo()) pageData["title"] = page.name() calligra = "{http://www.calligra.org/DTD/document-info}" about = tree.find(calligra + "about") keywords = about.find(calligra + "keyword") keys = str(keywords.text).split(",") pKeys = [] for key in keys: if key in self.pageKeys: pKeys.append(key) pageData["keys"] = pKeys page.flatten() page.waitForDone() batchsave = Application.batchmode() Application.setBatchmode(True) # Start making the format specific copy. for key in sizesList.keys(): # Update the progress bar a little self.progress.setLabelText("\n".join([pagesDone, timeString(self.parseTime(self.timer.elapsed()), estimatedString), str(i18n("Exporting for {key}")).format(key=key)])) w = sizesList[key] # copy over data projection = page.clone() projection.setBatchmode(True) # Crop. Cropping per guide only happens if said guides have been found. if w["Crop"] is True: listHGuides = [] listHGuides = page.horizontalGuides() listHGuides.sort() for i in range(len(listHGuides) - 1, 0, -1): if listHGuides[i] < 0 or listHGuides[i] > page.height(): listHGuides.pop(i) listVGuides = page.verticalGuides() listVGuides.sort() for i in range(len(listVGuides) - 1, 0, -1): if listVGuides[i] < 0 or listVGuides[i] > page.width(): listVGuides.pop(i) if self.configDictionary["cropToGuides"] and len(listVGuides) > 1: cropx = listVGuides[0] cropw = listVGuides[-1] - cropx else: cropx = self.configDictionary["cropLeft"] cropw = page.width() - self.configDictionary["cropRight"] - cropx if self.configDictionary["cropToGuides"] and len(listHGuides) > 1: cropy = listHGuides[0] croph = listHGuides[-1] - cropy else: cropy = self.configDictionary["cropTop"] croph = page.height() - self.configDictionary["cropBottom"] - cropy projection.crop(cropx, cropy, cropw, croph) projection.waitForDone() qApp.processEvents() # resize appropriately else: cropx = 0 cropy = 0 res = page.resolution() listScales = [projection.width(), projection.height(), res, res] projectionOldSize = [projection.width(), projection.height()] sizesCalc = sizesCalculator() listScales = sizesCalc.get_scale_from_resize_config(config=w, listSizes=listScales) projection.unlock() projection.scaleImage(listScales[0], listScales[1], listScales[2], listScales[3], "bicubic") projection.waitForDone() qApp.processEvents() # png, gif and other webformats should probably be in 8bit srgb at maximum. if key != "TIFF": if (projection.colorModel() != "RGBA" and projection.colorModel() != "GRAYA") or projection.colorDepth() != "U8": projection.setColorSpace("RGBA", "U8", "sRGB built-in") else: # Tiff on the other hand can handle all the colormodels, but can only handle integer bit depths. # Tiff is intended for print output, and 16 bit integer will be sufficient. if projection.colorDepth() != "U8" or projection.colorDepth() != "U16": projection.setColorSpace(page.colorModel(), "U16", page.colorProfile()) # save # Make sure the folder name for this export exists. It'll allow us to keep the # export folders nice and clean. folderName = str(key + "-" + w["FileType"]) if Path(exportPath / folderName).exists() is False: Path.mkdir(exportPath / folderName) # Get a nice and descriptive fle name. fn = str(Path(exportPath / folderName) / str("page_" + format(p, "03d") + "_" + str(listScales[0]) + "x" + str(listScales[1]) + "." + w["FileType"])) # Finally save and add the page to a list of pages. This will make it easy for the packaging function to # find the pages and store them. projection.exportImage(fn, InfoObject()) projection.waitForDone() qApp.processEvents() if key == "CBZ" or key == "EPUB": transform = {} transform["offsetX"] = cropx transform["offsetY"] = cropy transform["resDiff"] = page.resolution() / 72 transform["scaleWidth"] = projection.width() / projectionOldSize[0] transform["scaleHeight"] = projection.height() / projectionOldSize[1] pageData["transform"] = transform self.pagesLocationList[key].append(fn) projection.close() self.acbfPageData.append(pageData) page.close() self.progress.setValue(len(pagesList)) Application.setBatchmode(batchsave) # TODO: Check what or whether memory leaks are still caused and otherwise remove the entry below. print("CPMT: Export has finished. If there are memory leaks, they are caused by file layers.") return True print("CPMT: Export not happening because there aren't any pages.") QMessageBox.warning(self, i18n("Export not Possible"), i18n("Export not happening because there are no pages."), QMessageBox.Ok) return False """ Function to get the panel and text data. """ def getPanelsAndText(self, node, list): textLayersToSearch = ["text"] panelLayersToSearch = ["panels"] if "textLayerNames" in self.configDictionary.keys(): textLayersToSearch = self.configDictionary["textLayerNames"] if "panelLayerNames" in self.configDictionary.keys(): panelLayersToSearch = self.configDictionary["panelLayerNames"] if node.type() == "vectorlayer": for name in panelLayersToSearch: if str(name).lower() in str(node.name()).lower(): for shape in node.shapes(): if (shape.type() == "groupshape"): self.getPanelsAndTextVector(shape, list) else: self.handleShapeDescription(shape, list) for name in textLayersToSearch: if str(name).lower() in str(node.name()).lower(): for shape in node.shapes(): if (shape.type() == "groupshape"): self.getPanelsAndTextVector(shape, list, True) else: self.handleShapeDescription(shape, list, True) else: if node.childNodes(): for child in node.childNodes(): self.getPanelsAndText(node=child, list=list) def parseTime(self, time = 0): timeList = [] timeList.append(str(int(time / 60000))) timeList.append(format(int((time%60000) / 1000), "02d")) timeList.append(format(int(time % 1000), "03d")) return ":".join(timeList) """ Function to get the panel and text data from a group shape """ def getPanelsAndTextVector(self, group, list, textOnly=False): for shape in group.shapes(): if (shape.type() == "groupshape"): self.getPanelsAndTextVector(shape, list, textOnly) else: self.handleShapeDescription(shape, list, textOnly) """ Function to get text and panels in a format that acbf will accept """ def handleShapeDescription(self, shape, list, textOnly=False): if (shape.type() != "KoSvgTextShapeID" and textOnly is True): return shapeDesc = {} shapeDesc["name"] = shape.name() rect = shape.boundingBox() listOfPoints = [rect.topLeft(), rect.topRight(), rect.bottomRight(), rect.bottomLeft()] shapeDoc = minidom.parseString(shape.toSvg()) docElem = shapeDoc.documentElement svgRegExp = re.compile('[MLCSQHVATmlzcqshva]\d+\.?\d* \d+\.?\d*') transform = docElem.getAttribute("transform") coord = [] adjust = QTransform() # TODO: If we get global transform api, use that instead of parsing manually. if "translate" in transform: transform = transform.replace('translate(', '') for c in transform[:-1].split(" "): coord.append(float(c)) if len(coord) < 2: coord.append(coord[0]) adjust = QTransform(1, 0, 0, 1, coord[0], coord[1]) if "matrix" in transform: transform = transform.replace('matrix(', '') for c in transform[:-1].split(" "): coord.append(float(c)) adjust = QTransform(coord[0], coord[1], coord[2], coord[3], coord[4], coord[5]) path = QPainterPath() if docElem.localName == "path": dVal = docElem.getAttribute("d") listOfSvgStrings = [" "] listOfSvgStrings = svgRegExp.findall(dVal) if listOfSvgStrings: listOfPoints = [] for l in listOfSvgStrings: line = l[1:] coordinates = line.split(" ") if len(coordinates) < 2: coordinates.append(coordinates[0]) x = float(coordinates[-2]) y = float(coordinates[-1]) offset = QPointF() if l.islower(): offset = listOfPoints[0] if l.lower().startswith("m"): path.moveTo(QPointF(x, y) + offset) elif l.lower().startswith("h"): y = listOfPoints[-1].y() path.lineTo(QPointF(x, y) + offset) elif l.lower().startswith("v"): x = listOfPoints[-1].x() path.lineTo(QPointF(x, y) + offset) elif l.lower().startswith("c"): path.cubicTo(coordinates[0], coordinates[1], coordinates[2], coordinates[3], x, y) else: path.lineTo(QPointF(x, y) + offset) path.setFillRule(Qt.WindingFill) for polygon in path.simplified().toSubpathPolygons(adjust): for point in polygon: listOfPoints.append(point) elif docElem.localName == "rect": listOfPoints = [] if (docElem.hasAttribute("x")): x = float(docElem.getAttribute("x")) else: x = 0 if (docElem.hasAttribute("y")): y = float(docElem.getAttribute("y")) else: y = 0 w = float(docElem.getAttribute("width")) h = float(docElem.getAttribute("height")) path.addRect(QRectF(x, y, w, h)) for point in path.toFillPolygon(adjust): listOfPoints.append(point) elif docElem.localName == "ellipse": listOfPoints = [] if (docElem.hasAttribute("cx")): x = float(docElem.getAttribute("cx")) else: x = 0 if (docElem.hasAttribute("cy")): y = float(docElem.getAttribute("cy")) else: y = 0 ry = float(docElem.getAttribute("ry")) rx = float(docElem.getAttribute("rx")) path.addEllipse(QPointF(x, y), rx, ry) for point in path.toFillPolygon(adjust): listOfPoints.append(point) elif docElem.localName == "text": # NOTE: This only works for horizontal preformated text. Vertical text needs a different # ordering of the rects, and wraparound should try to take the shape it is wrapped in. family = "sans-serif" if docElem.hasAttribute("font-family"): family = docElem.getAttribute("font-family") size = "11" if docElem.hasAttribute("font-size"): size = docElem.getAttribute("font-size") multilineText = True for el in docElem.childNodes: if el.nodeType == minidom.Node.TEXT_NODE: multilineText = False if multilineText: listOfPoints = [] listOfRects = [] # First we collect all the possible line-rects. for el in docElem.childNodes: if docElem.hasAttribute("font-family"): family = docElem.getAttribute("font-family") if docElem.hasAttribute("font-size"): size = docElem.getAttribute("font-size") fontsize = int(size) font = QFont(family, fontsize) string = el.toxml() string = re.sub("\<.*?\>", " ", string) string = string.replace(" ", " ") width = min(QFontMetrics(font).width(string.strip()), rect.width()) height = QFontMetrics(font).height() anchor = "start" if docElem.hasAttribute("text-anchor"): anchor = docElem.getAttribute("text-anchor") top = rect.top() if len(listOfRects)>0: top = listOfRects[-1].bottom() if anchor == "start": spanRect = QRectF(rect.left(), top, width, height) listOfRects.append(spanRect) elif anchor == "end": spanRect = QRectF(rect.right()-width, top, width, height) listOfRects.append(spanRect) else: # Middle spanRect = QRectF(rect.center().x()-(width*0.5), top, width, height) listOfRects.append(spanRect) # Now we have all the rects, we can check each and draw a # polygon around them. heightAdjust = (rect.height()-(listOfRects[-1].bottom()-rect.top()))/len(listOfRects) for i in range(len(listOfRects)): span = listOfRects[i] addtionalHeight = i*heightAdjust if i == 0: listOfPoints.append(span.topLeft()) listOfPoints.append(span.topRight()) else: if listOfRects[i-1].width()< span.width(): listOfPoints.append(QPointF(span.right(), span.top()+addtionalHeight)) listOfPoints.insert(0, QPointF(span.left(), span.top()+addtionalHeight)) else: bottom = listOfRects[i-1].bottom()+addtionalHeight-heightAdjust listOfPoints.append(QPointF(listOfRects[i-1].right(), bottom)) listOfPoints.insert(0, QPointF(listOfRects[i-1].left(), bottom)) listOfPoints.append(QPointF(span.right(), rect.bottom())) listOfPoints.insert(0, QPointF(span.left(), rect.bottom())) path = QPainterPath() path.moveTo(listOfPoints[0]) for p in range(1, len(listOfPoints)): path.lineTo(listOfPoints[p]) path.closeSubpath() listOfPoints = [] for point in path.toFillPolygon(adjust): listOfPoints.append(point) shapeDesc["boundingBox"] = listOfPoints if (shape.type() == "KoSvgTextShapeID" and textOnly is True): shapeDesc["text"] = shape.toSvg() list.append(shapeDesc) """ Function to remove layers when they have the given labels. If not, but the node does have children, check those too. """ def removeLayers(self, labels, node): if node.colorLabel() in labels: node.remove() else: if node.childNodes(): for child in node.childNodes(): self.removeLayers(labels, node=child) """ package cbz puts all the meta-data and relevant files into an zip file ending with ".cbz" """ def package_cbz(self, exportPath): # Use the project name if there's no title to avoid sillyness with unnamed zipfiles. title = self.configDictionary["projectName"] if "title" in self.configDictionary.keys(): title = str(self.configDictionary["title"]).replace(" ", "_") # Get the appropriate path. url = str(exportPath / str(title + ".cbz")) # Create a zip file. cbzArchive = zipfile.ZipFile(url, mode="w", compression=zipfile.ZIP_STORED) # Add all the meta data files. cbzArchive.write(self.acbfLocation, Path(self.acbfLocation).name) cbzArchive.write(self.cometLocation, Path(self.cometLocation).name) cbzArchive.write(self.comicRackInfo, Path(self.comicRackInfo).name) comic_book_info_json_dump = str() self.progress.setLabelText(i18n("Saving out Comicbook\ninfo metadata file")) self.progress.setValue(self.progress.value()+1) comic_book_info_json_dump = exporters.comic_book_info.writeJson(self.configDictionary) cbzArchive.comment = comic_book_info_json_dump.encode("utf-8") # Add the pages. if "CBZ" in self.pagesLocationList.keys(): for page in self.pagesLocationList["CBZ"]: if (Path(page).exists()): cbzArchive.write(page, Path(page).name) self.progress.setLabelText(i18n("Packaging CBZ")) self.progress.setValue(self.progress.value()+1) # Close the zip file when done. cbzArchive.close() diff --git a/plugins/python/comics_project_management_tools/exporters/CPMT_EPUB_exporter.py b/plugins/python/comics_project_management_tools/exporters/CPMT_EPUB_exporter.py index c39db0877f..1947b14982 100644 --- a/plugins/python/comics_project_management_tools/exporters/CPMT_EPUB_exporter.py +++ b/plugins/python/comics_project_management_tools/exporters/CPMT_EPUB_exporter.py @@ -1,727 +1,827 @@ """ Copyright (c) 2018 Wolthera van Hövell tot Westerflier This file is part of the Comics Project Management Tools(CPMT). CPMT is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. CPMT is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with the CPMT. If not, see . """ """ Create an epub folder, finally, package to a epubzip. """ import shutil import os from pathlib import Path from PyQt5.QtXml import QDomDocument, QDomElement, QDomText, QDomNodeList from PyQt5.QtCore import Qt, QDateTime, QPointF -from PyQt5.QtGui import QImage, QPolygonF +from PyQt5.QtGui import QImage, QPolygonF, QColor def export(configDictionary = {}, projectURL = str(), pagesLocationList = [], pageData = []): path = Path(os.path.join(projectURL, configDictionary["exportLocation"])) exportPath = path / "EPUB-files" metaInf = exportPath / "META-INF" oebps = exportPath / "OEBPS" imagePath = oebps / "Images" - stylesPath = oebps / "Styles" + # Don't write empty folders. Epubcheck doesn't like that. + # stylesPath = oebps / "Styles" textPath = oebps / "Text" if exportPath.exists() is False: exportPath.mkdir() metaInf.mkdir() oebps.mkdir() imagePath.mkdir() - stylesPath.mkdir() + # stylesPath.mkdir() textPath.mkdir() mimetype = open(str(Path(exportPath / "mimetype")), mode="w") mimetype.write("application/epub+zip") mimetype.close() container = QDomDocument() cRoot = container.createElement("container") cRoot.setAttribute("version", "1.0") cRoot.setAttribute("xmlns", "urn:oasis:names:tc:opendocument:xmlns:container") container.appendChild(cRoot) rootFiles = container.createElement("rootfiles") rootfile = container.createElement("rootfile") rootfile.setAttribute("full-path", "OEBPS/content.opf") rootfile.setAttribute("media-type", "application/oebps-package+xml") rootFiles.appendChild(rootfile) cRoot.appendChild(rootFiles) containerFile = open(str(Path(metaInf / "container.xml")), 'w', newline="", encoding="utf-8") containerFile.write(container.toString(indent=2)) containerFile.close() # copyimages to images pagesList = [] if len(pagesLocationList)>0: if "cover" in configDictionary.keys(): coverNumber = configDictionary["pages"].index(configDictionary["cover"]) else: coverNumber = 0 for p in pagesLocationList: if os.path.exists(p): shutil.copy2(p, str(imagePath)) pagesList.append(str(Path(imagePath / os.path.basename(p)))) if len(pagesLocationList) >= coverNumber: coverpageurl = pagesList[coverNumber] else: print("CPMT: Couldn't find the location for the epub images.") return False - # for each image, make an xml file + # for each image, make an xhtml file + htmlFiles = [] listOfNavItems = {} + listofSpreads = [] regions = [] for i in range(len(pagesList)): pageName = "Page" + str(i) + ".xhtml" doc = QDomDocument() html = doc.createElement("html") doc.appendChild(html) html.setAttribute("xmlns", "http://www.w3.org/1999/xhtml") html.setAttribute("xmlns:epub", "http://www.idpf.org/2007/ops") + # The viewport is a prerequisite to get pre-paginated + # layouts working. We'll make the layout the same size + # as the image. + head = doc.createElement("head") viewport = doc.createElement("meta") viewport.setAttribute("name", "viewport") img = QImage() img.load(pagesLocationList[i]) w = img.width() h = img.height() widthHeight = "width="+str(w)+", height="+str(h) viewport.setAttribute("content", widthHeight) head.appendChild(viewport) html.appendChild(head) + # Here, we process the region navigation data to percentages + # because we have access here to the width and height of the viewport. + data = pageData[i] transform = data["transform"] for v in data["vector"]: pointsList = [] + dominantColor = QColor(Qt.white) + listOfColors = [] for point in v["boundingBox"]: offset = QPointF(transform["offsetX"], transform["offsetY"]) pixelPoint = QPointF(point.x() * transform["resDiff"], point.y() * transform["resDiff"]) newPoint = pixelPoint - offset x = max(0, min(w, int(newPoint.x() * transform["scaleWidth"]))) y = max(0, min(h, int(newPoint.y() * transform["scaleHeight"]))) + listOfColors.append(img.pixelColor(QPointF(x, y).toPoint())) pointsList.append(QPointF((x/w)*100, (y/h)*100)) regionType = "panel" if "text" in v.keys(): - regionType = "balloon" + regionType = "text" + if len(listOfColors)>0: + dominantColor = listOfColors[-1] + listOfColors = listOfColors[:-1] + for color in listOfColors: + dominantColor.setRedF(0.5*(dominantColor.redF()+color.redF())) + dominantColor.setGreenF(0.5*(dominantColor.greenF()+color.greenF())) + dominantColor.setBlueF(0.5*(dominantColor.blueF()+color.blueF())) region = {} bounds = QPolygonF(pointsList).boundingRect() region["points"] = bounds region["type"] = regionType region["page"] = str(Path(textPath / pageName)) + region["primaryColor"] = dominantColor.name() regions.append(region) + + # We can also figureout here whether the page can be seen as a table of contents entry. + if "acbf_title" in data["keys"]: listOfNavItems[str(Path(textPath / pageName))] = data["title"] + + # Or spreads... + + if "epub_spread" in data["keys"]: + listofSpreads.append(str(Path(textPath / pageName))) body = doc.createElement("body") img = doc.createElement("img") img.setAttribute("src", os.path.relpath(pagesList[i], str(textPath))) body.appendChild(img) html.appendChild(body) filename = str(Path(textPath / pageName)) docFile = open(filename, 'w', newline="", encoding="utf-8") docFile.write(doc.toString(indent=2)) docFile.close() if pagesList[i] == coverpageurl: coverpagehtml = os.path.relpath(filename, str(oebps)) htmlFiles.append(filename) # metadata - write_opf_file(oebps, configDictionary, htmlFiles, pagesList, coverpageurl, coverpagehtml) + write_opf_file(oebps, configDictionary, htmlFiles, pagesList, coverpageurl, coverpagehtml, listofSpreads) write_region_nav_file(oebps, configDictionary, htmlFiles, regions) # toc write_nav_file(oebps, configDictionary, htmlFiles, listOfNavItems) write_ncx_file(oebps, configDictionary, htmlFiles, listOfNavItems) package_epub(configDictionary, projectURL) return True """ Write OPF metadata file """ -def write_opf_file(path, configDictionary, htmlFiles, pagesList, coverpageurl, coverpagehtml): +def write_opf_file(path, configDictionary, htmlFiles, pagesList, coverpageurl, coverpagehtml, listofSpreads): # marc relators # This has several entries removed to reduce it to the most relevant entries. marcRelators = {"abr":i18n("Abridger"), "acp":i18n("Art copyist"), "act":i18n("Actor"), "adi":i18n("Art director"), "adp":i18n("Adapter"), "ann":i18n("Annotator"), "ant":i18n("Bibliographic antecedent"), "arc":i18n("Architect"), "ard":i18n("Artistic director"), "art":i18n("Artist"), "asn":i18n("Associated name"), "ato":i18n("Autographer"), "att":i18n("Attributed name"), "aud":i18n("Author of dialog"), "aut":i18n("Author"), "bdd":i18n("Binding designer"), "bjd":i18n("Bookjacket designer"), "bkd":i18n("Book designer"), "bkp":i18n("Book producer"), "blw":i18n("Blurb writer"), "bnd":i18n("Binder"), "bpd":i18n("Bookplate designer"), "bsl":i18n("Bookseller"), "cll":i18n("Calligrapher"), "clr":i18n("Colorist"), "cns":i18n("Censor"), "cov":i18n("Cover designer"), "cph":i18n("Copyright holder"), "cre":i18n("Creator"), "ctb":i18n("Contributor"), "cur":i18n("Curator"), "cwt":i18n("Commentator for written text"), "drm":i18n("Draftsman"), "dsr":i18n("Designer"), "dub":i18n("Dubious author"), "edt":i18n("Editor"), "etr":i18n("Etcher"), "exp":i18n("Expert"), "fnd":i18n("Funder"), "ill":i18n("Illustrator"), "ilu":i18n("Illuminator"), "ins":i18n("Inscriber"), "lse":i18n("Licensee"), "lso":i18n("Licensor"), "ltg":i18n("Lithographer"), "mdc":i18n("Metadata contact"), "oth":i18n("Other"), "own":i18n("Owner"), "pat":i18n("Patron"), "pbd":i18n("Publishing director"), "pbl":i18n("Publisher"), "prt":i18n("Printer"), "sce":i18n("Scenarist"), "scr":i18n("Scribe"), "spn":i18n("Sponsor"), "stl":i18n("Storyteller"), "trc":i18n("Transcriber"), "trl":i18n("Translator"), "tyd":i18n("Type designer"), "tyg":i18n("Typographer"), "wac":i18n("Writer of added commentary"), "wal":i18n("Writer of added lyrics"), "wam":i18n("Writer of accompanying material"), "wat":i18n("Writer of added text"), "win":i18n("Writer of introduction"), "wpr":i18n("Writer of preface"), "wst":i18n("Writer of supplementary textual content")} # opf file opfFile = QDomDocument() opfRoot = opfFile.createElement("package") opfRoot.setAttribute("version", "3.0") opfRoot.setAttribute("unique-identifier", "BookId") opfRoot.setAttribute("xmlns", "http://www.idpf.org/2007/opf") opfRoot.setAttribute("prefix", "rendition: http://www.idpf.org/vocab/rendition/#") opfFile.appendChild(opfRoot) opfMeta = opfFile.createElement("metadata") opfMeta.setAttribute("xmlns:dc", "http://purl.org/dc/elements/1.1/") opfMeta.setAttribute("xmlns:dcterms", "http://purl.org/dc/terms/") # EPUB metadata requires a title, language and uuid langString = "en-US" if "language" in configDictionary.keys(): langString = str(configDictionary["language"]).replace("_", "-") bookLang = opfFile.createElement("dc:language") bookLang.appendChild(opfFile.createTextNode(langString)) opfMeta.appendChild(bookLang) bookTitle = opfFile.createElement("dc:title") if "title" in configDictionary.keys(): bookTitle.appendChild(opfFile.createTextNode(str(configDictionary["title"]))) else: bookTitle.appendChild(opfFile.createTextNode("Comic with no Name")) opfMeta.appendChild(bookTitle) # Generate series title and the like here too. if "seriesName" in configDictionary.keys(): bookTitle.setAttribute("id", "main") refine = opfFile.createElement("meta") refine.setAttribute("refines", "#main") refine.setAttribute("property", "title-type") refine.appendChild(opfFile.createTextNode("main")) opfMeta.appendChild(refine) refine2 = opfFile.createElement("meta") refine2.setAttribute("refines", "#main") refine2.setAttribute("property", "display-seq") refine2.appendChild(opfFile.createTextNode("1")) opfMeta.appendChild(refine2) seriesTitle = opfFile.createElement("dc:title") seriesTitle.appendChild(opfFile.createTextNode(str(configDictionary["seriesName"]))) seriesTitle.setAttribute("id", "series") opfMeta.appendChild(seriesTitle) refineS = opfFile.createElement("meta") refineS.setAttribute("refines", "#series") refineS.setAttribute("property", "title-type") refineS.appendChild(opfFile.createTextNode("collection")) opfMeta.appendChild(refineS) refineS2 = opfFile.createElement("meta") refineS2.setAttribute("refines", "#series") refineS2.setAttribute("property", "display-seq") refineS2.appendChild(opfFile.createTextNode("2")) opfMeta.appendChild(refineS2) if "seriesNumber" in configDictionary.keys(): refineS3 = opfFile.createElement("meta") refineS3.setAttribute("refines", "#series") refineS3.setAttribute("property", "group-position") refineS3.appendChild(opfFile.createTextNode(str(configDictionary["seriesNumber"]))) opfMeta.appendChild(refineS3) uuid = str(configDictionary["uuid"]) uuid = uuid.strip("{") uuid = uuid.strip("}") # Append the id, and assign it as the bookID. uniqueID = opfFile.createElement("dc:identifier") uniqueID.appendChild(opfFile.createTextNode("urn:uuid:"+uuid)) uniqueID.setAttribute("id", "BookId") opfMeta.appendChild(uniqueID) if "authorList" in configDictionary.keys(): + authorEntry = 0 for authorE in range(len(configDictionary["authorList"])): authorDict = configDictionary["authorList"][authorE] authorType = "dc:creator" if "role" in authorDict.keys(): - if str(authorDict["role"]).lower() in ["editor", "assistant editor", "proofreader", "beta"]: + # This determines if someone was just a contributor, but might need a more thorough version. + if str(authorDict["role"]).lower() in ["editor", "assistant editor", "proofreader", "beta", "patron", "funder"]: authorType = "dc:contributor" author = opfFile.createElement(authorType) authorName = [] if "last-name" in authorDict.keys(): authorName.append(authorDict["last-name"]) if "first-name" in authorDict.keys(): authorName.append(authorDict["first-name"]) if "initials" in authorDict.keys(): authorName.append(authorDict["initials"]) if "nickname" in authorDict.keys(): authorName.append("(" + authorDict["nickname"] + ")") author.appendChild(opfFile.createTextNode(", ".join(authorName))) + author.setAttribute("id", "author"+str(authorE)) opfMeta.appendChild(author) if "role" in authorDict.keys(): author.setAttribute("id", "cre" + str(authorE)) role = opfFile.createElement("meta") role.setAttribute("refines", "cre" + str(authorE)) role.setAttribute("scheme", "marc:relators") role.setAttribute("property", "role") roleString = str(authorDict["role"]) - if roleString in marcRelators.values(): + if roleString in marcRelators.values() or roleString in marcRelators.keys(): i = list(marcRelators.values()).index(roleString) roleString = list(marcRelators.keys())[i] else: roleString = "oth" role.appendChild(opfFile.createTextNode(roleString)) opfMeta.appendChild(role) + refine = opfFile.createElement("meta") + refine.setAttribute("refines", "#author"+str(authorE)) + refine.setAttribute("property", "display-seq") + refine.appendChild(opfFile.createTextNode(str(authorE+1))) + opfMeta.appendChild(refine) if "publishingDate" in configDictionary.keys(): date = opfFile.createElement("dc:date") date.appendChild(opfFile.createTextNode(configDictionary["publishingDate"])) opfMeta.appendChild(date) #Creation date modified = opfFile.createElement("meta") modified.setAttribute("property", "dcterms:modified") modified.appendChild(opfFile.createTextNode(QDateTime.currentDateTimeUtc().toString(Qt.ISODate))) opfMeta.appendChild(modified) if "source" in configDictionary.keys(): if len(configDictionary["source"])>0: source = opfFile.createElement("dc:source") source.appendChild(opfFile.createTextNode(configDictionary["source"])) opfMeta.appendChild(source) description = opfFile.createElement("dc:description") if "summary" in configDictionary.keys(): description.appendChild(opfFile.createTextNode(configDictionary["summary"])) else: description.appendChild(opfFile.createTextNode("There was no summary upon generation of this file.")) opfMeta.appendChild(description) # Type can be dictionary or index, or one of those edupub thingies. Not necessary for comics. # typeE = opfFile.createElement("dc:type") # opfMeta.appendChild(typeE) + if "publisherName" in configDictionary.keys(): publisher = opfFile.createElement("dc:publisher") publisher.appendChild(opfFile.createTextNode(configDictionary["publisherName"])) opfMeta.appendChild(publisher) if "isbn-number" in configDictionary.keys(): - publishISBN = opfFile.createElement("dc:identifier") - publishISBN.appendChild(opfFile.createTextNode(str("urn:isbn:") + configDictionary["isbn-number"])) - opfMeta.appendChild(publishISBN) + isbnnumber = configDictionary["isbn-number"] + + if len(isbnnumber)>0: + publishISBN = opfFile.createElement("dc:identifier") + publishISBN.appendChild(opfFile.createTextNode(str("urn:isbn:") + isbnnumber)) + opfMeta.appendChild(publishISBN) + if "license" in configDictionary.keys(): + if len(configDictionary["license"])>0: rights = opfFile.createElement("dc:rights") rights.appendChild(opfFile.createTextNode(configDictionary["license"])) opfMeta.appendChild(rights) + + """ + Not handled + Relation - This is for whether the work has a relationship with another work. + It could be fanart, but also adaptation, an academic work, etc. + Coverage - This is for the time/place that the work covers. Typically to determine + whether an academic work deals with a certain time period or place. + For comics you could use this to mark historical comics, but other than + that we'd need a much better ui to define this. + """ + + # These are all dublin core subjects. + # 3.1 defines the ability to use an authority, but that + # might be a bit too complicated right now. if "genre" in configDictionary.keys(): genreListConf = configDictionary["genre"] if isinstance(configDictionary["genre"], dict): genreListConf = configDictionary["genre"].keys() for g in genreListConf: subject = opfFile.createElement("dc:subject") subject.appendChild(opfFile.createTextNode(g)) opfMeta.appendChild(subject) if "characters" in configDictionary.keys(): for name in configDictionary["characters"]: char = opfFile.createElement("dc:subject") char.appendChild(opfFile.createTextNode(name)) opfMeta.appendChild(char) if "format" in configDictionary.keys(): for formatF in configDictionary["format"]: f = opfFile.createElement("dc:subject") f.appendChild(opfFile.createTextNode(formatF)) opfMeta.appendChild(f) if "otherKeywords" in configDictionary.keys(): for key in configDictionary["otherKeywords"]: word = opfFile.createElement("dc:subject") word.appendChild(opfFile.createTextNode(key)) opfMeta.appendChild(word) # Pre-pagination and layout # Comic are always prepaginated. elLayout = opfFile.createElement("meta") elLayout.setAttribute("property", "rendition:layout") elLayout.appendChild(opfFile.createTextNode("pre-paginated")) opfMeta.appendChild(elLayout) # We should figure out if the pages are portrait or not... elOrientation = opfFile.createElement("meta") elOrientation.setAttribute("property", "rendition:orientation") elOrientation.appendChild(opfFile.createTextNode("portrait")) opfMeta.appendChild(elOrientation) elSpread = opfFile.createElement("meta") elSpread.setAttribute("property", "rendition:spread") elSpread.appendChild(opfFile.createTextNode("landscape")) opfMeta.appendChild(elSpread) opfRoot.appendChild(opfMeta) # Manifest opfManifest = opfFile.createElement("manifest") toc = opfFile.createElement("item") toc.setAttribute("id", "ncx") toc.setAttribute("href", "toc.ncx") toc.setAttribute("media-type", "application/x-dtbncx+xml") opfManifest.appendChild(toc) region = opfFile.createElement("item") region.setAttribute("id", "regions") region.setAttribute("href", "region-nav.xhtml") region.setAttribute("media-type", "application/xhtml+xml") region.setAttribute("properties", "data-nav") # Set the propernavmap to use this later) opfManifest.appendChild(region) nav = opfFile.createElement("item") nav.setAttribute("id", "nav") nav.setAttribute("href", "nav.xhtml") nav.setAttribute("media-type", "application/xhtml+xml") nav.setAttribute("properties", "nav") # Set the propernavmap to use this later) opfManifest.appendChild(nav) ids = 0 for p in pagesList: item = opfFile.createElement("item") item.setAttribute("id", "img"+str(ids)) ids +=1 item.setAttribute("href", os.path.relpath(p, str(path))) item.setAttribute("media-type", "image/png") if os.path.basename(p) == os.path.basename(coverpageurl): item.setAttribute("properties", "cover-image") opfManifest.appendChild(item) ids = 0 for p in htmlFiles: item = opfFile.createElement("item") item.setAttribute("id", "p"+str(ids)) ids +=1 item.setAttribute("href", os.path.relpath(p, str(path))) item.setAttribute("media-type", "application/xhtml+xml") opfManifest.appendChild(item) opfRoot.appendChild(opfManifest) # Spine opfSpine = opfFile.createElement("spine") # this sets the table of contents to use the ncx file opfSpine.setAttribute("toc", "ncx") # Reading Direction: spreadRight = True direction = 0 if "readingDirection" in configDictionary.keys(): if configDictionary["readingDirection"] is "rightToLeft": opfSpine.setAttribute("page-progression-direction", "rtl") spreadRight = False direction = 1 else: opfSpine.setAttribute("page-progression-direction", "ltr") # Here we'd need to switch between the two and if spread keywrod use neither but combine with spread-none - # TODO implement spread keyword. - spread = False ids = 0 for p in htmlFiles: item = opfFile.createElement("itemref") item.setAttribute("idref", "p"+str(ids)) ids +=1 props = [] - if spread: - # Don't do a spread for this one. - props.append("spread-none") + if p in listofSpreads: + # Put this one in the center. + props.append("rendition:page-spread-center") # Reset the spread boolean. # It needs to point at the first side after the spread. # So ltr -> spread-left, rtl->spread-right if direction == 0: spreadRight = False else: spreadRight = True else: if spreadRight: props.append("page-spread-right") spreadRight = False else: props.append("page-spread-left") spreadRight = True item.setAttribute("properties", " ".join(props)) opfSpine.appendChild(item) opfRoot.appendChild(opfSpine) # Guide opfGuide = opfFile.createElement("guide") if coverpagehtml is not None and coverpagehtml.isspace() is False and len(coverpagehtml) > 0: item = opfFile.createElement("reference") item.setAttribute("type", "cover") item.setAttribute("title", "Cover") item.setAttribute("href", coverpagehtml) opfGuide.appendChild(item) opfRoot.appendChild(opfGuide) docFile = open(str(Path(path / "content.opf")), 'w', newline="", encoding="utf-8") docFile.write(opfFile.toString(indent=2)) docFile.close() return True """ Write a region navmap file. """ def write_region_nav_file(path, configDictionary, htmlFiles, regions = []): navDoc = QDomDocument() navRoot = navDoc.createElement("html") navRoot.setAttribute("xmlns", "http://www.w3.org/1999/xhtml") navRoot.setAttribute("xmlns:epub", "http://www.idpf.org/2007/ops") navDoc.appendChild(navRoot) head = navDoc.createElement("head") title = navDoc.createElement("title") title.appendChild(navDoc.createTextNode("Region Navigation")) head.appendChild(title) navRoot.appendChild(head) body = navDoc.createElement("body") navRoot.appendChild(body) nav = navDoc.createElement("nav") nav.setAttribute("epub:type", "region-based") + nav.setAttribute("prefix", "ahl: http://idpf.org/epub/vocab/ahl") body.appendChild(nav) + # Let's write the panels and balloons down now. + olPanels = navDoc.createElement("ol") for region in regions: if region["type"] == "panel": pageName = os.path.relpath(region["page"], str(path)) print("accessing panel") li = navDoc.createElement("li") li.setAttribute("epub:type", "panel") + anchor = navDoc.createElement("a") bounds = region["points"] anchor.setAttribute("href", pageName+"#xywh=percent:"+str(bounds.x())+","+str(bounds.y())+","+str(bounds.width())+","+str(bounds.height())) + + if len(region["primaryColor"])>0: + primaryC = navDoc.createElement("meta") + primaryC.setAttribute("property","ahl:primary-color") + primaryC.setAttribute("content", region["primaryColor"]) + anchor.appendChild(primaryC) + li.appendChild(anchor) olBalloons = navDoc.createElement("ol") + + """ + The region nav spec specifies that we should have text-areas/balloons as a refinement on + the panel. + For each panel, we'll check if there's balloons/text-areas inside, and we'll do that by + checking whether the center point is inside the panel because some comics have balloons + that overlap the gutters. + """ for balloon in regions: - if balloon["type"] == "balloon" and balloon["page"] == region["page"] and bounds.contains(balloon["points"]): + if balloon["type"] == "text" and balloon["page"] == region["page"] and bounds.contains(balloon["points"].center()): liBalloon = navDoc.createElement("li") - liBalloon.setAttribute("epub:type", "balloon") + liBalloon.setAttribute("epub:type", "text-area") + anchorBalloon = navDoc.createElement("a") BBounds = balloon["points"] anchorBalloon.setAttribute("href", pageName+"#xywh=percent:"+str(BBounds.x())+","+str(BBounds.y())+","+str(BBounds.width())+","+str(BBounds.height())) + liBalloon.appendChild(anchorBalloon) olBalloons.appendChild(liBalloon) + if olBalloons.hasChildNodes(): li.appendChild(olBalloons) olPanels.appendChild(li) nav.appendChild(olPanels) navFile = open(str(Path(path / "region-nav.xhtml")), 'w', newline="", encoding="utf-8") navFile.write(navDoc.toString(indent=2)) navFile.close() return True """ Write XHTML nav file. + +This is virtually the same as the NCX file, except that +the navigation document can be styled, and is what 3.1 and +3.2 expect as a primary navigation document. + +This function will both create a table of contents, using the +"acbf_title" feature, as well as a regular pageslist. """ def write_nav_file(path, configDictionary, htmlFiles, listOfNavItems): navDoc = QDomDocument() navRoot = navDoc.createElement("html") navRoot.setAttribute("xmlns", "http://www.w3.org/1999/xhtml") navRoot.setAttribute("xmlns:epub", "http://www.idpf.org/2007/ops") navDoc.appendChild(navRoot) head = navDoc.createElement("head") title = navDoc.createElement("title") title.appendChild(navDoc.createTextNode("Table of Contents")) head.appendChild(title) body = navDoc.createElement("body") navRoot.appendChild(body) + # The Table of Contents + toc = navDoc.createElement("nav") toc.setAttribute("epub:type", "toc") oltoc = navDoc.createElement("ol") li = navDoc.createElement("li") anchor = navDoc.createElement("a") anchor.setAttribute("href", os.path.relpath(htmlFiles[0], str(path))) anchor.appendChild(navDoc.createTextNode("Start")) li.appendChild(anchor) oltoc.appendChild(li) for fileName in listOfNavItems.keys(): li = navDoc.createElement("li") anchor = navDoc.createElement("a") anchor.setAttribute("href", os.path.relpath(fileName, str(path))) anchor.appendChild(navDoc.createTextNode(listOfNavItems[fileName])) li.appendChild(anchor) oltoc.appendChild(li) toc.appendChild(oltoc) body.appendChild(toc) + # The Pages List. + pageslist = navDoc.createElement("nav") pageslist.setAttribute("epub:type", "page-list") olpages = navDoc.createElement("ol") entry = 1 for i in range(len(htmlFiles)): li = navDoc.createElement("li") anchor = navDoc.createElement("a") anchor.setAttribute("href", os.path.relpath(htmlFiles[1], str(path))) anchor.appendChild(navDoc.createTextNode(str(i))) li.appendChild(anchor) olpages.appendChild(li) pageslist.appendChild(olpages) body.appendChild(pageslist) navFile = open(str(Path(path / "nav.xhtml")), 'w', newline="", encoding="utf-8") navFile.write(navDoc.toString(indent=2)) navFile.close() return True """ Write a NCX file. + +This is the same as the navigation document above, but then +for 2.0 backward compatibility. """ def write_ncx_file(path, configDictionary, htmlFiles, listOfNavItems): tocDoc = QDomDocument() ncx = tocDoc.createElement("ncx") ncx.setAttribute("version", "2005-1") ncx.setAttribute("xmlns", "http://www.daisy.org/z3986/2005/ncx/") tocDoc.appendChild(ncx) tocHead = tocDoc.createElement("head") + # NCX also has some meta values that are in the head. + # They are shared with the opf metadata document. + uuid = str(configDictionary["uuid"]) uuid = uuid.strip("{") uuid = uuid.strip("}") metaID = tocDoc.createElement("meta") metaID.setAttribute("content", uuid) metaID.setAttribute("name", "dtb:uid") tocHead.appendChild(metaID) metaDepth = tocDoc.createElement("meta") metaDepth.setAttribute("content", str(1)) metaDepth.setAttribute("name", "dtb:depth") tocHead.appendChild(metaDepth) metaTotal = tocDoc.createElement("meta") metaTotal.setAttribute("content", str(len(htmlFiles))) metaTotal.setAttribute("name", "dtb:totalPageCount") tocHead.appendChild(metaTotal) metaMax = tocDoc.createElement("meta") metaMax.setAttribute("content", str(len(htmlFiles))) metaMax.setAttribute("name", "dtb:maxPageNumber") tocHead.appendChild(metaDepth) ncx.appendChild(tocHead) docTitle = tocDoc.createElement("docTitle") text = tocDoc.createElement("text") if "title" in configDictionary.keys(): text.appendChild(tocDoc.createTextNode(str(configDictionary["title"]))) else: text.appendChild(tocDoc.createTextNode("Comic with no Name")) docTitle.appendChild(text) ncx.appendChild(docTitle) + + # The navmap is a table of contents. navmap = tocDoc.createElement("navMap") navPoint = tocDoc.createElement("navPoint") navPoint.setAttribute("id", "navPoint-1") navPoint.setAttribute("playOrder", "1") navLabel = tocDoc.createElement("navLabel") navLabelText = tocDoc.createElement("text") navLabelText.appendChild(tocDoc.createTextNode("Start")) navLabel.appendChild(navLabelText) navContent = tocDoc.createElement("content") navContent.setAttribute("src", os.path.relpath(htmlFiles[0], str(path))) navPoint.appendChild(navLabel) navPoint.appendChild(navContent) navmap.appendChild(navPoint) entry = 1 for fileName in listOfNavItems.keys(): entry +=1 navPointT = tocDoc.createElement("navPoint") navPointT.setAttribute("id", "navPoint-"+str(entry)) navPointT.setAttribute("playOrder", str(entry)) navLabelT = tocDoc.createElement("navLabel") navLabelTText = tocDoc.createElement("text") navLabelTText.appendChild(tocDoc.createTextNode(listOfNavItems[fileName])) navLabelT.appendChild(navLabelTText) navContentT = tocDoc.createElement("content") navContentT.setAttribute("src", os.path.relpath(fileName, str(path))) navPointT.appendChild(navLabelT) navPointT.appendChild(navContentT) navmap.appendChild(navPointT) ncx.appendChild(navmap) + # The pages list on the other hand just lists all pages. + pagesList = tocDoc.createElement("pageList") navLabelPages = tocDoc.createElement("navLabel") navLabelPagesText = tocDoc.createElement("navLabel") navLabelPagesText.appendChild(tocDoc.createTextNode("Pages")) pagesList.appendChild(navLabelPages) for i in range(len(htmlFiles)): pageTarget = tocDoc.createElement("pageTarget") pageTarget.setAttribute("type", "normal") pageTarget.setAttribute("id", "page-"+str(i)) pageTarget.setAttribute("value", str(i)) navLabelPagesTarget = tocDoc.createElement("navLabel") navLabelPagesTargetText = tocDoc.createElement("navLabel") navLabelPagesTargetText.appendChild(tocDoc.createTextNode(str(i))) navLabelPagesTarget.appendChild(navLabelPagesTargetText) pageTarget.appendChild(navLabelPagesTarget) pageTargetContent = tocDoc.createElement("content") pageTargetContent.setAttribute("src", os.path.relpath(htmlFiles[i], str(path))) pageTarget.appendChild(pageTargetContent) pagesList.appendChild(pageTarget) ncx.appendChild(pagesList) + # Save the document. + docFile = open(str(Path(path / "toc.ncx")), 'w', newline="", encoding="utf-8") docFile.write(tocDoc.toString(indent=2)) docFile.close() return True """ package epub packages the whole epub folder and renames the zip file to .epub. """ def package_epub(configDictionary = {}, projectURL = str()): # Use the project name if there's no title to avoid sillyness with unnamed zipfiles. title = configDictionary["projectName"] if "title" in configDictionary.keys(): title = str(configDictionary["title"]).replace(" ", "_") # Get the appropriate paths. url = os.path.join(projectURL, configDictionary["exportLocation"], title) epub = os.path.join(projectURL, configDictionary["exportLocation"], "EPUB-files") # Make the archive. shutil.make_archive(base_name=url, format="zip", root_dir=epub) # Rename the archive to epub. shutil.move(src=str(url + ".zip"), dst=str(url + ".epub"))