diff --git a/plugins/python/comics_project_management_tools/comics_exporter.py b/plugins/python/comics_project_management_tools/comics_exporter.py index a7b017b538..4a681136a4 100644 --- a/plugins/python/comics_project_management_tools/comics_exporter.py +++ b/plugins/python/comics_project_management_tools/comics_exporter.py @@ -1,645 +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"] def __init__(self): pass """ The 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("Preparing export.", str(), 0, lengthProcess) self.progress.setWindowTitle("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 = self.configDictionary["title"] self.acbfLocation = str(exportPath / "metadata" / str(title + ".acbf")) locationStandAlone = str(exportPath / str(title + ".acbf")) self.progress.setLabelText("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("Saving out EPUB") self.progress.setValue(self.progress.value()+1) export_success = exporters.EPUB.export(self.configDictionary, self.projectURL, self.pagesLocationList["EPUB"]) print("CPMT: Exported to EPUB", export_success) else: QMessageBox.warning(self, i18n("Export not possible"), i18n("Nothing to export,\nurl 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 = self.configDictionary["title"] self.progress.setLabelText("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("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) - timeString = str(i18n("Time passed: {passedString}:\n Estimated:{estimated}")).format(passedString=passedString, estimated=estimatedString) - self.progress.setLabelText("\n".join([pagesDone, timeString, i18n("Opening next page.")])) + 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 - timeString = str(i18n("Time passed: {passedString}:\n Estimated:{estimated}")).format(passedString=self.parseTime(self.timer.elapsed()), estimated=estimatedString) - self.progress.setLabelText("\n".join([pagesDone, timeString, i18n("Cleaning up page.")])) + 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 - timeString = str(i18n("Time passed: {passedString}:\n Estimated:{estimated}")).format(passedString=self.parseTime(self.timer.elapsed()), estimated=estimatedString) - self.progress.setLabelText("\n".join([pagesDone, timeString, str(i18n("Exporting for {key}")).format(key=key)])) + 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": 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\nthere aren't any 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 - TODO: move this to a new file. """ 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 wraped 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 = self.configDictionary["title"] # 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("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("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/comics_project_manager_docker.py b/plugins/python/comics_project_management_tools/comics_project_manager_docker.py index fbebe3bad7..bbbe0445e6 100644 --- a/plugins/python/comics_project_management_tools/comics_project_manager_docker.py +++ b/plugins/python/comics_project_management_tools/comics_project_manager_docker.py @@ -1,922 +1,922 @@ """ 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 . """ """ This is a docker that helps you organise your comics project. """ import sys import json import os import zipfile # quick reading of documents import shutil import enum from math import floor import xml.etree.ElementTree as ET from PyQt5.QtCore import QElapsedTimer, QSize, Qt, QRect from PyQt5.QtGui import QStandardItem, QStandardItemModel, QImage, QIcon, QPixmap, QFontMetrics, QPainter, QPalette, QFont from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QListView, QToolButton, QMenu, QAction, QPushButton, QSpacerItem, QSizePolicy, QWidget, QAbstractItemView, QProgressDialog, QDialog, QFileDialog, QDialogButtonBox, qApp, QSplitter, QSlider, QLabel, QStyledItemDelegate, QStyle, QMessageBox import math from krita import * from . import comics_metadata_dialog, comics_exporter, comics_export_dialog, comics_project_setup_wizard, comics_template_dialog, comics_project_settings_dialog, comics_project_page_viewer, comics_project_translation_scraper """ A very simple class so we can have a label that is single line, but doesn't force the widget size to be bigger. This is used by the project name. """ class Elided_Text_Label(QLabel): mainText = str() def __init__(self, parent=None): super(QLabel, self).__init__(parent) self.setMinimumWidth(self.fontMetrics().width("...")) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) def setMainText(self, text=str()): self.mainText = text self.elideText() def elideText(self): self.setText(self.fontMetrics().elidedText(self.mainText, Qt.ElideRight, self.width())) def resizeEvent(self, event): self.elideText() class CPE(enum.IntEnum): TITLE = Qt.DisplayRole URL = Qt.UserRole + 1 KEYWORDS = Qt.UserRole+2 DESCRIPTION = Qt.UserRole+3 LASTEDIT = Qt.UserRole+4 EDITOR = Qt.UserRole+5 IMAGE = Qt.DecorationRole class comic_page_delegate(QStyledItemDelegate): def __init__(self, parent=None): super(QStyledItemDelegate, self).__init__(parent) def paint(self, painter, option, index): if (index.isValid() == False): return painter.save() painter.setOpacity(0.6) if(option.state & QStyle.State_Selected): painter.fillRect(option.rect, option.palette.highlight()) if (option.state & QStyle.State_MouseOver): painter.setOpacity(0.25) painter.fillRect(option.rect, option.palette.highlight()) painter.setOpacity(1.0) painter.setFont(option.font) metrics = QFontMetrics(option.font) regular = QFont(option.font) italics = QFont(option.font) italics.setItalic(True) icon = QIcon(index.data(CPE.IMAGE)) rect = option.rect margin = 4 decoratonSize = QSize(option.decorationSize) imageSize = icon.actualSize(option.decorationSize) leftSideThumbnail = (decoratonSize.width()-imageSize.width())/2 if (rect.width() < decoratonSize.width()): leftSideThumbnail = max(0, (rect.width()-imageSize.width())/2) topSizeThumbnail = ((rect.height()-imageSize.height())/2)+rect.top() painter.drawImage(QRect(leftSideThumbnail, topSizeThumbnail, imageSize.width(), imageSize.height()), icon.pixmap(imageSize).toImage()) labelWidth = rect.width()-decoratonSize.width()-(margin*3) if (decoratonSize.width()+(margin*2)< rect.width()): textRect = QRect(decoratonSize.width()+margin, margin+rect.top(), labelWidth, metrics.height()) textTitle = metrics.elidedText(str(index.row()+1)+". "+index.data(CPE.TITLE), Qt.ElideRight, labelWidth) painter.drawText(textRect, Qt.TextWordWrap, textTitle) if rect.height()/(metrics.lineSpacing()+margin) > 5 or index.data(CPE.KEYWORDS) is not None: painter.setOpacity(0.6) textRect = QRect(textRect.left(), textRect.bottom()+margin, labelWidth, metrics.height()) if textRect.bottom() < rect.bottom(): textKeyWords = index.data(CPE.KEYWORDS) if textKeyWords == None: textKeyWords = i18n("No keywords") painter.setOpacity(0.3) painter.setFont(italics) textKeyWords = metrics.elidedText(textKeyWords, Qt.ElideRight, labelWidth) painter.drawText(textRect, Qt.TextWordWrap, textKeyWords) painter.setFont(regular) if rect.height()/(metrics.lineSpacing()+margin) > 3: painter.setOpacity(0.6) textRect = QRect(textRect.left(), textRect.bottom()+margin, labelWidth, metrics.height()) if textRect.bottom()+metrics.height() < rect.bottom(): textLastEdit = index.data(CPE.LASTEDIT) if textLastEdit is None: textLastEdit = i18n("No last edit timestamp") if index.data(CPE.EDITOR) is not None: textLastEdit += " - " + index.data(CPE.EDITOR) if (index.data(CPE.LASTEDIT) is None) and (index.data(CPE.EDITOR) is None): painter.setOpacity(0.3) painter.setFont(italics) textLastEdit = metrics.elidedText(textLastEdit, Qt.ElideRight, labelWidth) painter.drawText(textRect, Qt.TextWordWrap, textLastEdit) painter.setFont(regular) descRect = QRect(textRect.left(), textRect.bottom()+margin, labelWidth, (rect.bottom()-margin) - (textRect.bottom()+margin)) if textRect.bottom()+metrics.height() < rect.bottom(): textRect.setBottom(textRect.bottom()+(margin/2)) textRect.setLeft(textRect.left()-(margin/2)) painter.setOpacity(0.4) painter.drawLine(textRect.bottomLeft(), textRect.bottomRight()) painter.setOpacity(1.0) textDescription = index.data(CPE.DESCRIPTION) if textDescription is None: textDescription = i18n("No description") painter.setOpacity(0.3) painter.setFont(italics) linesTotal = floor(descRect.height()/metrics.lineSpacing()) if linesTotal == 1: textDescription = metrics.elidedText(textDescription, Qt.ElideRight, labelWidth) painter.drawText(descRect, Qt.TextWordWrap, textDescription) else: descRect.setHeight(linesTotal*metrics.lineSpacing()) totalDescHeight = metrics.boundingRect(descRect, Qt.TextWordWrap, textDescription).height() if totalDescHeight>descRect.height(): if totalDescHeight-metrics.lineSpacing()>descRect.height(): painter.setOpacity(0.5) painter.drawText(descRect, Qt.TextWordWrap, textDescription) descRect.setHeight((linesTotal-1)*metrics.lineSpacing()) painter.drawText(descRect, Qt.TextWordWrap, textDescription) descRect.setHeight((linesTotal-2)*metrics.lineSpacing()) painter.drawText(descRect, Qt.TextWordWrap, textDescription) else: painter.setOpacity(0.75) painter.drawText(descRect, Qt.TextWordWrap, textDescription) descRect.setHeight((linesTotal-1)*metrics.lineSpacing()) painter.drawText(descRect, Qt.TextWordWrap, textDescription) else: painter.drawText(descRect, Qt.TextWordWrap, textDescription) painter.setFont(regular) painter.restore() """ This is a Krita docker called 'Comics Manager'. It allows people to create comics project files, load those files, add pages, remove pages, move pages, manage the metadata, and finally export the result. The logic behind this docker is that it is very easy to get lost in a comics project due to the massive amount of files. By having a docker that gives the user quick access to the pages and also allows them to do all of the meta-stuff, like meta data, but also reordering the pages, the chaos of managing the project should take up less time, and more time can be focussed on actual writing and drawing. """ class comics_project_manager_docker(DockWidget): setupDictionary = {} stringName = i18n("Comics Manager") projecturl = None def __init__(self): super().__init__() self.setWindowTitle(self.stringName) # Setup layout: base = QHBoxLayout() widget = QWidget() widget.setLayout(base) baseLayout = QSplitter() base.addWidget(baseLayout) self.setWidget(widget) buttonLayout = QVBoxLayout() buttonBox = QWidget() buttonBox.setLayout(buttonLayout) baseLayout.addWidget(buttonBox) # Comic page list and pages model self.comicPageList = QListView() self.comicPageList.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.comicPageList.setDragEnabled(True) self.comicPageList.setDragDropMode(QAbstractItemView.InternalMove) self.comicPageList.setDefaultDropAction(Qt.MoveAction) self.comicPageList.setAcceptDrops(True) self.comicPageList.setItemDelegate(comic_page_delegate()) self.pagesModel = QStandardItemModel() self.comicPageList.doubleClicked.connect(self.slot_open_page) self.comicPageList.setIconSize(QSize(128, 128)) # self.comicPageList.itemDelegate().closeEditor.connect(self.slot_write_description) self.pagesModel.layoutChanged.connect(self.slot_write_config) self.pagesModel.rowsInserted.connect(self.slot_write_config) self.pagesModel.rowsRemoved.connect(self.slot_write_config) self.pagesModel.rowsMoved.connect(self.slot_write_config) self.comicPageList.setModel(self.pagesModel) pageBox = QWidget() pageBox.setLayout(QVBoxLayout()) zoomSlider = QSlider(Qt.Horizontal, None) zoomSlider.setRange(1, 8) zoomSlider.setValue(4) zoomSlider.setTickInterval(1) zoomSlider.setMinimumWidth(10) zoomSlider.valueChanged.connect(self.slot_scale_thumbnails) self.projectName = Elided_Text_Label() pageBox.layout().addWidget(self.projectName) pageBox.layout().addWidget(zoomSlider) pageBox.layout().addWidget(self.comicPageList) baseLayout.addWidget(pageBox) self.btn_project = QToolButton() self.btn_project.setPopupMode(QToolButton.MenuButtonPopup) self.btn_project.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) menu_project = QMenu() self.action_new_project = QAction(i18n("New Project"), self) self.action_new_project.triggered.connect(self.slot_new_project) self.action_load_project = QAction(i18n("Open Project"), self) self.action_load_project.triggered.connect(self.slot_open_config) menu_project.addAction(self.action_new_project) menu_project.addAction(self.action_load_project) self.btn_project.setMenu(menu_project) self.btn_project.setDefaultAction(self.action_load_project) buttonLayout.addWidget(self.btn_project) # Settings dropdown with actions for the different settings menus. self.btn_settings = QToolButton() self.btn_settings.setPopupMode(QToolButton.MenuButtonPopup) self.btn_settings.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.action_edit_project_settings = QAction(i18n("Project Settings"), self) self.action_edit_project_settings.triggered.connect(self.slot_edit_project_settings) self.action_edit_meta_data = QAction(i18n("Meta Data"), self) self.action_edit_meta_data.triggered.connect(self.slot_edit_meta_data) self.action_edit_export_settings = QAction(i18n("Export Settings"), self) self.action_edit_export_settings.triggered.connect(self.slot_edit_export_settings) menu_settings = QMenu() menu_settings.addAction(self.action_edit_project_settings) menu_settings.addAction(self.action_edit_meta_data) menu_settings.addAction(self.action_edit_export_settings) self.btn_settings.setDefaultAction(self.action_edit_project_settings) self.btn_settings.setMenu(menu_settings) buttonLayout.addWidget(self.btn_settings) self.btn_settings.setDisabled(True) # Add page drop down with different page actions. self.btn_add_page = QToolButton() self.btn_add_page.setPopupMode(QToolButton.MenuButtonPopup) self.btn_add_page.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) self.action_add_page = QAction(i18n("Add Page"), self) self.action_add_page.triggered.connect(self.slot_add_new_page_single) self.action_add_template = QAction(i18n("Add Page From Template"), self) self.action_add_template.triggered.connect(self.slot_add_new_page_from_template) self.action_add_existing = QAction(i18n("Add Existing Pages"), self) self.action_add_existing.triggered.connect(self.slot_add_page_from_url) self.action_remove_selected_page = QAction(i18n("Remove Page"), self) self.action_remove_selected_page.triggered.connect(self.slot_remove_selected_page) self.action_resize_all_pages = QAction(i18n("Batch Resize"), self) self.action_resize_all_pages.triggered.connect(self.slot_batch_resize) self.btn_add_page.setDefaultAction(self.action_add_page) self.action_show_page_viewer = QAction(i18n("View Page In Window"), self) self.action_show_page_viewer.triggered.connect(self.slot_show_page_viewer) self.action_scrape_authors = QAction(i18n("Scrape Author Info"), self) self.action_scrape_authors.setToolTip(i18n("Search for author information in documents and add it to the author list. This doesn't check for duplicates.")) self.action_scrape_authors.triggered.connect(self.slot_scrape_author_list) self.action_scrape_translations = QAction(i18n("Scrape Text for Translation"), self) self.action_scrape_translations.triggered.connect(self.slot_scrape_translations) actionList = [] menu_page = QMenu() actionList.append(self.action_add_page) actionList.append(self.action_add_template) actionList.append(self.action_add_existing) actionList.append(self.action_remove_selected_page) actionList.append(self.action_resize_all_pages) actionList.append(self.action_show_page_viewer) actionList.append(self.action_scrape_authors) actionList.append(self.action_scrape_translations) menu_page.addActions(actionList) self.btn_add_page.setMenu(menu_page) buttonLayout.addWidget(self.btn_add_page) self.btn_add_page.setDisabled(True) self.comicPageList.setContextMenuPolicy(Qt.ActionsContextMenu) self.comicPageList.addActions(actionList) # Export button that... exports. self.btn_export = QPushButton(i18n("Export Comic")) self.btn_export.clicked.connect(self.slot_export) buttonLayout.addWidget(self.btn_export) self.btn_export.setDisabled(True) self.btn_project_url = QPushButton(i18n("Copy Location")) self.btn_project_url.setToolTip(i18n("Copies the path of the project to the clipboard. Useful for quickly copying to a file manager or the like.")) self.btn_project_url.clicked.connect(self.slot_copy_project_url) self.btn_project_url.setDisabled(True) buttonLayout.addWidget(self.btn_project_url) self.page_viewer_dialog = comics_project_page_viewer.comics_project_page_viewer() Application.notifier().imageSaved.connect(self.slot_check_for_page_update) buttonLayout.addItem(QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)) """ Open the config file and load the json file into a dictionary. """ def slot_open_config(self): self.path_to_config = QFileDialog.getOpenFileName(caption=i18n("Please select the json comic config file."), filter=str(i18n("json files") + "(*.json)"))[0] if os.path.exists(self.path_to_config) is True: configFile = open(self.path_to_config, "r", newline="", encoding="utf-16") self.setupDictionary = json.load(configFile) self.projecturl = os.path.dirname(str(self.path_to_config)) configFile.close() self.load_config() """ Further config loading. """ def load_config(self): self.projectName.setMainText(text=str(self.setupDictionary["projectName"])) self.fill_pages() self.btn_settings.setEnabled(True) self.btn_add_page.setEnabled(True) self.btn_export.setEnabled(True) self.btn_project_url.setEnabled(True) """ Fill the pages model with the pages from the pages list. """ def fill_pages(self): self.loadingPages = True self.pagesModel.clear() pagesList = [] if "pages" in self.setupDictionary.keys(): pagesList = self.setupDictionary["pages"] progress = QProgressDialog() progress.setMinimum(0) progress.setMaximum(len(pagesList)) progress.setWindowTitle(i18n("Loading pages...")) for url in pagesList: absurl = os.path.join(self.projecturl, url) if (os.path.exists(absurl)): #page = Application.openDocument(absurl) page = zipfile.ZipFile(absurl, "r") thumbnail = QImage.fromData(page.read("preview.png")) pageItem = QStandardItem() dataList = self.get_description_and_title(page.read("documentinfo.xml")) if (dataList[0].isspace() or len(dataList[0]) < 1): dataList[0] = os.path.basename(url) pageItem.setText(dataList[0].replace("_", " ")) pageItem.setDragEnabled(True) pageItem.setDropEnabled(False) pageItem.setEditable(False) pageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) pageItem.setData(dataList[1], role = CPE.DESCRIPTION) pageItem.setData(url, role = CPE.URL) pageItem.setData(dataList[2], role = CPE.KEYWORDS) pageItem.setData(dataList[3], role = CPE.LASTEDIT) pageItem.setData(dataList[4], role = CPE.EDITOR) pageItem.setToolTip(url) page.close() self.pagesModel.appendRow(pageItem) progress.setValue(progress.value() + 1) progress.setValue(len(pagesList)) self.loadingPages = False """ Function that is triggered by the zoomSlider Resizes the thumbnails. """ def slot_scale_thumbnails(self, multiplier=4): self.comicPageList.setIconSize(QSize(multiplier * 32, multiplier * 32)) """ Function that takes the documentinfo.xml and parses it for the title, subject and abstract tags, to get the title and description. @returns a stringlist with the name on 0 and the description on 1. """ def get_description_and_title(self, string): xmlDoc = ET.fromstring(string) calligra = str("{http://www.calligra.org/DTD/document-info}") name = "" if ET.iselement(xmlDoc[0].find(calligra + 'title')): name = xmlDoc[0].find(calligra + 'title').text if name is None: name = " " desc = "" if ET.iselement(xmlDoc[0].find(calligra + 'subject')): desc = xmlDoc[0].find(calligra + 'subject').text if desc is None or desc.isspace() or len(desc) < 1: if ET.iselement(xmlDoc[0].find(calligra + 'abstract')): desc = xmlDoc[0].find(calligra + 'abstract').text if desc is not None: if desc.startswith(""): desc = desc[:-len("]]>")] keywords = "" if ET.iselement(xmlDoc[0].find(calligra + 'keyword')): keywords = xmlDoc[0].find(calligra + 'keyword').text date = "" if ET.iselement(xmlDoc[0].find(calligra + 'date')): date = xmlDoc[0].find(calligra + 'date').text author = [] if ET.iselement(xmlDoc[1].find(calligra + 'creator-first-name')): string = xmlDoc[1].find(calligra + 'creator-first-name').text if string is not None: author.append(string) if ET.iselement(xmlDoc[1].find(calligra + 'creator-last-name')): string = xmlDoc[1].find(calligra + 'creator-last-name').text if string is not None: author.append(string) if ET.iselement(xmlDoc[1].find(calligra + 'full-name')): string = xmlDoc[1].find(calligra + 'full-name').text if string is not None: author.append(string) return [name, desc, keywords, date, " ".join(author)] """ Scrapes authors from the author data in the document info and puts them into the author list. Doesn't check for duplicates. """ def slot_scrape_author_list(self): listOfAuthors = [] if "authorList" in self.setupDictionary.keys(): listOfAuthors = self.setupDictionary["authorList"] if "pages" in self.setupDictionary.keys(): for relurl in self.setupDictionary["pages"]: absurl = os.path.join(self.projecturl, relurl) page = zipfile.ZipFile(absurl, "r") xmlDoc = ET.fromstring(page.read("documentinfo.xml")) calligra = str("{http://www.calligra.org/DTD/document-info}") authorelem = xmlDoc.find(calligra + 'author') author = {} if ET.iselement(authorelem.find(calligra + 'full-name')): author["nickname"] = str(authorelem.find(calligra + 'full-name').text) if ET.iselement(authorelem.find(calligra + 'creator-first-name')): author["first-name"] = str(authorelem.find(calligra + 'creator-first-name').text) if ET.iselement(authorelem.find(calligra + 'initial')): author["initials"] = str(authorelem.find(calligra + 'initial').text) if ET.iselement(authorelem.find(calligra + 'creator-last-name')): author["last-name"] = str(authorelem.find(calligra + 'creator-last-name').text) if ET.iselement(authorelem.find(calligra + 'email')): author["email"] = str(authorelem.find(calligra + 'email').text) if ET.iselement(authorelem.find(calligra + 'contact')): contact = authorelem.find(calligra + 'contact') contactMode = contact.get("type") if contactMode == "email": author["email"] = str(contact.text) if contactMode == "homepage": author["homepage"] = str(contact.text) if ET.iselement(authorelem.find(calligra + 'position')): author["role"] = str(authorelem.find(calligra + 'position').text) listOfAuthors.append(author) page.close() self.setupDictionary["authorList"] = listOfAuthors """ Edit the general project settings like the project name, concept, pages location, export location, template location, metadata """ def slot_edit_project_settings(self): dialog = comics_project_settings_dialog.comics_project_details_editor(self.projecturl) dialog.setConfig(self.setupDictionary, self.projecturl) if dialog.exec_() == QDialog.Accepted: self.setupDictionary = dialog.getConfig(self.setupDictionary) self.slot_write_config() self.projectName.setMainText(str(self.setupDictionary["projectName"])) """ This allows users to select existing pages and add them to the pages list. The pages are currently not copied to the pages folder. Useful for existing projects. """ def slot_add_page_from_url(self): # get the pages. urlList = QFileDialog.getOpenFileNames(caption=i18n("Which existing pages to add?"), directory=self.projecturl, filter=str(i18n("Krita files") + "(*.kra)"))[0] # get the existing pages list. pagesList = [] if "pages" in self.setupDictionary.keys(): pagesList = self.setupDictionary["pages"] # And add each url in the url list to the pages list and the model. for url in urlList: if self.projecturl not in urlList: newUrl = os.path.join(self.projecturl, self.setupDictionary["pagesLocation"], os.path.basename(url)) shutil.move(url, newUrl) url = newUrl relative = os.path.relpath(url, self.projecturl) if url not in pagesList: page = zipfile.ZipFile(url, "r") thumbnail = QImage.fromData(page.read("preview.png")) dataList = self.get_description_and_title(page.read("documentinfo.xml")) if (dataList[0].isspace() or len(dataList[0]) < 1): dataList[0] = os.path.basename(url) newPageItem = QStandardItem() newPageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) newPageItem.setDragEnabled(True) newPageItem.setDropEnabled(False) newPageItem.setEditable(False) newPageItem.setText(dataList[0].replace("_", " ")) newPageItem.setData(dataList[1], role = CPE.DESCRIPTION) newPageItem.setData(relative, role = CPE.URL) newPageItem.setData(dataList[2], role = CPE.KEYWORDS) newPageItem.setData(dataList[3], role = CPE.LASTEDIT) newPageItem.setData(dataList[4], role = CPE.EDITOR) newPageItem.setToolTip(relative) page.close() self.pagesModel.appendRow(newPageItem) """ Remove the selected page from the list of pages. This does not remove it from disk(far too dangerous). """ def slot_remove_selected_page(self): index = self.comicPageList.currentIndex() self.pagesModel.removeRow(index.row()) """ This function adds a new page from the default template. If there's no default template, or the file does not exist, it will show the create/import template dialog. It will remember the selected item as the default template. """ def slot_add_new_page_single(self): templateUrl = "templatepage" templateExists = False if "singlePageTemplate" in self.setupDictionary.keys(): templateUrl = self.setupDictionary["singlePageTemplate"] if os.path.exists(os.path.join(self.projecturl, templateUrl)): templateExists = True if templateExists is False: if "templateLocation" not in self.setupDictionary.keys(): self.setupDictionary["templateLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where are the templates located?"), options=QFileDialog.ShowDirsOnly), self.projecturl) templateDir = os.path.join(self.projecturl, self.setupDictionary["templateLocation"]) template = comics_template_dialog.comics_template_dialog(templateDir) if template.exec_() == QDialog.Accepted: templateUrl = os.path.relpath(template.url(), self.projecturl) self.setupDictionary["singlePageTemplate"] = templateUrl if os.path.exists(os.path.join(self.projecturl, templateUrl)): self.add_new_page(templateUrl) """ This function always asks for a template showing the new template window. This allows users to have multiple different templates created for back covers, spreads, other and have them accesible, while still having the convenience of a singular "add page" that adds a default. """ def slot_add_new_page_from_template(self): if "templateLocation" not in self.setupDictionary.keys(): self.setupDictionary["templateLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where are the templates located?"), options=QFileDialog.ShowDirsOnly), self.projecturl) templateDir = os.path.join(self.projecturl, self.setupDictionary["templateLocation"]) template = comics_template_dialog.comics_template_dialog(templateDir) if template.exec_() == QDialog.Accepted: templateUrl = os.path.relpath(template.url(), self.projecturl) self.add_new_page(templateUrl) """ This is the actual function that adds the template using the template url. It will attempt to name the new page projectName+number. """ def add_new_page(self, templateUrl): # check for page list and or location. pagesList = [] if "pages" in self.setupDictionary.keys(): pagesList = self.setupDictionary["pages"] if not "pageNumber" in self.setupDictionary.keys(): self.setupDictionary['pageNumber'] = 0 if (str(self.setupDictionary["pagesLocation"]).isspace()): self.setupDictionary["pagesLocation"] = os.path.relpath(QFileDialog.getExistingDirectory(caption=i18n("Where should the pages go?"), options=QFileDialog.ShowDirsOnly), self.projecturl) # Search for the possible name. extraUnderscore = str() if str(self.setupDictionary["projectName"])[-1].isdigit(): extraUnderscore = "_" self.setupDictionary['pageNumber'] += 1 pageName = str(self.setupDictionary["projectName"]).replace(" ", "_") + extraUnderscore + str(format(self.setupDictionary['pageNumber'], "03d")) url = os.path.join(str(self.setupDictionary["pagesLocation"]), pageName + ".kra") # open the page by opening the template and resaving it, or just opening it. absoluteUrl = os.path.join(self.projecturl, url) if (os.path.exists(absoluteUrl)): newPage = Application.openDocument(absoluteUrl) else: booltemplateExists = os.path.exists(os.path.join(self.projecturl, templateUrl)) if booltemplateExists is False: templateUrl = os.path.relpath(QFileDialog.getOpenFileName(caption=i18n("Which image should be the basis the new page?"), directory=self.projecturl, filter=str(i18n("Krita files") + "(*.kra)"))[0], self.projecturl) newPage = Application.openDocument(os.path.join(self.projecturl, templateUrl)) newPage.waitForDone() newPage.setFileName(absoluteUrl) newPage.setName(pageName.replace("_", " ")) newPage.save() newPage.waitForDone() # Get out the extra data for the standard item. newPageItem = QStandardItem() newPageItem.setIcon(QIcon(QPixmap.fromImage(newPage.thumbnail(256, 256)))) newPageItem.setDragEnabled(True) newPageItem.setDropEnabled(False) newPageItem.setEditable(False) newPageItem.setText(pageName.replace("_", " ")) newPageItem.setData("", role = CPE.DESCRIPTION) newPageItem.setData(url, role = CPE.URL) newPageItem.setData("", role = CPE.KEYWORDS) newPageItem.setData("", role = CPE.LASTEDIT) newPageItem.setData("", role = CPE.EDITOR) newPageItem.setToolTip(url) # close page document. while os.path.exists(absoluteUrl) is False: qApp.processEvents() newPage.close() # add item to page. self.pagesModel.appendRow(newPageItem) """ Write to the json configuratin file. This also checks the current state of the pages list. """ def slot_write_config(self): # Don't load when the pages are still being loaded, otherwise we'll be overwriting our own pages list. if (self.loadingPages is False): print("CPMT: writing comic configuration...") # Generate a pages list from the pagesmodel. pagesList = [] for i in range(self.pagesModel.rowCount()): index = self.pagesModel.index(i, 0) url = str(self.pagesModel.data(index, role=CPE.URL)) if url not in pagesList: pagesList.append(url) self.setupDictionary["pages"] = pagesList # Save to our json file. configFile = open(self.path_to_config, "w", newline="", encoding="utf-16") json.dump(self.setupDictionary, configFile, indent=4, sort_keys=True, ensure_ascii=False) configFile.close() print("CPMT: done") """ Open a page in the pagesmodel in Krita. """ def slot_open_page(self, index): if index.column() is 0: # Get the absolute url from the relative one in the pages model. absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(index, role=CPE.URL))) # Make sure the page exists. if os.path.exists(absoluteUrl): page = Application.openDocument(absoluteUrl) # Set the title to the filename if it was empty. It looks a bit neater. if page.name().isspace or len(page.name()) < 1: page.setName(str(self.pagesModel.data(index, role=Qt.DisplayRole)).replace("_", " ")) # Add views for the document so the user can use it. Application.activeWindow().addView(page) Application.setActiveDocument(page) else: print("CPMT: The page cannot be opened because the file doesn't exist:", absoluteUrl) """ Call up the metadata editor dialog. Only when the dialog is "Accepted" will the metadata be saved. """ def slot_edit_meta_data(self): dialog = comics_metadata_dialog.comic_meta_data_editor() dialog.setConfig(self.setupDictionary) if (dialog.exec_() == QDialog.Accepted): self.setupDictionary = dialog.getConfig(self.setupDictionary) self.slot_write_config() """ An attempt at making the description editable from the comic pages list. It is currently not working because ZipFile has no overwrite mechanism, and I don't have the energy to write one yet. """ def slot_write_description(self, index): for row in range(self.pagesModel.rowCount()): index = self.pagesModel.index(row, 1) indexUrl = self.pagesModel.index(row, 0) absoluteUrl = os.path.join(self.projecturl, str(self.pagesModel.data(indexUrl, role=CPE.URL))) page = zipfile.ZipFile(absoluteUrl, "a") xmlDoc = ET.ElementTree() ET.register_namespace("", "http://www.calligra.org/DTD/document-info") location = os.path.join(self.projecturl, "documentinfo.xml") xmlDoc.parse(location) xmlroot = ET.fromstring(page.read("documentinfo.xml")) calligra = "{http://www.calligra.org/DTD/document-info}" aboutelem = xmlroot.find(calligra + 'about') if ET.iselement(aboutelem.find(calligra + 'subject')): desc = aboutelem.find(calligra + 'subject') desc.text = self.pagesModel.data(index, role=Qt.EditRole) xmlstring = ET.tostring(xmlroot, encoding='unicode', method='xml', short_empty_elements=False) page.writestr(zinfo_or_arcname="documentinfo.xml", data=xmlstring) for document in Application.documents(): if str(document.fileName()) == str(absoluteUrl): document.setDocumentInfo(xmlstring) page.close() """ Calls up the export settings dialog. Only when accepted will the configuration be written. """ def slot_edit_export_settings(self): dialog = comics_export_dialog.comic_export_setting_dialog() dialog.setConfig(self.setupDictionary) if (dialog.exec_() == QDialog.Accepted): self.setupDictionary = dialog.getConfig(self.setupDictionary) self.slot_write_config() """ Export the comic. Won't work without export settings set. """ def slot_export(self): exporter = comics_exporter.comicsExporter() exporter.set_config(self.setupDictionary, self.projecturl) exportSuccess = exporter.export() if exportSuccess: print("CPMT: Export success! The files have been written to the export folder!") QMessageBox.information(self, "Export success!", "The files have been written to the export folder!", QMessageBox.Ok) """ Calls up the comics project setup wizard so users can create a new json file with the basic information. """ def slot_new_project(self): setup = comics_project_setup_wizard.ComicsProjectSetupWizard() setup.showDialog() """ This is triggered by any document save. It checks if the given url in in the pages list, and if so, updates the appropriate page thumbnail. This helps with the management of the pages, because the user will be able to see the thumbnails as a todo for the whole comic, giving a good overview over whether they still need to ink, color or the like for a given page, and it thus also rewards the user whenever they save. """ def slot_check_for_page_update(self, url): if "pages" in self.setupDictionary.keys(): relUrl = os.path.relpath(url, self.projecturl) if relUrl in self.setupDictionary["pages"]: index = self.pagesModel.index(self.setupDictionary["pages"].index(relUrl), 0) if index.isValid(): pageItem = self.pagesModel.itemFromIndex(index) page = zipfile.ZipFile(url, "r") dataList = self.get_description_and_title(page.read("documentinfo.xml")) if (dataList[0].isspace() or len(dataList[0]) < 1): dataList[0] = os.path.basename(url) thumbnail = QImage.fromData(page.read("preview.png")) pageItem.setIcon(QIcon(QPixmap.fromImage(thumbnail))) pageItem.setText(dataList[0]) pageItem.setData(dataList[1], role = CPE.DESCRIPTION) pageItem.setData(url, role = CPE.URL) pageItem.setData(dataList[2], role = CPE.KEYWORDS) pageItem.setData(dataList[3], role = CPE.LASTEDIT) pageItem.setData(dataList[4], role = CPE.EDITOR) self.pagesModel.setItem(index.row(), index.column(), pageItem) """ Resize all the pages in the pages list. It will show a dialog with the options for resizing. Then, it will try to pop up a progress dialog while resizing. The progress dialog shows the remaining time and pages. """ def slot_batch_resize(self): dialog = QDialog() dialog.setWindowTitle(i18n("Risize all pages.")) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(dialog.accept) buttons.rejected.connect(dialog.reject) sizesBox = comics_export_dialog.comic_export_resize_widget("Scale", batch=True, fileType=False) exporterSizes = comics_exporter.sizesCalculator() dialog.setLayout(QVBoxLayout()) dialog.layout().addWidget(sizesBox) dialog.layout().addWidget(buttons) if dialog.exec_() == QDialog.Accepted: progress = QProgressDialog(i18n("Resizing pages..."), str(), 0, len(self.setupDictionary["pages"])) progress.setWindowTitle(i18n("Resizing pages.")) progress.setCancelButton(None) timer = QElapsedTimer() timer.start() config = {} config = sizesBox.get_config(config) for p in range(len(self.setupDictionary["pages"])): absoluteUrl = os.path.join(self.projecturl, self.setupDictionary["pages"][p]) progress.setValue(p) timePassed = timer.elapsed() if (p > 0): timeEstimated = (len(self.setupDictionary["pages"]) - p) * (timePassed / p) passedString = str(int(timePassed / 60000)) + ":" + format(int(timePassed / 1000), "02d") + ":" + format(timePassed % 1000, "03d") estimatedString = str(int(timeEstimated / 60000)) + ":" + format(int(timeEstimated / 1000), "02d") + ":" + format(int(timeEstimated % 1000), "03d") progress.setLabelText(str(i18n("{pages} of {pagesTotal} done. \nTime passed: {passedString}:\n Estimated:{estimated}")).format(pages=p, pagesTotal=len(self.setupDictionary["pages"]), passedString=passedString, estimated=estimatedString)) qApp.processEvents() if os.path.exists(absoluteUrl): doc = Application.openDocument(absoluteUrl) listScales = exporterSizes.get_scale_from_resize_config(config["Scale"], [doc.width(), doc.height(), doc.resolution(), doc.resolution()]) doc.scaleImage(listScales[0], listScales[1], listScales[2], listScales[3], "bicubic") doc.waitForDone() doc.save() doc.waitForDone() doc.close() def slot_show_page_viewer(self): index = int(self.comicPageList.currentIndex().row()) self.page_viewer_dialog.load_comic(self.path_to_config) self.page_viewer_dialog.go_to_page_index(index) self.page_viewer_dialog.show() """ Function to copy the current project location into the clipboard. This is useful for users because they'll be able to use that url to quickly move to the project location in outside applications. """ def slot_copy_project_url(self): if self.projecturl is not None: clipboard = qApp.clipboard() clipboard.setText(str(self.projecturl)) """ Scrape text files with the textlayer keys for text, and put those in a POT file. This makes it possible to handle translations. """ def slot_scrape_translations(self): translationFolder = self.setupDictionary.get("translationLocation", "translations") fullTranslationPath = os.path.join(self.projecturl, translationFolder) os.makedirs(fullTranslationPath, exist_ok=True) textLayersToSearch = self.setupDictionary.get("textLayerNames", ["text"]) scraper = comics_project_translation_scraper.translation_scraper(self.projecturl, translationFolder, textLayersToSearch, self.setupDictionary["projectName"]) # Run text scraper. language = self.setupDictionary.get("language", "en") metadata = {} metadata["title"] = self.setupDictionary.get("title", "") metadata["summary"] = self.setupDictionary.get("summary", "") metadata["keywords"] = ", ".join(self.setupDictionary.get("otherKeywords", [""])) metadata["transnotes"] = self.setupDictionary.get("translatorHeader", "Translator's Notes") scraper.start(self.setupDictionary["pages"], language, metadata) - QMessageBox.information(self, i18n("Scraping success!"), i18n("POT file has been written to:")+fullTranslationPath, QMessageBox.Ok) + QMessageBox.information(self, i18n("Scraping success!"), i18n("POT file has been written to: ")+fullTranslationPath, QMessageBox.Ok) """ This is required by the dockwidget class, otherwise unused. """ def canvasChanged(self, canvas): pass """ Add docker to program """ Application.addDockWidgetFactory(DockWidgetFactory("comics_project_manager_docker", DockWidgetFactoryBase.DockRight, comics_project_manager_docker)) diff --git a/plugins/python/comics_project_management_tools/exporters/CPMT_ACBF_XML_Exporter.py b/plugins/python/comics_project_management_tools/exporters/CPMT_ACBF_XML_Exporter.py index 87b48fe60b..fd386283eb 100644 --- a/plugins/python/comics_project_management_tools/exporters/CPMT_ACBF_XML_Exporter.py +++ b/plugins/python/comics_project_management_tools/exporters/CPMT_ACBF_XML_Exporter.py @@ -1,797 +1,797 @@ """ 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 . """ """ Write the Advanced Comic Book Data xml file. http://acbf.wikia.com/wiki/ACBF_Specifications """ import os import re from PyQt5.QtCore import QDate, Qt, QPointF, QByteArray, QBuffer from PyQt5.QtGui import QImage, QColor, QFont, QRawFont from PyQt5.QtXml import QDomDocument, QDomElement, QDomText, QDomNodeList from . import CPMT_po_parser as po_parser def write_xml(configDictionary = {}, pageData = [], pagesLocationList = [], locationBasic = str(), locationStandAlone = str(), projectUrl = str()): acbfGenreList = ["science_fiction", "fantasy", "adventure", "horror", "mystery", "crime", "military", "real_life", "superhero", "humor", "western", "manga", "politics", "caricature", "sports", "history", "biography", "education", "computer", "religion", "romance", "children", "non-fiction", "adult", "alternative", "other", "artbook"] acbfAuthorRolesList = ["Writer", "Adapter", "Artist", "Penciller", "Inker", "Colorist", "Letterer", "Cover Artist", "Photographer", "Editor", "Assistant Editor", "Translator", "Other", "Designer"] document = QDomDocument() root = document.createElement("ACBF") root.setAttribute("xmlns", "http://www.fictionbook-lib.org/xml/acbf/1.0") document.appendChild(root) emphasisStyle = {} strongStyle = {} if "acbfStyles" in configDictionary.keys(): stylesDictionary = configDictionary.get("acbfStyles", {}) emphasisStyle = stylesDictionary.get("emphasis", {}) strongStyle = stylesDictionary.get("strong", {}) styleString = "\n" tabs = " " for key in sorted(stylesDictionary.keys()): style = stylesDictionary.get(key, {}) if key == "emphasis" or key == "strong": styleClass = key+" {\n" elif key == "speech": styleClass = "text-area {\n" elif key == "general": styleClass = "* {\n" elif key == "inverted": styleClass = "text-area[inverted=\"True\"] {\n" else: styleClass = "text-area[type=\""+key+"\"] {\n" styleString += tabs+styleClass if "color" in style.keys(): styleString += tabs+tabs+"color:"+style["color"]+";\n" if "font" in style.keys(): fonts = style["font"] genericfont = style.get("genericfont", "sans-serif") if isinstance(fonts, list): styleString += tabs+tabs+"font-family:\""+str("\", \"").join(fonts)+"\", "+genericfont+";\n" else: styleString += tabs+tabs+"font-family:\""+fonts+"\", "+genericfont+";\n" if "bold" in style.keys(): if style["bold"]: styleString += tabs+tabs+"font-weight: bold;\n" if "ital" in style.keys(): if style["ital"]: styleString += tabs+tabs+"font-style: italic;\n" else: styleString += tabs+tabs+"font-style: normal;\n" styleString += tabs+"}\n" style = document.createElement("style") style.setAttribute("type", "text/css") style.appendChild(document.createTextNode(styleString)) root.appendChild(style) meta = document.createElement("meta-data") translationFolder = configDictionary.get("translationLocation", "translations") fullTranslationPath = os.path.join(projectUrl, translationFolder) poParser = po_parser.po_file_parser(fullTranslationPath, True) bookInfo = document.createElement("book-info") if "authorList" in configDictionary.keys(): for authorE in range(len(configDictionary["authorList"])): author = document.createElement("author") authorDict = configDictionary["authorList"][authorE] if "first-name" in authorDict.keys(): authorN = document.createElement("first-name") authorN.appendChild(document.createTextNode(str(authorDict["first-name"]))) author.appendChild(authorN) if "last-name" in authorDict.keys(): authorN = document.createElement("last-name") authorN.appendChild(document.createTextNode(str(authorDict["last-name"]))) author.appendChild(authorN) if "initials" in authorDict.keys(): authorN = document.createElement("middle-name") authorN.appendChild(document.createTextNode(str(authorDict["initials"]))) author.appendChild(authorN) if "nickname" in authorDict.keys(): authorN = document.createElement("nickname") authorN.appendChild(document.createTextNode(str(authorDict["nickname"]))) author.appendChild(authorN) if "homepage" in authorDict.keys(): authorN = document.createElement("home-page") authorN.appendChild(document.createTextNode(str(authorDict["homepage"]))) author.appendChild(authorN) if "email" in authorDict.keys(): authorN = document.createElement("email") authorN.appendChild(document.createTextNode(str(authorDict["email"]))) author.appendChild(authorN) if "role" in authorDict.keys(): if str(authorDict["role"]).title() in acbfAuthorRolesList: author.setAttribute("activity", str(authorDict["role"])) if "language" in authorDict.keys(): author.setAttribute("lang", str(authorDict["language"])) bookInfo.appendChild(author) bookTitle = document.createElement("book-title") if "title" in configDictionary.keys(): bookTitle.appendChild(document.createTextNode(str(configDictionary["title"]))) else: bookTitle.appendChild(document.createTextNode(str("Comic with no Name"))) bookInfo.appendChild(bookTitle) extraGenres = [] if "genre" in configDictionary.keys(): genreListConf = configDictionary["genre"] if isinstance(configDictionary["genre"], dict): genreListConf = configDictionary["genre"].keys() for genre in genreListConf: genreModified = str(genre).lower() genreModified.replace(" ", "_") if genreModified in acbfGenreList: bookGenre = document.createElement("genre") bookGenre.appendChild(document.createTextNode(str(genreModified))) if isinstance(configDictionary["genre"], dict): genreMatch = configDictionary["genre"][genreModified] if genreMatch>0: bookGenre.setAttribute("match", str(genreMatch)) bookInfo.appendChild(bookGenre) else: extraGenres.append(genre) annotation = document.createElement("annotation") if "summary" in configDictionary.keys(): paragraphList = str(configDictionary["summary"]).split("\n") for para in paragraphList: p = document.createElement("p") p.appendChild(document.createTextNode(str(para))) annotation.appendChild(p) else: p = document.createElement("p") p.appendChild(document.createTextNode(str("There was no summary upon generation of this file."))) annotation.appendChild(p) bookInfo.appendChild(annotation) if "characters" in configDictionary.keys(): character = document.createElement("characters") for name in configDictionary["characters"]: char = document.createElement("name") char.appendChild(document.createTextNode(str(name))) character.appendChild(char) bookInfo.appendChild(character) keywords = document.createElement("keywords") stringKeywordsList = [] for key in extraGenres: stringKeywordsList.append(str(key)) if "otherKeywords" in configDictionary.keys(): for key in configDictionary["otherKeywords"]: stringKeywordsList.append(str(key)) if "format" in configDictionary.keys(): for key in configDictionary["format"]: stringKeywordsList.append(str(key)) keywords.appendChild(document.createTextNode(", ".join(stringKeywordsList))) bookInfo.appendChild(keywords) coverpageurl = "" coverpage = document.createElement("coverpage") if "pages" in configDictionary.keys(): if "cover" in configDictionary.keys(): pageList = [] pageList = configDictionary["pages"] coverNumber = max([pageList.index(configDictionary["cover"]), 0]) image = document.createElement("image") if len(pagesLocationList) >= coverNumber: coverpageurl = pagesLocationList[coverNumber] image.setAttribute("href", os.path.basename(coverpageurl)) coverpage.appendChild(image) bookInfo.appendChild(coverpage) if "language" in configDictionary.keys(): language = document.createElement("languages") textlayer = document.createElement("text-layer") textlayer.setAttribute("lang", configDictionary["language"]) textlayer.setAttribute("show", "False") textlayerNative = document.createElement("text-layer") textlayerNative.setAttribute("lang", configDictionary["language"]) textlayerNative.setAttribute("show", "True") language.appendChild(textlayer) language.appendChild(textlayerNative) translationComments = {} for lang in poParser.get_translation_list(): textlayer = document.createElement("text-layer") textlayer.setAttribute("lang", lang) textlayer.setAttribute("show", "True") language.appendChild(textlayer) translationComments[lang] = [] translation = poParser.get_entry_for_key("@meta-title "+configDictionary["title"], lang).get("trans", None) if translation is not None: bookTitleTr = document.createElement("book-title") bookTitleTr.setAttribute("lang", lang) bookTitleTr.appendChild(document.createTextNode(translation)) bookInfo.appendChild(bookTitleTr) translation = poParser.get_entry_for_key("@meta-summary "+configDictionary["summary"], lang).get("trans", None) if translation is not None: annotationTr = document.createElement("annotation") annotationTr.setAttribute("lang", lang) annotationTr.appendChild(document.createTextNode(translation)) bookInfo.appendChild(annotationTr) translation = poParser.get_entry_for_key("@meta-keywords "+", ".join(configDictionary["otherKeywords"]), lang).get("trans", None) if translation is not None: keywordsTr = document.createElement("keywords") keywordsTr.setAttribute("lang", lang) keywordsTr.appendChild(document.createTextNode(translation)) bookInfo.appendChild(keywordsTr) bookInfo.appendChild(language) bookTitle.setAttribute("lang", configDictionary["language"]) annotation.setAttribute("lang", configDictionary["language"]) keywords.setAttribute("lang", configDictionary["language"]) if "databaseReference" in configDictionary.keys(): database = document.createElement("databaseref") dbRef = configDictionary["databaseReference"] database.setAttribute("dbname", dbRef.get("name", "")) if "type" in dbRef.keys(): database.setAttribute("type", dbRef["type"]) database.appendChild(document.createTextNode(dbRef.get("entry", ""))) bookInfo.appendChild(database) if "seriesName" in configDictionary.keys(): sequence = document.createElement("sequence") sequence.setAttribute("title", configDictionary["seriesName"]) if "seriesVolume" in configDictionary.keys(): sequence.setAttribute("volume", str(configDictionary["seriesVolume"])) if "seriesNumber" in configDictionary.keys(): sequence.appendChild(document.createTextNode(str(configDictionary["seriesNumber"]))) else: sequence.appendChild(document.createTextNode(str(0))) bookInfo.appendChild(sequence) contentrating = document.createElement("content-rating") if "rating" in configDictionary.keys(): contentrating.appendChild(document.createTextNode(str(configDictionary["rating"]))) else: contentrating.appendChild(document.createTextNode(str("Unrated."))) if "ratingSystem" in configDictionary.keys(): contentrating.setAttribute("type", configDictionary["ratingSystem"]) bookInfo.appendChild(contentrating) if "readingDirection" in configDictionary.keys(): readingDirection = document.createElement("reading-direction") if configDictionary["readingDirection"] is "rightToLeft": readingDirection.appendChild(document.createTextNode(str("RTL"))) else: readingDirection.appendChild(document.createTextNode(str("LTR"))) bookInfo.appendChild(readingDirection) meta.appendChild(bookInfo) publisherInfo = document.createElement("publish-info") if "publisherName" in configDictionary.keys(): publisherName = document.createElement("publisher") publisherName.appendChild(document.createTextNode(str(configDictionary["publisherName"]))) publisherInfo.appendChild(publisherName) if "publishingDate" in configDictionary.keys(): publishingDate = document.createElement("publish-date") publishingDate.setAttribute("value", configDictionary["publishingDate"]) publishingDate.appendChild(document.createTextNode(QDate.fromString(configDictionary["publishingDate"], Qt.ISODate).toString(Qt.SystemLocaleLongDate))) publisherInfo.appendChild(publishingDate) if "publisherCity" in configDictionary.keys(): publishCity = document.createElement("city") publishCity.appendChild(document.createTextNode(str(configDictionary["publisherCity"]))) publisherInfo.appendChild(publishCity) if "isbn-number" in configDictionary.keys(): publishISBN = document.createElement("isbn") publishISBN.appendChild(document.createTextNode(str(configDictionary["isbn-number"]))) publisherInfo.appendChild(publishISBN) license = str(configDictionary.get("license", "")) if license.isspace() is False and len(license) > 0: publishLicense = document.createElement("license") publishLicense.appendChild(document.createTextNode(license)) publisherInfo.appendChild(publishLicense) meta.appendChild(publisherInfo) documentInfo = document.createElement("document-info") # TODO: ACBF apparantly uses first/middle/last/nick/email/homepage for the document auhtor too... # The following code compensates for me not understanding this initially. if "acbfAuthor" in configDictionary.keys(): if isinstance(configDictionary["acbfAuthor"], list): for e in configDictionary["acbfAuthor"]: acbfAuthor = document.createElement("author") authorDict = e if "first-name" in authorDict.keys(): authorN = document.createElement("first-name") authorN.appendChild(document.createTextNode(str(authorDict["first-name"]))) acbfAuthor.appendChild(authorN) if "last-name" in authorDict.keys(): authorN = document.createElement("last-name") authorN.appendChild(document.createTextNode(str(authorDict["last-name"]))) acbfAuthor.appendChild(authorN) if "initials" in authorDict.keys(): authorN = document.createElement("middle-name") authorN.appendChild(document.createTextNode(str(authorDict["initials"]))) acbfAuthor.appendChild(authorN) if "nickname" in authorDict.keys(): authorN = document.createElement("nickname") authorN.appendChild(document.createTextNode(str(authorDict["nickname"]))) acbfAuthor.appendChild(authorN) if "homepage" in authorDict.keys(): authorN = document.createElement("home-page") authorN.appendChild(document.createTextNode(str(authorDict["homepage"]))) acbfAuthor.appendChild(authorN) if "email" in authorDict.keys(): authorN = document.createElement("email") authorN.appendChild(document.createTextNode(str(authorDict["email"]))) acbfAuthor.appendChild(authorN) if "language" in authorDict.keys(): acbfAuthor.setAttribute("lang", str(authorDict["language"])) documentInfo.appendChild(acbfAuthor) else: acbfAuthor = document.createElement("author") acbfAuthorNick = document.createElement("nickname") acbfAuthorNick.appendChild(document.createTextNode(str(configDictionary["acbfAuthor"]))) acbfAuthor.appendChild(acbfAuthorNick) documentInfo.appendChild(acbfAuthor) else: acbfAuthor = document.createElement("author") acbfAuthorNick = document.createElement("nickname") acbfAuthorNick.appendChild(document.createTextNode(str("Anon"))) acbfAuthor.appendChild(acbfAuthorNick) documentInfo.appendChild(acbfAuthor) acbfDate = document.createElement("creation-date") now = QDate.currentDate() acbfDate.setAttribute("value", now.toString(Qt.ISODate)) acbfDate.appendChild(document.createTextNode(str(now.toString(Qt.SystemLocaleLongDate)))) documentInfo.appendChild(acbfDate) if "acbfSource" in configDictionary.keys(): acbfSource = document.createElement("source") acbfSourceP = document.createElement("p") acbfSourceP.appendChild(document.createTextNode(str(configDictionary["acbfSource"]))) acbfSource.appendChild(acbfSourceP) documentInfo.appendChild(acbfSource) if "acbfID" in configDictionary.keys(): acbfID = document.createElement("id") acbfID.appendChild(document.createTextNode(str(configDictionary["acbfID"]))) documentInfo.appendChild(acbfID) if "acbfVersion" in configDictionary.keys(): acbfVersion = document.createElement("version") acbfVersion.appendChild(document.createTextNode(str(configDictionary["acbfVersion"]))) documentInfo.appendChild(acbfVersion) if "acbfHistory" in configDictionary.keys(): acbfHistory = document.createElement("history") for h in configDictionary["acbfHistory"]: p = document.createElement("p") p.appendChild(document.createTextNode(str(h))) acbfHistory.appendChild(p) documentInfo.appendChild(acbfHistory) meta.appendChild(documentInfo) root.appendChild(meta) body = document.createElement("body") references = document.createElement("references") def figure_out_type(svg = QDomElement()): type = None skipList = ["speech", "emphasis", "strong", "inverted", "general"] if svg.attribute("text-anchor") == "middle" or svg.attribute("text-align") == "center": if "acbfStyles" in configDictionary.keys(): stylesDictionary = configDictionary.get("acbfStyles", {}) for key in stylesDictionary.keys(): if key not in skipList: style = stylesDictionary.get(key, {}) font = style.get("font", "") if isinstance(fonts, list): if svg.attribute("family") in font: type = key elif svg.attribute("family") == font: type = key else: type = None elif svg.attribute("text-align") == "justified": type = "formal" else: type = "commentary" inverted = None #Figure out whether this is inverted colored text. if svg.hasAttribute("fill"): stylesDictionary = configDictionary.get("acbfStyles", {}) key = stylesDictionary.get("general", {}) regular = QColor(key.get("color", "#000000")) key = stylesDictionary.get("inverted", {}) invertedColor = QColor(key.get("color", "#FFFFFF")) textColor = QColor(svg.attribute("fill")) # Proceed to get luma for the three colors. lightnessR = (0.21 * regular.redF()) + (0.72 * regular.greenF()) + (0.07 * regular.blueF()) lightnessI = (0.21 * invertedColor.redF()) + (0.72 * invertedColor.greenF()) + (0.07 * invertedColor.blueF()) lightnessT = (0.21 * textColor.redF()) + (0.72 * textColor.greenF()) + (0.07 * textColor.blueF()) if lightnessI > lightnessR: if lightnessT > (lightnessI+lightnessR)*0.5: inverted = "True" else: if lightnessT < (lightnessI+lightnessR)*0.5: inverted = "True" return [type, inverted] listOfPageColors = [] for p in range(0, len(pagesLocationList)): page = pagesLocationList[p] imageFile = QImage() imageFile.load(page) imageRect = imageFile.rect().adjusted(0, 0, -1, -1) pageColor = findDominantColor([imageFile.pixelColor(imageRect.topLeft()), imageFile.pixelColor(imageRect.topRight()), imageFile.pixelColor(imageRect.bottomRight()), imageFile.pixelColor(imageRect.bottomLeft())]) listOfPageColors.append(pageColor) language = "en" if "language" in configDictionary.keys(): language = configDictionary["language"] textLayer = document.createElement("text-layer") textLayer.setAttribute("lang", language) data = pageData[p] transform = data["transform"] frameList = [] listOfTextColors = [] for v in data["vector"]: boundingBoxText = [] listOfBoundaryColors = [] 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(imageRect.width(), int(newPoint.x() * transform["scaleWidth"]))) y = max(0, min(imageRect.height(), int(newPoint.y() * transform["scaleHeight"]))) listOfBoundaryColors.append(imageFile.pixelColor(x, y)) pointText = str(x) + "," + str(y) boundingBoxText.append(pointText) mainColor = findDominantColor(listOfBoundaryColors) if "text" in v.keys(): textArea = document.createElement("text-area") textArea.setAttribute("points", " ".join(boundingBoxText)) # TODO: Rotate will require proper global transform api as transform info is not written intotext. #textArea.setAttribute("text-rotation", str(v["rotate"])) svg = QDomDocument() svg.setContent(v["text"]) figureOut = figure_out_type(svg.documentElement()) type = figureOut[0] inverted = figureOut[1] paragraph = QDomDocument() paragraph.appendChild(paragraph.createElement("p")) parseTextChildren(paragraph, svg.documentElement(), paragraph.documentElement(), emphasisStyle, strongStyle) textArea.appendChild(paragraph.documentElement()) textArea.setAttribute("bgcolor", mainColor.name()) if type is not None: textArea.setAttribute("type", type) if inverted is not None: textArea.setAttribute("inverted", inverted) textLayer.appendChild(textArea) else: f = {} f["points"] = " ".join(boundingBoxText) frameList.append(f) listOfTextColors.append(mainColor) textLayer.setAttribute("bgcolor", findDominantColor(listOfTextColors).name()) textLayerList = document.createElement("trlist") for lang in poParser.get_translation_list(): textLayerTr = document.createElement("text-layer") textLayerTr.setAttribute("lang", lang) for i in range(len(data["vector"])): d = data["vector"] v = d[i] boundingBoxText = [] 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 = int(newPoint.x() * transform["scaleWidth"]) y = int(newPoint.y() * transform["scaleHeight"]) pointText = str(x) + "," + str(y) boundingBoxText.append(pointText) if "text" in v.keys(): textArea = document.createElement("text-area") textArea.setAttribute("points", " ".join(boundingBoxText)) # TODO: Rotate will require proper global transform api as transform info is not written intotext. #textArea.setAttribute("text-rotation", str(v["rotate"])) svg = QDomDocument() svg.setContent(v["text"]) figureOut = figure_out_type(svg.documentElement()) type = figureOut[0] inverted = figureOut[1] string = re.sub("\<\/*?text.*?\>",'', str(v["text"])) string = re.sub("\s+?", " ", string) translationEntry = poParser.get_entry_for_key(string, lang) string = translationEntry.get("trans", string) svg.setContent(""+string+"") paragraph = QDomDocument() paragraph.appendChild(paragraph.createElement("p")) parseTextChildren(paragraph, svg.documentElement(), paragraph.documentElement(), emphasisStyle, strongStyle) if "translComment" in translationEntry.keys(): key = translationEntry["translComment"] listOfComments = [] listOfComments = translationComments[lang] index = 0 if key in listOfComments: index = listOfComments.index(key)+1 else: listOfComments.append(key) index = len(listOfComments) translationComments[lang] = listOfComments refID = "-".join(["tn", lang, str(index)]) anchor = document.createElement("a") anchor.setAttribute("href", "#"+refID) anchor.appendChild(document.createTextNode("*")) paragraph.documentElement().appendChild(anchor) textArea.appendChild(paragraph.documentElement()) textLayerTr.appendChild(textArea) if type is not None: textArea.setAttribute("type", type) if inverted is not None: textArea.setAttribute("inverted", inverted) textArea.setAttribute("bgcolor", listOfTextColors[i].name()) textLayerTr.setAttribute("bgcolor", findDominantColor(listOfTextColors).name()) textLayerList.appendChild(textLayerTr) if page is not coverpageurl: pg = document.createElement("page") image = document.createElement("image") image.setAttribute("href", os.path.basename(page)) pg.appendChild(image) if "acbf_title" in data["keys"]: title = document.createElement("title") title.setAttribute("lang", language) title.appendChild(document.createTextNode(str(data["title"]))) pg.appendChild(title) for lang in poParser.get_translation_list(): titleTrans = " " titlekey = "@page-title "+str(data["title"]) translationEntry = poParser.get_entry_for_key(titlekey, lang) titleTrans = translationEntry.get("trans", titleTrans) if titleTrans.isspace() is False: titleT = document.createElement("title") titleT.setAttribute("lang", lang) titleT.appendChild(document.createTextNode(titleTrans)) pg.appendChild(titleT) if "acbf_none" in data["keys"]: pg.setAttribute("transition", "none") if "acbf_blend" in data["keys"]: pg.setAttribute("transition", "blend") if "acbf_fade" in data["keys"]: pg.setAttribute("transition", "fade") if "acbf_horizontal" in data["keys"]: pg.setAttribute("transition", "scroll_right") if "acbf_vertical" in data["keys"]: pg.setAttribute("transition", "scroll_down") for f in frameList: frame = document.createElement("frame") frame.setAttribute("points", f["points"]) pg.appendChild(frame) pg.appendChild(textLayer) pg.setAttribute("bgcolor", pageColor.name()) for n in range(0, textLayerList.childNodes().size()): node = textLayerList.childNodes().at(n) pg.appendChild(node) body.appendChild(pg) else: for f in frameList: frame = document.createElement("frame") frame.setAttribute("points", f["points"]) coverpage.appendChild(frame) coverpage.appendChild(textLayer) coverpage.setAttribute("bgcolor", pageColor.name()) for n in range(0, textLayerList.childNodes().size()): node = textLayerList.childNodes().at(n) coverpage.appendChild(node) bodyColor = findDominantColor(listOfPageColors) body.setAttribute("bgcolor", bodyColor.name()) if configDictionary.get("includeTranslComment", False): for lang in translationComments.keys(): for key in translationComments[lang]: index = translationComments[lang].index(key)+1 refID = "-".join(["tn", lang, str(index)]) ref = document.createElement("reference") ref.setAttribute("lang", lang) ref.setAttribute("id", refID) transHeaderStr = configDictionary.get("translatorHeader", "Translator's Notes") - transHeaderStr = poParser.get_entry_for_key("@meta-translator", lang).get("trans", transHeaderStr) + transHeaderStr = poParser.get_entry_for_key("@meta-translator "+transHeaderStr, lang).get("trans", transHeaderStr) translatorHeader = document.createElement("p") translatorHeader.appendChild(document.createTextNode(transHeaderStr+":")) ref.appendChild(translatorHeader) refPara = document.createElement("p") refPara.appendChild(document.createTextNode(key)) ref.appendChild(refPara) references.appendChild(ref) root.appendChild(body) if references.childNodes().size(): root.appendChild(references) f = open(locationBasic, 'w', newline="", encoding="utf-8") f.write(document.toString(indent=2)) f.close() success = True success = createStandAloneACBF(configDictionary, document, locationStandAlone, pagesLocationList) return success def createStandAloneACBF(configDictionary, document = QDomDocument(), location = str(), pagesLocationList = []): title = configDictionary["projectName"] if "title" in configDictionary.keys(): title = configDictionary["title"] root = document.firstChildElement("ACBF") meta = root.firstChildElement("meta-data") bookInfo = meta.firstChildElement("book-info") cover = bookInfo.firstChildElement("coverpage") body = root.firstChildElement("body") pages = [] for p in range(0, len(body.elementsByTagName("page"))): pages.append(body.elementsByTagName("page").item(p).toElement()) if (cover): pages.append(cover) data = document.createElement("data") root.appendChild(data) # Covert pages to base64 strings. for i in range(0, len(pages)): image = pages[i].firstChildElement("image") href = image.attribute("href") for p in pagesLocationList: if href in p: binary = document.createElement("binary") binary.setAttribute("id", href) imageFile = QImage() imageFile.load(p) imageData = QByteArray() buffer = QBuffer(imageData) imageFile.save(buffer, "PNG") # For now always embed as png. contentType = "image/png" binary.setAttribute("content-type", contentType) binary.appendChild(document.createTextNode(str(bytearray(imageData.toBase64()).decode("ascii")))) image.setAttribute("href", "#" + href) data.appendChild(binary) f = open(location, 'w', newline="", encoding="utf-8") f.write(document.toString(indent=2)) f.close() return True """ Function to parse svg text to acbf ready text """ def parseTextChildren(document = QDomDocument(), elRead = QDomElement(), elWrite = QDomElement(), emphasisStyle = {}, strongStyle = {}): for n in range(0, elRead.childNodes().size()): childNode = elRead.childNodes().item(n) if childNode.isText(): if elWrite.hasChildNodes() and str(childNode.nodeValue()).startswith(" ") is False: elWrite.appendChild(document.createTextNode(" ")) elWrite.appendChild(document.createTextNode(str(childNode.nodeValue()))) elif childNode.hasChildNodes(): childNode = childNode.toElement() fontFamily = str(childNode.attribute("font-family")) fontWeight = str(childNode.attribute("font-weight", "400")) fontItalic = str(childNode.attribute("font-style")) fontStrikeThrough = str(childNode.attribute("text-decoration")) fontBaseLine = str(childNode.attribute("baseline-shift")) newElementMade = False emphasis = False strong = False if len(emphasisStyle.keys()) > 0: emphasis = compare_styles(emphasisStyle, fontFamily, fontWeight, fontItalic) else: if fontItalic == "italic": emphasis = True if len(strongStyle.keys()) > 0: strong = compare_styles(strongStyle, fontFamily, fontWeight, fontItalic) else: if fontWeight == "bold" or int(fontWeight) > 400: strong = True if strong: newElement = document.createElement("strong") newElementMade = True elif emphasis: newElement = document.createElement("emphasis") newElementMade = True elif fontStrikeThrough == "line-through": newElement = document.createElement("strikethrough") newElementMade = True elif fontBaseLine.isalnum(): if (fontBaseLine == "super"): newElement = document.createElement("sup") newElementMade = True elif (fontBaseLine == "sub"): newElement = document.createElement("sub") newElementMade = True if newElementMade is True: parseTextChildren(document, childNode, newElement, emphasisStyle, strongStyle) elWrite.appendChild(newElement) else: parseTextChildren(document, childNode, elWrite, emphasisStyle, strongStyle) # If it is not a text node, nor does it have children(which could be textnodes), # we should assume it's empty and ignore it. elWrite.normalize() for e in range(0, elWrite.childNodes().size()): el = elWrite.childNodes().item(e) if el.isText(): eb = el.nodeValue() el.setNodeValue(eb.replace(" ", " ")) def compare_styles(style = {}, fontFamily = str(), fontWeight = str(), fontStyle = str()): compare = [] if "font" in style.keys(): font = style.get("font") if isinstance(font, list): compare.append(fontFamily in font) else: compare.append((fontFamily == font)) if "bold" in style.keys(): compare.append(fontWeight == "bold" or int(fontWeight) > 400) if "ital" in style.keys(): compare.append(fontStyle == "italic") countTrue = 0 for i in compare: if i is True: countTrue +=1 if countTrue > 1: return True else: return False """ This function tries to determine if there's a dominant color, and if not, it'll mix all of them. """ def findDominantColor(listOfColors = [QColor()]): dominantColor = QColor() listOfColorNames = {} for color in listOfColors: count = listOfColorNames.get(color.name(), 0) listOfColorNames[color.name()] = count+1 # Check if there's a sense of dominant color: clear_dominant = False if len(listOfColorNames) == 2 and len(listOfColors) == 1: clear_dominant = True elif len(listOfColorNames) == 3 and len(listOfColors) == 2: clear_dominant = True elif len(listOfColorNames.keys()) < (len(listOfColors)*0.5): clear_dominant = True if clear_dominant: namesSorted = sorted(listOfColorNames, key=listOfColorNames.get, reverse=True) dominantColor = QColor(namesSorted[0]) else: 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())) return dominantColor