1"""
2Copyright (c) 2017 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
3
4This file is part of the Comics Project Management Tools(CPMT).
5
6CPMT is free software: you can redistribute it and/or modify
7it under the terms of the GNU General Public License as published by
8the Free Software Foundation, either version 3 of the License, or
9(at your option) any later version.
10
11CPMT is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with the CPMT.  If not, see <http://www.gnu.org/licenses/>.
18"""
19
20"""
21An exporter that take the comicsConfig and uses it to generate several files.
22"""
23import sys
24from pathlib import Path
25import zipfile
26from xml.dom import minidom
27from xml.etree import ElementTree as ET
28import types
29import re
30from PyQt5.QtWidgets import QLabel, QProgressDialog, QMessageBox, qApp  # For the progress dialog.
31from PyQt5.QtCore import QElapsedTimer, QLocale, Qt, QRectF, QPointF
32from PyQt5.QtGui import QImage, QTransform, QPainterPath, QFontMetrics, QFont
33from krita import *
34from . import exporters
35
36"""
37The sizesCalculator is a convenience class for interpretting the resize configuration
38from the export settings dialog. It is also used for batch resize.
39"""
40
41
42class sizesCalculator():
43
44    def __init__(self):
45        pass
46
47    def get_scale_from_resize_config(self, config, listSizes):
48        listScaleTo = listSizes
49        oldWidth = listSizes[0]
50        oldHeight = listSizes[1]
51        oldXDPI = listSizes[2]
52        oldYDPI = listSizes[3]
53        if "Method" in config.keys():
54            method = config["Method"]
55            if method == 0:
56                # percentage
57                percentage = config["Percentage"] / 100
58                listScaleTo[0] = round(oldWidth * percentage)
59                listScaleTo[1] = round(oldHeight * percentage)
60            if method == 1:
61                # dpi
62                DPI = config["DPI"]
63                listScaleTo[0] = round((oldWidth / oldXDPI) * DPI)
64                listScaleTo[1] = round((oldHeight / oldYDPI) * DPI)
65                listScaleTo[2] = DPI
66                listScaleTo[3] = DPI
67            if method == 2:
68                # maximum width
69                width = config["Width"]
70                listScaleTo[0] = width
71                listScaleTo[1] = round((oldHeight / oldWidth) * width)
72            if method == 3:
73                # maximum height
74                height = config["Height"]
75                listScaleTo[1] = height
76                listScaleTo[0] = round((oldWidth / oldHeight) * height)
77        return listScaleTo
78
79
80"""
81The comicsExporter is a class that batch exports to all the requested formats.
82Make it, set_config with the right data, and then call up "export".
83
84The majority of the functions are meta-data encoding functions.
85"""
86
87
88class comicsExporter():
89    acbfLocation = str()
90    acbfPageData = []
91    cometLocation = str()
92    comicRackInfo = str()
93    pagesLocationList = {}
94
95    # set of keys used to define specific export behaviour for this page.
96    pageKeys = ["acbf_title", "acbf_none", "acbf_fade", "acbf_blend", "acbf_horizontal", "acbf_vertical", "epub_spread"]
97
98    def __init__(self):
99        pass
100
101    """
102    The configuration of the exporter.
103
104    @param config: A dictionary containing all the config.
105
106    @param projectUrl: the main location of the project folder.
107    """
108
109    def set_config(self, config, projectURL):
110        self.configDictionary = config
111        self.projectURL = projectURL
112        self.pagesLocationList = {}
113        self.acbfLocation = str()
114        self.acbfPageData = []
115        self.cometLocation = str()
116        self.comicRackInfo = str()
117
118    """
119    Export everything according to config and get yourself a coffee.
120    This won't work if the config hasn't been set.
121    """
122
123    def export(self):
124        export_success = False
125
126        path = Path(self.projectURL)
127        if path.exists():
128            # Make a meta-data folder so we keep the export folder nice and clean.
129            exportPath = path / self.configDictionary["exportLocation"]
130            if Path(exportPath / "metadata").exists() is False:
131                Path(exportPath / "metadata").mkdir()
132
133            # Get to which formats to export, and set the sizeslist.
134            lengthProcess = len(self.configDictionary["pages"])
135            sizesList = {}
136            if "CBZ" in self.configDictionary.keys():
137                if self.configDictionary["CBZactive"]:
138                    lengthProcess += 5
139                    sizesList["CBZ"] = self.configDictionary["CBZ"]
140            if "EPUB" in self.configDictionary.keys():
141                if self.configDictionary["EPUBactive"]:
142                    lengthProcess += 1
143                    sizesList["EPUB"] = self.configDictionary["EPUB"]
144            if "TIFF" in self.configDictionary.keys():
145                if self.configDictionary["TIFFactive"]:
146                    sizesList["TIFF"] = self.configDictionary["TIFF"]
147            # Export the pngs according to the sizeslist.
148            # Create a progress dialog.
149            self.progress = QProgressDialog(i18n("Preparing export."), str(), 0, lengthProcess)
150            self.progress.setWindowTitle(i18n("Exporting Comic..."))
151            self.progress.setCancelButton(None)
152            self.timer = QElapsedTimer()
153            self.timer.start()
154            self.progress.show()
155            qApp.processEvents()
156            export_success = self.save_out_pngs(sizesList)
157
158            # Export acbf metadata.
159            if export_success:
160                if "CBZ" in sizesList.keys():
161                    title = self.configDictionary["projectName"]
162                    if "title" in self.configDictionary.keys():
163                        title = str(self.configDictionary["title"]).replace(" ", "_")
164
165                    self.acbfLocation = str(exportPath / "metadata" / str(title + ".acbf"))
166
167                    locationStandAlone = str(exportPath / str(title + ".acbf"))
168                    self.progress.setLabelText(i18n("Saving out ACBF and\nACBF standalone"))
169                    self.progress.setValue(self.progress.value()+2)
170                    export_success = exporters.ACBF.write_xml(self.configDictionary, self.acbfPageData, self.pagesLocationList["CBZ"], self.acbfLocation, locationStandAlone, self.projectURL)
171                    print("CPMT: Exported to ACBF", export_success)
172
173            # Export and package CBZ and Epub.
174            if export_success:
175                if "CBZ" in sizesList.keys():
176                    export_success = self.export_to_cbz(exportPath)
177                    print("CPMT: Exported to CBZ", export_success)
178                if "EPUB" in sizesList.keys():
179                    self.progress.setLabelText(i18n("Saving out EPUB"))
180                    self.progress.setValue(self.progress.value()+1)
181                    export_success = exporters.EPUB.export(self.configDictionary, self.projectURL, self.pagesLocationList["EPUB"], self.acbfPageData)
182                    print("CPMT: Exported to EPUB", export_success)
183        else:
184            QMessageBox.warning(None, i18n("Export not Possible"), i18n("Nothing to export, URL not set."), QMessageBox.Ok)
185            print("CPMT: Nothing to export, url not set.")
186
187        return export_success
188
189    """
190    This calls up all the functions necessary for making a cbz.
191    """
192
193    def export_to_cbz(self, exportPath):
194        title = self.configDictionary["projectName"]
195        if "title" in self.configDictionary.keys():
196            title = str(self.configDictionary["title"]).replace(" ", "_")
197        self.progress.setLabelText(i18n("Saving out CoMet\nmetadata file"))
198        self.progress.setValue(self.progress.value()+1)
199        self.cometLocation = str(exportPath / "metadata" / str(title + " CoMet.xml"))
200        export_success = exporters.CoMet.write_xml(self.configDictionary, self.pagesLocationList["CBZ"], self.cometLocation)
201        self.comicRackInfo = str(exportPath / "metadata" / "ComicInfo.xml")
202        self.progress.setLabelText(i18n("Saving out Comicrack\nmetadata file"))
203        self.progress.setValue(self.progress.value()+1)
204        export_success = exporters.comic_rack_xml.write_xml(self.configDictionary, self.pagesLocationList["CBZ"], self.comicRackInfo)
205        self.package_cbz(exportPath)
206        return export_success
207
208    def save_out_pngs(self, sizesList):
209        # A small fix to ensure crop to guides is set.
210        if "cropToGuides" not in self.configDictionary.keys():
211            self.configDictionary["cropToGuides"] = False
212
213        # Check if we have pages at all...
214        if "pages" in self.configDictionary.keys():
215
216            # Check if there's export methods, and if so make sure the appropriate dictionaries are initialised.
217            if len(sizesList.keys()) < 1:
218                QMessageBox.warning(None, i18n("Export not Possible"), i18n("Export failed because there's no export settings configured."), QMessageBox.Ok)
219                print("CPMT: Export failed because there's no export methods set.")
220                return False
221            else:
222                for key in sizesList.keys():
223                    self.pagesLocationList[key] = []
224
225            # Get the appropriate paths.
226            path = Path(self.projectURL)
227            exportPath = path / self.configDictionary["exportLocation"]
228            pagesList = self.configDictionary["pages"]
229            fileName = str(exportPath)
230
231            """
232            Mini function to handle the setup of this string.
233            """
234            def timeString(timePassed, timeEstimated):
235                return str(i18n("Time passed: {passedString}\n Estimated: {estimated}")).format(passedString=timePassed, estimated=timeEstimated)
236
237            for p in range(0, len(pagesList)):
238                pagesDone = str(i18n("{pages} of {pagesTotal} done.")).format(pages=p, pagesTotal=len(pagesList))
239
240                # Update the label in the progress dialog.
241                self.progress.setValue(p)
242                timePassed = self.timer.elapsed()
243                if p > 0:
244                    timeEstimated = (len(pagesList) - p) * (timePassed / p)
245                    estimatedString = self.parseTime(timeEstimated)
246                else:
247                    estimatedString = str(u"\u221E")
248                passedString = self.parseTime(timePassed)
249                self.progress.setLabelText("\n".join([pagesDone, timeString(passedString, estimatedString), i18n("Opening next page")]))
250                qApp.processEvents()
251                # Get the appropriate url and open the page.
252                url = str(Path(self.projectURL) / pagesList[p])
253                page = Application.openDocument(url)
254                page.waitForDone()
255
256                # Update the progress bar a little
257                self.progress.setLabelText("\n".join([pagesDone, timeString(self.parseTime(self.timer.elapsed()), estimatedString), i18n("Cleaning up page")]))
258
259                # remove layers and flatten.
260                labelList = self.configDictionary["labelsToRemove"]
261                panelsAndText = []
262
263                # These three lines are what is causing the page not to close.
264                root = page.rootNode()
265                self.getPanelsAndText(root, panelsAndText)
266                self.removeLayers(labelList, root)
267                page.refreshProjection()
268                # We'll need the offset and scale for aligning the panels and text correctly. We're getting this from the CBZ
269
270                pageData = {}
271                pageData["vector"] = panelsAndText
272                tree = ET.fromstring(page.documentInfo())
273                pageData["title"] = page.name()
274                calligra = "{http://www.calligra.org/DTD/document-info}"
275                about = tree.find(calligra + "about")
276                keywords = about.find(calligra + "keyword")
277                keys = str(keywords.text).split(",")
278                pKeys = []
279                for key in keys:
280                    if key in self.pageKeys:
281                        pKeys.append(key)
282                pageData["keys"] = pKeys
283                page.flatten()
284                page.waitForDone()
285                batchsave = Application.batchmode()
286                Application.setBatchmode(True)
287                # Start making the format specific copy.
288                for key in sizesList.keys():
289
290                    # Update the progress bar a little
291                    self.progress.setLabelText("\n".join([pagesDone, timeString(self.parseTime(self.timer.elapsed()), estimatedString), str(i18n("Exporting for {key}")).format(key=key)]))
292
293                    w = sizesList[key]
294                    # copy over data
295                    projection = page.clone()
296                    projection.setBatchmode(True)
297                    # Crop. Cropping per guide only happens if said guides have been found.
298                    if w["Crop"] is True:
299                        listHGuides = []
300                        listHGuides = page.horizontalGuides()
301                        listHGuides.sort()
302                        for i in range(len(listHGuides) - 1, 0, -1):
303                            if listHGuides[i] < 0 or listHGuides[i] > page.height():
304                                listHGuides.pop(i)
305                        listVGuides = page.verticalGuides()
306                        listVGuides.sort()
307                        for i in range(len(listVGuides) - 1, 0, -1):
308                            if listVGuides[i] < 0 or listVGuides[i] > page.width():
309                                listVGuides.pop(i)
310                        if self.configDictionary["cropToGuides"] and len(listVGuides) > 1:
311                            cropx = listVGuides[0]
312                            cropw = listVGuides[-1] - cropx
313                        else:
314                            cropx = self.configDictionary["cropLeft"]
315                            cropw = page.width() - self.configDictionary["cropRight"] - cropx
316                        if self.configDictionary["cropToGuides"] and len(listHGuides) > 1:
317                            cropy = listHGuides[0]
318                            croph = listHGuides[-1] - cropy
319                        else:
320                            cropy = self.configDictionary["cropTop"]
321                            croph = page.height() - self.configDictionary["cropBottom"] - cropy
322                        projection.crop(cropx, cropy, cropw, croph)
323                        projection.waitForDone()
324                        qApp.processEvents()
325                        # resize appropriately
326                    else:
327                        cropx = 0
328                        cropy = 0
329                    res = page.resolution()
330                    listScales = [projection.width(), projection.height(), res, res]
331                    projectionOldSize = [projection.width(), projection.height()]
332                    sizesCalc = sizesCalculator()
333                    listScales = sizesCalc.get_scale_from_resize_config(config=w, listSizes=listScales)
334                    projection.scaleImage(listScales[0], listScales[1], listScales[2], listScales[3], "bicubic")
335                    projection.waitForDone()
336                    qApp.processEvents()
337                    # png, gif and other webformats should probably be in 8bit srgb at maximum.
338                    if key != "TIFF":
339                        if (projection.colorModel() != "RGBA" and projection.colorModel() != "GRAYA") or projection.colorDepth() != "U8":
340                            projection.setColorSpace("RGBA", "U8", "sRGB built-in")
341                    else:
342                        # Tiff on the other hand can handle all the colormodels, but can only handle integer bit depths.
343                        # Tiff is intended for print output, and 16 bit integer will be sufficient.
344                        if projection.colorDepth() != "U8" or projection.colorDepth() != "U16":
345                            projection.setColorSpace(page.colorModel(), "U16", page.colorProfile())
346                    # save
347                    # Make sure the folder name for this export exists. It'll allow us to keep the
348                    # export folders nice and clean.
349                    folderName = str(key + "-" + w["FileType"])
350                    if Path(exportPath / folderName).exists() is False:
351                        Path.mkdir(exportPath / folderName)
352                    # Get a nice and descriptive fle name.
353                    fn = str(Path(exportPath / folderName) / str("page_" + format(p, "03d") + "_" + str(listScales[0]) + "x" + str(listScales[1]) + "." + w["FileType"]))
354                    # Finally save and add the page to a list of pages. This will make it easy for the packaging function to
355                    # find the pages and store them.
356                    projection.exportImage(fn, InfoObject())
357                    projection.waitForDone()
358                    qApp.processEvents()
359                    if key == "CBZ" or key == "EPUB":
360                        transform = {}
361                        transform["offsetX"] = cropx
362                        transform["offsetY"] = cropy
363                        transform["resDiff"] = page.resolution() / 72
364                        transform["scaleWidth"] = projection.width() / projectionOldSize[0]
365                        transform["scaleHeight"] = projection.height() / projectionOldSize[1]
366                        pageData["transform"] = transform
367                    self.pagesLocationList[key].append(fn)
368                    projection.close()
369                self.acbfPageData.append(pageData)
370                page.close()
371            self.progress.setValue(len(pagesList))
372            Application.setBatchmode(batchsave)
373            # TODO: Check what or whether memory leaks are still caused and otherwise remove the entry below.
374            print("CPMT: Export has finished. If there are memory leaks, they are caused by file layers.")
375            return True
376        print("CPMT: Export not happening because there aren't any pages.")
377        QMessageBox.warning(None, i18n("Export not Possible"), i18n("Export not happening because there are no pages."), QMessageBox.Ok)
378        return False
379
380    """
381    Function to get the panel and text data.
382    """
383
384    def getPanelsAndText(self, node, list):
385        textLayersToSearch = ["text"]
386        panelLayersToSearch = ["panels"]
387        if "textLayerNames" in self.configDictionary.keys():
388            textLayersToSearch = self.configDictionary["textLayerNames"]
389        if "panelLayerNames" in self.configDictionary.keys():
390            panelLayersToSearch = self.configDictionary["panelLayerNames"]
391        if node.type() == "vectorlayer":
392            for name in panelLayersToSearch:
393                if str(name).lower() in str(node.name()).lower():
394                    for shape in node.shapes():
395                        if (shape.type() == "groupshape"):
396                            self.getPanelsAndTextVector(shape, list)
397                        else:
398                            self.handleShapeDescription(shape, list)
399            for name in textLayersToSearch:
400                if str(name).lower() in str(node.name()).lower():
401                    for shape in node.shapes():
402                        if (shape.type() == "groupshape"):
403                            self.getPanelsAndTextVector(shape, list, True)
404                        else:
405                            self.handleShapeDescription(shape, list, True)
406        else:
407            if node.childNodes():
408                for child in node.childNodes():
409                    self.getPanelsAndText(node=child, list=list)
410
411    def parseTime(self, time = 0):
412        timeList = []
413        timeList.append(str(int(time / 60000)))
414        timeList.append(format(int((time%60000) / 1000), "02d"))
415        timeList.append(format(int(time % 1000), "03d"))
416        return ":".join(timeList)
417    """
418    Function to get the panel and text data from a group shape
419    """
420
421    def getPanelsAndTextVector(self, group, list, textOnly=False):
422        for shape in group.shapes():
423            if (shape.type() == "groupshape"):
424                self.getPanelsAndTextVector(shape, list, textOnly)
425            else:
426                self.handleShapeDescription(shape, list, textOnly)
427    """
428    Function to get text and panels in a format that acbf will accept
429    """
430
431    def handleShapeDescription(self, shape, list, textOnly=False):
432        if (shape.type() != "KoSvgTextShapeID" and textOnly is True):
433            return
434        shapeDesc = {}
435        shapeDesc["name"] = shape.name()
436        rect = shape.boundingBox()
437        listOfPoints = [rect.topLeft(), rect.topRight(), rect.bottomRight(), rect.bottomLeft()]
438        shapeDoc = minidom.parseString(shape.toSvg())
439        docElem = shapeDoc.documentElement
440        svgRegExp = re.compile('[MLCSQHVATmlzcqshva]\d+\.?\d* \d+\.?\d*')
441        transform = docElem.getAttribute("transform")
442        coord = []
443        adjust = QTransform()
444        # TODO: If we get global transform api, use that instead of parsing manually.
445        if "translate" in transform:
446            transform = transform.replace('translate(', '')
447            for c in transform[:-1].split(" "):
448                if "," in c:
449                    c = c.replace(",", "")
450                coord.append(float(c))
451            if len(coord) < 2:
452                coord.append(coord[0])
453            adjust = QTransform(1, 0, 0, 1, coord[0], coord[1])
454        if "matrix" in transform:
455            transform = transform.replace('matrix(', '')
456            for c in transform[:-1].split(" "):
457                if "," in c:
458                    c = c.replace(",", "")
459                coord.append(float(c))
460            adjust = QTransform(coord[0], coord[1], coord[2], coord[3], coord[4], coord[5])
461        path = QPainterPath()
462        if docElem.localName == "path":
463            dVal = docElem.getAttribute("d")
464            listOfSvgStrings = [" "]
465            listOfSvgStrings = svgRegExp.findall(dVal)
466            if listOfSvgStrings:
467                listOfPoints = []
468                for l in listOfSvgStrings:
469                    line = l[1:]
470                    coordinates = line.split(" ")
471                    if len(coordinates) < 2:
472                        coordinates.append(coordinates[0])
473                    x = float(coordinates[-2])
474                    y = float(coordinates[-1])
475                    offset = QPointF()
476                    if l.islower():
477                        offset = listOfPoints[0]
478                    if l.lower().startswith("m"):
479                        path.moveTo(QPointF(x, y) + offset)
480                    elif l.lower().startswith("h"):
481                        y = listOfPoints[-1].y()
482                        path.lineTo(QPointF(x, y) + offset)
483                    elif l.lower().startswith("v"):
484                        x = listOfPoints[-1].x()
485                        path.lineTo(QPointF(x, y) + offset)
486                    elif l.lower().startswith("c"):
487                        path.cubicTo(coordinates[0], coordinates[1], coordinates[2], coordinates[3], x, y)
488                    else:
489                        path.lineTo(QPointF(x, y) + offset)
490                path.setFillRule(Qt.WindingFill)
491                for polygon in path.simplified().toSubpathPolygons(adjust):
492                    for point in polygon:
493                        listOfPoints.append(point)
494        elif docElem.localName == "rect":
495            listOfPoints = []
496            if (docElem.hasAttribute("x")):
497                x = float(docElem.getAttribute("x"))
498            else:
499                x = 0
500            if (docElem.hasAttribute("y")):
501                y = float(docElem.getAttribute("y"))
502            else:
503                y = 0
504            w = float(docElem.getAttribute("width"))
505            h = float(docElem.getAttribute("height"))
506            path.addRect(QRectF(x, y, w, h))
507            for point in path.toFillPolygon(adjust):
508                listOfPoints.append(point)
509        elif docElem.localName == "ellipse":
510            listOfPoints = []
511            if (docElem.hasAttribute("cx")):
512                x = float(docElem.getAttribute("cx"))
513            else:
514                x = 0
515            if (docElem.hasAttribute("cy")):
516                y = float(docElem.getAttribute("cy"))
517            else:
518                y = 0
519            ry = float(docElem.getAttribute("ry"))
520            rx = float(docElem.getAttribute("rx"))
521            path.addEllipse(QPointF(x, y), rx, ry)
522            for point in path.toFillPolygon(adjust):
523                listOfPoints.append(point)
524        elif docElem.localName == "text":
525            # NOTE: This only works for horizontal preformated text. Vertical text needs a different
526            # ordering of the rects, and wraparound should try to take the shape it is wrapped in.
527            family = "sans-serif"
528            if docElem.hasAttribute("font-family"):
529                family = docElem.getAttribute("font-family")
530            size = "11"
531            if docElem.hasAttribute("font-size"):
532                size = docElem.getAttribute("font-size")
533            multilineText = True
534            for el in docElem.childNodes:
535                if el.nodeType == minidom.Node.TEXT_NODE:
536                    multilineText = False
537            if multilineText:
538                listOfPoints = []
539                listOfRects = []
540
541                # First we collect all the possible line-rects.
542                for el in docElem.childNodes:
543                    if docElem.hasAttribute("font-family"):
544                        family = docElem.getAttribute("font-family")
545                    if docElem.hasAttribute("font-size"):
546                        size = docElem.getAttribute("font-size")
547                    fontsize = int(size)
548                    font = QFont(family, fontsize)
549                    string = el.toxml()
550                    string = re.sub("\<.*?\>", " ", string)
551                    string = string.replace("  ", " ")
552                    width = min(QFontMetrics(font).width(string.strip()), rect.width())
553                    height = QFontMetrics(font).height()
554                    anchor = "start"
555                    if docElem.hasAttribute("text-anchor"):
556                        anchor = docElem.getAttribute("text-anchor")
557                    top = rect.top()
558                    if len(listOfRects)>0:
559                        top = listOfRects[-1].bottom()
560                    if anchor == "start":
561                        spanRect = QRectF(rect.left(), top, width, height)
562                        listOfRects.append(spanRect)
563                    elif anchor == "end":
564                        spanRect = QRectF(rect.right()-width, top, width, height)
565                        listOfRects.append(spanRect)
566                    else:
567                        # Middle
568                        spanRect = QRectF(rect.center().x()-(width*0.5), top, width, height)
569                        listOfRects.append(spanRect)
570                # Now we have all the rects, we can check each and draw a
571                # polygon around them.
572                heightAdjust = (rect.height()-(listOfRects[-1].bottom()-rect.top()))/len(listOfRects)
573                for i in range(len(listOfRects)):
574                    span = listOfRects[i]
575                    addtionalHeight = i*heightAdjust
576                    if i == 0:
577                        listOfPoints.append(span.topLeft())
578                        listOfPoints.append(span.topRight())
579                    else:
580                        if listOfRects[i-1].width()< span.width():
581                            listOfPoints.append(QPointF(span.right(), span.top()+addtionalHeight))
582                            listOfPoints.insert(0, QPointF(span.left(), span.top()+addtionalHeight))
583                        else:
584                            bottom = listOfRects[i-1].bottom()+addtionalHeight-heightAdjust
585                            listOfPoints.append(QPointF(listOfRects[i-1].right(), bottom))
586                            listOfPoints.insert(0, QPointF(listOfRects[i-1].left(), bottom))
587                listOfPoints.append(QPointF(span.right(), rect.bottom()))
588                listOfPoints.insert(0, QPointF(span.left(), rect.bottom()))
589                path = QPainterPath()
590                path.moveTo(listOfPoints[0])
591                for p in range(1, len(listOfPoints)):
592                    path.lineTo(listOfPoints[p])
593                path.closeSubpath()
594                listOfPoints = []
595                for point in path.toFillPolygon(adjust):
596                    listOfPoints.append(point)
597        shapeDesc["boundingBox"] = listOfPoints
598        if (shape.type() == "KoSvgTextShapeID" and textOnly is True):
599            shapeDesc["text"] = shape.toSvg()
600        list.append(shapeDesc)
601
602    """
603    Function to remove layers when they have the given labels.
604
605    If not, but the node does have children, check those too.
606    """
607
608    def removeLayers(self, labels, node):
609        if node.colorLabel() in labels:
610            node.remove()
611        else:
612            if node.childNodes():
613                for child in node.childNodes():
614                    self.removeLayers(labels, node=child)
615
616    """
617    package cbz puts all the meta-data and relevant files into an zip file ending with ".cbz"
618    """
619
620    def package_cbz(self, exportPath):
621
622        # Use the project name if there's no title to avoid sillyness with unnamed zipfiles.
623        title = self.configDictionary["projectName"]
624        if "title" in self.configDictionary.keys():
625            title = str(self.configDictionary["title"]).replace(" ", "_")
626
627        # Get the appropriate path.
628        url = str(exportPath / str(title + ".cbz"))
629
630        # Create a zip file.
631        cbzArchive = zipfile.ZipFile(url, mode="w", compression=zipfile.ZIP_STORED)
632
633        # Add all the meta data files.
634        cbzArchive.write(self.acbfLocation, Path(self.acbfLocation).name)
635        cbzArchive.write(self.cometLocation, Path(self.cometLocation).name)
636        cbzArchive.write(self.comicRackInfo, Path(self.comicRackInfo).name)
637        comic_book_info_json_dump = str()
638        self.progress.setLabelText(i18n("Saving out Comicbook\ninfo metadata file"))
639        self.progress.setValue(self.progress.value()+1)
640        comic_book_info_json_dump = exporters.comic_book_info.writeJson(self.configDictionary)
641        cbzArchive.comment = comic_book_info_json_dump.encode("utf-8")
642
643        # Add the pages.
644        if "CBZ" in self.pagesLocationList.keys():
645            for page in self.pagesLocationList["CBZ"]:
646                if (Path(page).exists()):
647                    cbzArchive.write(page, Path(page).name)
648        self.progress.setLabelText(i18n("Packaging CBZ"))
649        self.progress.setValue(self.progress.value()+1)
650        # Close the zip file when done.
651        cbzArchive.close()
652