Krita Source Code Documentation
Loading...
Searching...
No Matches
CPMT_EPUB_exporter.py
Go to the documentation of this file.
1"""
2SPDX-FileCopyrightText: 2018 Wolthera van Hövell tot Westerflier <griffinvalley@gmail.com>
3
4This file is part of the Comics Project Management Tools(CPMT).
5
6SPDX-License-Identifier: GPL-3.0-or-later
7"""
8
9"""
10Create an epub folder, finally, package to a epubzip.
11"""
12
13import shutil
14import os
15from pathlib import Path
16import zipfile
17try:
18 from PyQt6.QtXml import QDomDocument
19 from PyQt6.QtCore import Qt, QDateTime, QPointF
20 from PyQt6.QtGui import QImage, QPolygonF, QColor
21except:
22 from PyQt5.QtXml import QDomDocument
23 from PyQt5.QtCore import Qt, QDateTime, QPointF
24 from PyQt5.QtGui import QImage, QPolygonF, QColor
25from builtins import i18n
26
27def export(configDictionary = {}, projectURL = str(), pagesLocationList = [], pageData = []):
28 path = Path(os.path.join(projectURL, configDictionary["exportLocation"]))
29 exportPath = path / "EPUB-files"
30 metaInf = exportPath / "META-INF"
31 oebps = exportPath / "OEBPS"
32 imagePath = oebps / "Images"
33 # Don't write empty folders. Epubcheck doesn't like that.
34 # stylesPath = oebps / "Styles"
35 textPath = oebps / "Text"
36
37 if exportPath.exists() is False:
38 exportPath.mkdir()
39 metaInf.mkdir()
40 oebps.mkdir()
41 imagePath.mkdir()
42 # stylesPath.mkdir()
43 textPath.mkdir()
44
45 # Due the way EPUB verifies, the mimetype needs to be packaged in first.
46 # Due the way zips are constructed, the only way to ensure that is to
47 # Fill the zip as we go along...
48
49 # Use the project name if there's no title to avoid sillyness with unnamed zipfiles.
50 title = configDictionary["projectName"]
51 if "title" in configDictionary.keys():
52 title = str(configDictionary["title"]).replace(" ", "_")
53
54 # Get the appropriate path.
55 url = str(path / str(title + ".epub"))
56
57 # Create a zip file.
58 epubArchive = zipfile.ZipFile(url, mode="w", compression=zipfile.ZIP_STORED)
59
60 mimetype = open(str(Path(exportPath / "mimetype")), mode="w")
61 mimetype.write("application/epub+zip")
62 mimetype.close()
63
64 # Write to zip.
65 epubArchive.write(Path(exportPath / "mimetype"), Path("mimetype"))
66
67 container = QDomDocument()
68 cRoot = container.createElement("container")
69 cRoot.setAttribute("version", "1.0")
70 cRoot.setAttribute("xmlns", "urn:oasis:names:tc:opendocument:xmlns:container")
71 container.appendChild(cRoot)
72 rootFiles = container.createElement("rootfiles")
73 rootfile = container.createElement("rootfile")
74 rootfile.setAttribute("full-path", "OEBPS/content.opf")
75 rootfile.setAttribute("media-type", "application/oebps-package+xml")
76 rootFiles.appendChild(rootfile)
77 cRoot.appendChild(rootFiles)
78
79 containerFileName = str(Path(metaInf / "container.xml"))
80
81 containerFile = open(containerFileName, 'w', newline="", encoding="utf-8")
82 containerFile.write(container.toString(indent=2))
83 containerFile.close()
84
85 # Write to zip.
86 epubArchive.write(containerFileName, os.path.relpath(containerFileName, str(exportPath)))
87
88 # copyimages to images
89 pagesList = []
90 if len(pagesLocationList)>0:
91 if "cover" in configDictionary.keys():
92 coverNumber = configDictionary["pages"].index(configDictionary["cover"])
93 else:
94 coverNumber = 0
95 for p in pagesLocationList:
96 if os.path.exists(p):
97 shutil.copy2(p, str(imagePath))
98 filename = str(Path(imagePath / os.path.basename(p)))
99 pagesList.append(filename)
100 epubArchive.write(filename, os.path.relpath(filename, str(exportPath)))
101 if len(pagesLocationList) >= coverNumber:
102 coverpageurl = pagesList[coverNumber]
103 else:
104 print("CPMT: Couldn't find the location for the epub images.")
105 return False
106
107 # for each image, make an xhtml file
108
109 htmlFiles = []
110 listOfNavItems = {}
111 listofSpreads = []
112 regions = []
113 for i in range(len(pagesList)):
114 pageName = "Page" + str(i) + ".xhtml"
115 doc = QDomDocument()
116 html = doc.createElement("html")
117 doc.appendChild(html)
118 html.setAttribute("xmlns", "http://www.w3.org/1999/xhtml")
119 html.setAttribute("xmlns:epub", "http://www.idpf.org/2007/ops")
120
121 # The viewport is a prerequisite to get pre-paginated
122 # layouts working. We'll make the layout the same size
123 # as the image.
124
125 head = doc.createElement("head")
126 viewport = doc.createElement("meta")
127 viewport.setAttribute("name", "viewport")
128
129 img = QImage()
130 img.load(pagesLocationList[i])
131 w = img.width()
132 h = img.height()
133
134 widthHeight = "width="+str(w)+", height="+str(h)
135
136 viewport.setAttribute("content", widthHeight)
137 head.appendChild(viewport)
138 html.appendChild(head)
139
140 # Here, we process the region navigation data to percentages
141 # because we have access here to the width and height of the viewport.
142
143 data = pageData[i]
144 transform = data["transform"]
145 for v in data["vector"]:
146 pointsList = []
147 dominantColor = QColor(Qt.white)
148 listOfColors = []
149 for point in v["boundingBox"]:
150 offset = QPointF(transform["offsetX"], transform["offsetY"])
151 pixelPoint = QPointF(point.x() * transform["resDiff"], point.y() * transform["resDiff"])
152 newPoint = pixelPoint - offset
153 x = max(0, min(w, int(newPoint.x() * transform["scaleWidth"])))
154 y = max(0, min(h, int(newPoint.y() * transform["scaleHeight"])))
155 listOfColors.append(img.pixelColor(QPointF(x, y).toPoint()))
156 pointsList.append(QPointF((x/w)*100, (y/h)*100))
157 regionType = "panel"
158 if "text" in v.keys():
159 regionType = "text"
160 if len(listOfColors)>0:
161 dominantColor = listOfColors[-1]
162 listOfColors = listOfColors[:-1]
163 for color in listOfColors:
164 dominantColor.setRedF(0.5*(dominantColor.redF()+color.redF()))
165 dominantColor.setGreenF(0.5*(dominantColor.greenF()+color.greenF()))
166 dominantColor.setBlueF(0.5*(dominantColor.blueF()+color.blueF()))
167 region = {}
168 bounds = QPolygonF(pointsList).boundingRect()
169 region["points"] = bounds
170 region["type"] = regionType
171 region["page"] = str(Path(textPath / pageName))
172 region["primaryColor"] = dominantColor.name()
173 regions.append(region)
174
175 # We can also figureout here whether the page can be seen as a table of contents entry.
176
177 if "acbf_title" in data["keys"]:
178 listOfNavItems[str(Path(textPath / pageName))] = data["title"]
179
180 # Or spreads...
181
182 if "epub_spread" in data["keys"]:
183 listofSpreads.append(str(Path(textPath / pageName)))
184
185 body = doc.createElement("body")
186
187 img = doc.createElement("img")
188 img.setAttribute("src", os.path.relpath(pagesList[i], str(textPath)))
189 body.appendChild(img)
190
191 html.appendChild(body)
192
193 filename = str(Path(textPath / pageName))
194 docFile = open(filename, 'w', newline="", encoding="utf-8")
195 docFile.write(doc.toString(indent=2))
196 docFile.close()
197
198 if pagesList[i] == coverpageurl:
199 coverpagehtml = os.path.relpath(filename, str(oebps))
200 htmlFiles.append(filename)
201
202 # Write to zip.
203 epubArchive.write(filename, os.path.relpath(filename, str(exportPath)))
204
205 # metadata
206
207 filename = write_opf_file(oebps, configDictionary, htmlFiles, pagesList, coverpageurl, coverpagehtml, listofSpreads)
208 epubArchive.write(filename, os.path.relpath(filename, str(exportPath)))
209
210 filename = write_region_nav_file(oebps, configDictionary, htmlFiles, regions)
211 epubArchive.write(filename, os.path.relpath(filename, str(exportPath)))
212
213 # toc
214 filename = write_nav_file(oebps, configDictionary, htmlFiles, listOfNavItems)
215 epubArchive.write(filename, os.path.relpath(filename, str(exportPath)))
216
217 filename = write_ncx_file(oebps, configDictionary, htmlFiles, listOfNavItems)
218 epubArchive.write(filename, os.path.relpath(filename, str(exportPath)))
219
220 epubArchive.close()
221
222 return True
223
224"""
225Write OPF metadata file
226"""
227
228
229def write_opf_file(path, configDictionary, htmlFiles, pagesList, coverpageurl, coverpagehtml, listofSpreads):
230
231 # marc relators
232 # This has several entries removed to reduce it to the most relevant entries.
233 marcRelators = {"abr":i18n("Abridger"), "acp":i18n("Art copyist"), "act":i18n("Actor"), "adi":i18n("Art director"), "adp":i18n("Adapter"), "ann":i18n("Annotator"), "ant":i18n("Bibliographic antecedent"), "arc":i18n("Architect"), "ard":i18n("Artistic director"), "art":i18n("Artist"), "asn":i18n("Associated name"), "ato":i18n("Autographer"), "att":i18n("Attributed name"), "aud":i18n("Author of dialog"), "aut":i18n("Author"), "bdd":i18n("Binding designer"), "bjd":i18n("Bookjacket designer"), "bkd":i18n("Book designer"), "bkp":i18n("Book producer"), "blw":i18n("Blurb writer"), "bnd":i18n("Binder"), "bpd":i18n("Bookplate designer"), "bsl":i18n("Bookseller"), "cll":i18n("Calligrapher"), "clr":i18n("Colorist"), "cns":i18n("Censor"), "cov":i18n("Cover designer"), "cph":i18n("Copyright holder"), "cre":i18n("Creator"), "ctb":i18n("Contributor"), "cur":i18n("Curator"), "cwt":i18n("Commentator for written text"), "drm":i18n("Draftsman"), "dsr":i18n("Designer"), "dub":i18n("Dubious author"), "edt":i18n("Editor"), "etr":i18n("Etcher"), "exp":i18n("Expert"), "fnd":i18n("Funder"), "ill":i18n("Illustrator"), "ilu":i18n("Illuminator"), "ins":i18n("Inscriber"), "lse":i18n("Licensee"), "lso":i18n("Licensor"), "ltg":i18n("Lithographer"), "mdc":i18n("Metadata contact"), "oth":i18n("Other"), "own":i18n("Owner"), "pat":i18n("Patron"), "pbd":i18n("Publishing director"), "pbl":i18n("Publisher"), "prt":i18n("Printer"), "sce":i18n("Scenarist"), "scr":i18n("Scribe"), "spn":i18n("Sponsor"), "stl":i18n("Storyteller"), "trc":i18n("Transcriber"), "trl":i18n("Translator"), "tyd":i18n("Type designer"), "tyg":i18n("Typographer"), "wac":i18n("Writer of added commentary"), "wal":i18n("Writer of added lyrics"), "wam":i18n("Writer of accompanying material"), "wat":i18n("Writer of added text"), "win":i18n("Writer of introduction"), "wpr":i18n("Writer of preface"), "wst":i18n("Writer of supplementary textual content")}
234
235 # opf file
236 opfFile = QDomDocument()
237 opfRoot = opfFile.createElement("package")
238 opfRoot.setAttribute("version", "3.0")
239 opfRoot.setAttribute("unique-identifier", "BookId")
240 opfRoot.setAttribute("xmlns", "http://www.idpf.org/2007/opf")
241 opfRoot.setAttribute("prefix", "rendition: http://www.idpf.org/vocab/rendition/#")
242 opfFile.appendChild(opfRoot)
243
244 opfMeta = opfFile.createElement("metadata")
245 opfMeta.setAttribute("xmlns:dc", "http://purl.org/dc/elements/1.1/")
246 opfMeta.setAttribute("xmlns:dcterms", "http://purl.org/dc/terms/")
247
248 # EPUB metadata requires a title, language and uuid
249
250 langString = "en-US"
251 if "language" in configDictionary.keys():
252 langString = str(configDictionary["language"]).replace("_", "-")
253
254 bookLang = opfFile.createElement("dc:language")
255 bookLang.appendChild(opfFile.createTextNode(langString))
256 opfMeta.appendChild(bookLang)
257
258 bookTitle = opfFile.createElement("dc:title")
259 if "title" in configDictionary.keys():
260 bookTitle.appendChild(opfFile.createTextNode(str(configDictionary["title"])))
261 else:
262 bookTitle.appendChild(opfFile.createTextNode("Comic with no Name"))
263 opfMeta.appendChild(bookTitle)
264
265 # Generate series title and the like here too.
266 if "seriesName" in configDictionary.keys():
267 bookTitle.setAttribute("id", "main")
268
269 refine = opfFile.createElement("meta")
270 refine.setAttribute("refines", "#main")
271 refine.setAttribute("property", "title-type")
272 refine.appendChild(opfFile.createTextNode("main"))
273 opfMeta.appendChild(refine)
274
275 refine2 = opfFile.createElement("meta")
276 refine2.setAttribute("refines", "#main")
277 refine2.setAttribute("property", "display-seq")
278 refine2.appendChild(opfFile.createTextNode("1"))
279 opfMeta.appendChild(refine2)
280
281 seriesTitle = opfFile.createElement("dc:title")
282 seriesTitle.appendChild(opfFile.createTextNode(str(configDictionary["seriesName"])))
283 seriesTitle.setAttribute("id", "series")
284 opfMeta.appendChild(seriesTitle)
285
286 refineS = opfFile.createElement("meta")
287 refineS.setAttribute("refines", "#series")
288 refineS.setAttribute("property", "title-type")
289 refineS.appendChild(opfFile.createTextNode("collection"))
290 opfMeta.appendChild(refineS)
291
292 refineS2 = opfFile.createElement("meta")
293 refineS2.setAttribute("refines", "#series")
294 refineS2.setAttribute("property", "display-seq")
295 refineS2.appendChild(opfFile.createTextNode("2"))
296 opfMeta.appendChild(refineS2)
297
298 if "seriesNumber" in configDictionary.keys():
299 refineS3 = opfFile.createElement("meta")
300 refineS3.setAttribute("refines", "#series")
301 refineS3.setAttribute("property", "group-position")
302 refineS3.appendChild(opfFile.createTextNode(str(configDictionary["seriesNumber"])))
303 opfMeta.appendChild(refineS3)
304
305 uuid = str(configDictionary["uuid"])
306 uuid = uuid.strip("{")
307 uuid = uuid.strip("}")
308
309 # Append the id, and assign it as the bookID.
310 uniqueID = opfFile.createElement("dc:identifier")
311 uniqueID.appendChild(opfFile.createTextNode("urn:uuid:"+uuid))
312 uniqueID.setAttribute("id", "BookId")
313 opfMeta.appendChild(uniqueID)
314
315 if "authorList" in configDictionary.keys():
316 authorEntry = 0
317 for authorE in range(len(configDictionary["authorList"])):
318 authorDict = configDictionary["authorList"][authorE]
319 authorType = "dc:creator"
320 if "role" in authorDict.keys():
321 # This determines if someone was just a contributor, but might need a more thorough version.
322 if str(authorDict["role"]).lower() in ["editor", "assistant editor", "proofreader", "beta", "patron", "funder"]:
323 authorType = "dc:contributor"
324 author = opfFile.createElement(authorType)
325 authorName = []
326 if "last-name" in authorDict.keys():
327 authorName.append(authorDict["last-name"])
328 if "first-name" in authorDict.keys():
329 authorName.append(authorDict["first-name"])
330 if "initials" in authorDict.keys():
331 authorName.append(authorDict["initials"])
332 if "nickname" in authorDict.keys():
333 authorName.append("(" + authorDict["nickname"] + ")")
334 author.appendChild(opfFile.createTextNode(", ".join(authorName)))
335 author.setAttribute("id", "cre" + str(authorE))
336 opfMeta.appendChild(author)
337 if "role" in authorDict.keys():
338 role = opfFile.createElement("meta")
339 role.setAttribute("refines", "#cre" + str(authorE))
340 role.setAttribute("scheme", "marc:relators")
341 role.setAttribute("property", "role")
342 roleString = str(authorDict["role"])
343 if roleString in marcRelators.values() or roleString in marcRelators.keys():
344 i = list(marcRelators.values()).index(roleString)
345 roleString = list(marcRelators.keys())[i]
346 else:
347 roleString = "oth"
348 role.appendChild(opfFile.createTextNode(roleString))
349 opfMeta.appendChild(role)
350 refine = opfFile.createElement("meta")
351 refine.setAttribute("refines", "#cre"+str(authorE))
352 refine.setAttribute("property", "display-seq")
353 refine.appendChild(opfFile.createTextNode(str(authorE+1)))
354 opfMeta.appendChild(refine)
355
356 if "publishingDate" in configDictionary.keys():
357 date = opfFile.createElement("dc:date")
358 date.appendChild(opfFile.createTextNode(configDictionary["publishingDate"]))
359 opfMeta.appendChild(date)
360
361 #Creation date
362 modified = opfFile.createElement("meta")
363 modified.setAttribute("property", "dcterms:modified")
364 modified.appendChild(opfFile.createTextNode(QDateTime.currentDateTimeUtc().toString(Qt.DateFormat.ISODate)))
365 opfMeta.appendChild(modified)
366
367 if "source" in configDictionary.keys():
368 if len(configDictionary["source"])>0:
369 source = opfFile.createElement("dc:source")
370 source.appendChild(opfFile.createTextNode(configDictionary["source"]))
371 opfMeta.appendChild(source)
372
373 description = opfFile.createElement("dc:description")
374 if "summary" in configDictionary.keys():
375 description.appendChild(opfFile.createTextNode(configDictionary["summary"]))
376 else:
377 description.appendChild(opfFile.createTextNode("There was no summary upon generation of this file."))
378 opfMeta.appendChild(description)
379
380 # Type can be dictionary or index, or one of those edupub thingies. Not necessary for comics.
381 # typeE = opfFile.createElement("dc:type")
382 # opfMeta.appendChild(typeE)
383
384 if "publisherName" in configDictionary.keys():
385 publisher = opfFile.createElement("dc:publisher")
386 publisher.appendChild(opfFile.createTextNode(configDictionary["publisherName"]))
387 opfMeta.appendChild(publisher)
388
389
390 if "isbn-number" in configDictionary.keys():
391 isbnnumber = configDictionary["isbn-number"]
392
393 if len(isbnnumber)>0:
394 publishISBN = opfFile.createElement("dc:identifier")
395 publishISBN.appendChild(opfFile.createTextNode(str("urn:isbn:") + isbnnumber))
396 opfMeta.appendChild(publishISBN)
397
398 if "license" in configDictionary.keys():
399
400 if len(configDictionary["license"])>0:
401 rights = opfFile.createElement("dc:rights")
402 rights.appendChild(opfFile.createTextNode(configDictionary["license"]))
403 opfMeta.appendChild(rights)
404
405 """
406 Not handled
407 Relation - This is for whether the work has a relationship with another work.
408 It could be fanart, but also adaptation, an academic work, etc.
409 Coverage - This is for the time/place that the work covers. Typically to determine
410 whether an academic work deals with a certain time period or place.
411 For comics you could use this to mark historical comics, but other than
412 that we'd need a much better ui to define this.
413 """
414
415 # These are all dublin core subjects.
416 # 3.1 defines the ability to use an authority, but that
417 # might be a bit too complicated right now.
418
419 if "genre" in configDictionary.keys():
420 genreListConf = configDictionary["genre"]
421 if isinstance(configDictionary["genre"], dict):
422 genreListConf = configDictionary["genre"].keys()
423 for g in genreListConf:
424 subject = opfFile.createElement("dc:subject")
425 subject.appendChild(opfFile.createTextNode(g))
426 opfMeta.appendChild(subject)
427 if "characters" in configDictionary.keys():
428 for name in configDictionary["characters"]:
429 char = opfFile.createElement("dc:subject")
430 char.appendChild(opfFile.createTextNode(name))
431 opfMeta.appendChild(char)
432 if "format" in configDictionary.keys():
433 for formatF in configDictionary["format"]:
434 f = opfFile.createElement("dc:subject")
435 f.appendChild(opfFile.createTextNode(formatF))
436 opfMeta.appendChild(f)
437 if "otherKeywords" in configDictionary.keys():
438 for key in configDictionary["otherKeywords"]:
439 word = opfFile.createElement("dc:subject")
440 word.appendChild(opfFile.createTextNode(key))
441 opfMeta.appendChild(word)
442
443 # Pre-pagination and layout
444 # Comic are always prepaginated.
445
446 elLayout = opfFile.createElement("meta")
447 elLayout.setAttribute("property", "rendition:layout")
448 elLayout.appendChild(opfFile.createTextNode("pre-paginated"))
449 opfMeta.appendChild(elLayout)
450
451 # We should figure out if the pages are portrait or not...
452 elOrientation = opfFile.createElement("meta")
453 elOrientation.setAttribute("property", "rendition:orientation")
454 elOrientation.appendChild(opfFile.createTextNode("portrait"))
455 opfMeta.appendChild(elOrientation)
456
457 elSpread = opfFile.createElement("meta")
458 elSpread.setAttribute("property", "rendition:spread")
459 elSpread.appendChild(opfFile.createTextNode("landscape"))
460 opfMeta.appendChild(elSpread)
461
462 opfRoot.appendChild(opfMeta)
463
464 # Manifest
465
466 opfManifest = opfFile.createElement("manifest")
467 toc = opfFile.createElement("item")
468 toc.setAttribute("id", "ncx")
469 toc.setAttribute("href", "toc.ncx")
470 toc.setAttribute("media-type", "application/x-dtbncx+xml")
471 opfManifest.appendChild(toc)
472
473 region = opfFile.createElement("item")
474 region.setAttribute("id", "regions")
475 region.setAttribute("href", "region-nav.xhtml")
476 region.setAttribute("media-type", "application/xhtml+xml")
477 region.setAttribute("properties", "data-nav") # Set the propernavmap to use this later)
478 opfManifest.appendChild(region)
479
480 nav = opfFile.createElement("item")
481 nav.setAttribute("id", "nav")
482 nav.setAttribute("href", "nav.xhtml")
483 nav.setAttribute("media-type", "application/xhtml+xml")
484 nav.setAttribute("properties", "nav") # Set the propernavmap to use this later)
485 opfManifest.appendChild(nav)
486
487 ids = 0
488 for p in pagesList:
489 item = opfFile.createElement("item")
490 item.setAttribute("id", "img"+str(ids))
491 ids +=1
492 item.setAttribute("href", os.path.relpath(p, str(path)))
493 item.setAttribute("media-type", "image/png")
494 if os.path.basename(p) == os.path.basename(coverpageurl):
495 item.setAttribute("properties", "cover-image")
496 opfManifest.appendChild(item)
497
498
499 ids = 0
500 for p in htmlFiles:
501 item = opfFile.createElement("item")
502 item.setAttribute("id", "p"+str(ids))
503 ids +=1
504 item.setAttribute("href", os.path.relpath(p, str(path)))
505 item.setAttribute("media-type", "application/xhtml+xml")
506 opfManifest.appendChild(item)
507
508
509 opfRoot.appendChild(opfManifest)
510
511 # Spine
512
513 opfSpine = opfFile.createElement("spine")
514 # this sets the table of contents to use the ncx file
515 opfSpine.setAttribute("toc", "ncx")
516 # Reading Direction:
517
518 spreadRight = True
519 direction = 0
520 if "readingDirection" in configDictionary.keys():
521 if configDictionary["readingDirection"] == "rightToLeft":
522 opfSpine.setAttribute("page-progression-direction", "rtl")
523 spreadRight = False
524 direction = 1
525 else:
526 opfSpine.setAttribute("page-progression-direction", "ltr")
527
528 # Here we'd need to switch between the two and if spread keywrod use neither but combine with spread-none
529
530 ids = 0
531 for p in htmlFiles:
532 item = opfFile.createElement("itemref")
533 item.setAttribute("idref", "p"+str(ids))
534 ids +=1
535 props = []
536 if p in listofSpreads:
537 # Put this one in the center.
538 props.append("rendition:page-spread-center")
539
540 # Reset the spread boolean.
541 # It needs to point at the first side after the spread.
542 # So ltr -> spread-left, rtl->spread-right
543 if direction == 0:
544 spreadRight = False
545 else:
546 spreadRight = True
547 else:
548 if spreadRight:
549 props.append("page-spread-right")
550 spreadRight = False
551 else:
552 props.append("page-spread-left")
553 spreadRight = True
554 item.setAttribute("properties", " ".join(props))
555 opfSpine.appendChild(item)
556 opfRoot.appendChild(opfSpine)
557
558 # Guide
559
560 opfGuide = opfFile.createElement("guide")
561 if coverpagehtml is not None and coverpagehtml.isspace() is False and len(coverpagehtml) > 0:
562 item = opfFile.createElement("reference")
563 item.setAttribute("type", "cover")
564 item.setAttribute("title", "Cover")
565 item.setAttribute("href", coverpagehtml)
566 opfGuide.appendChild(item)
567 opfRoot.appendChild(opfGuide)
568
569 docFile = open(str(Path(path / "content.opf")), 'w', newline="", encoding="utf-8")
570 docFile.write(opfFile.toString(indent=2))
571 docFile.close()
572 return str(Path(path / "content.opf"))
573
574"""
575Write a region navmap file.
576"""
577
578def write_region_nav_file(path, configDictionary, htmlFiles, regions = []):
579 navDoc = QDomDocument()
580 navRoot = navDoc.createElement("html")
581 navRoot.setAttribute("xmlns", "http://www.w3.org/1999/xhtml")
582 navRoot.setAttribute("xmlns:epub", "http://www.idpf.org/2007/ops")
583 navDoc.appendChild(navRoot)
584
585 head = navDoc.createElement("head")
586 title = navDoc.createElement("title")
587 title.appendChild(navDoc.createTextNode("Region Navigation"))
588 head.appendChild(title)
589 navRoot.appendChild(head)
590
591 body = navDoc.createElement("body")
592 navRoot.appendChild(body)
593
594 nav = navDoc.createElement("nav")
595 nav.setAttribute("epub:type", "region-based")
596 nav.setAttribute("prefix", "ahl: http://idpf.org/epub/vocab/ahl")
597 body.appendChild(nav)
598
599 # Let's write the panels and balloons down now.
600
601 olPanels = navDoc.createElement("ol")
602 for region in regions:
603 if region["type"] == "panel":
604 pageName = os.path.relpath(region["page"], str(path))
605 print("accessing panel")
606 li = navDoc.createElement("li")
607 li.setAttribute("epub:type", "panel")
608
609 anchor = navDoc.createElement("a")
610 bounds = region["points"]
611 anchor.setAttribute("href", pageName+"#xywh=percent:"+str(bounds.x())+","+str(bounds.y())+","+str(bounds.width())+","+str(bounds.height()))
612
613 if len(region["primaryColor"])>0:
614 primaryC = navDoc.createElement("meta")
615 primaryC.setAttribute("property","ahl:primary-color")
616 primaryC.setAttribute("content", region["primaryColor"])
617 anchor.appendChild(primaryC)
618
619 li.appendChild(anchor)
620 olBalloons = navDoc.createElement("ol")
621
622 """
623 The region nav spec specifies that we should have text-areas/balloons as a refinement on
624 the panel.
625 For each panel, we'll check if there's balloons/text-areas inside, and we'll do that by
626 checking whether the center point is inside the panel because some comics have balloons
627 that overlap the gutters.
628 """
629 for balloon in regions:
630 if balloon["type"] == "text" and balloon["page"] == region["page"] and bounds.contains(balloon["points"].center()):
631 liBalloon = navDoc.createElement("li")
632 liBalloon.setAttribute("epub:type", "text-area")
633
634 anchorBalloon = navDoc.createElement("a")
635 BBounds = balloon["points"]
636 anchorBalloon.setAttribute("href", pageName+"#xywh=percent:"+str(BBounds.x())+","+str(BBounds.y())+","+str(BBounds.width())+","+str(BBounds.height()))
637
638 liBalloon.appendChild(anchorBalloon)
639 olBalloons.appendChild(liBalloon)
640
641 if olBalloons.hasChildNodes():
642 li.appendChild(olBalloons)
643 olPanels.appendChild(li)
644 nav.appendChild(olPanels)
645
646 navFile = open(str(Path(path / "region-nav.xhtml")), 'w', newline="", encoding="utf-8")
647 navFile.write(navDoc.toString(indent=2))
648 navFile.close()
649 return str(Path(path / "region-nav.xhtml"))
650
651"""
652Write XHTML nav file.
653
654This is virtually the same as the NCX file, except that
655the navigation document can be styled, and is what 3.1 and
6563.2 expect as a primary navigation document.
657
658This function will both create a table of contents, using the
659"acbf_title" feature, as well as a regular pageslist.
660"""
661
662def write_nav_file(path, configDictionary, htmlFiles, listOfNavItems):
663 navDoc = QDomDocument()
664 navRoot = navDoc.createElement("html")
665 navRoot.setAttribute("xmlns", "http://www.w3.org/1999/xhtml")
666 navRoot.setAttribute("xmlns:epub", "http://www.idpf.org/2007/ops")
667 navDoc.appendChild(navRoot)
668
669 head = navDoc.createElement("head")
670 title = navDoc.createElement("title")
671 title.appendChild(navDoc.createTextNode("Table of Contents"))
672 head.appendChild(title)
673 navRoot.appendChild(head)
674
675 body = navDoc.createElement("body")
676 navRoot.appendChild(body)
677
678 # The Table of Contents
679
680 toc = navDoc.createElement("nav")
681 toc.setAttribute("epub:type", "toc")
682 oltoc = navDoc.createElement("ol")
683 li = navDoc.createElement("li")
684 anchor = navDoc.createElement("a")
685 anchor.setAttribute("href", os.path.relpath(htmlFiles[0], str(path)))
686 anchor.appendChild(navDoc.createTextNode("Start"))
687 li.appendChild(anchor)
688 oltoc.appendChild(li)
689 for fileName in listOfNavItems.keys():
690 li = navDoc.createElement("li")
691 anchor = navDoc.createElement("a")
692 anchor.setAttribute("href", os.path.relpath(fileName, str(path)))
693 anchor.appendChild(navDoc.createTextNode(listOfNavItems[fileName]))
694 li.appendChild(anchor)
695 oltoc.appendChild(li)
696
697 toc.appendChild(oltoc)
698 body.appendChild(toc)
699
700 # The Pages List.
701
702 pageslist = navDoc.createElement("nav")
703 pageslist.setAttribute("epub:type", "page-list")
704 olpages = navDoc.createElement("ol")
705
706 entry = 1
707 for i in range(len(htmlFiles)):
708 li = navDoc.createElement("li")
709 anchor = navDoc.createElement("a")
710 anchor.setAttribute("href", os.path.relpath(htmlFiles[1], str(path)))
711 anchor.appendChild(navDoc.createTextNode(str(i)))
712 li.appendChild(anchor)
713 olpages.appendChild(li)
714 pageslist.appendChild(olpages)
715
716 body.appendChild(pageslist)
717
718
719
720 navFile = open(str(Path(path / "nav.xhtml")), 'w', newline="", encoding="utf-8")
721 navFile.write(navDoc.toString(indent=2))
722 navFile.close()
723 return str(Path(path / "nav.xhtml"))
724
725"""
726Write a NCX file.
727
728This is the same as the navigation document above, but then
729for 2.0 backward compatibility.
730"""
731
732def write_ncx_file(path, configDictionary, htmlFiles, listOfNavItems):
733 tocDoc = QDomDocument()
734 ncx = tocDoc.createElement("ncx")
735 ncx.setAttribute("version", "2005-1")
736 ncx.setAttribute("xmlns", "http://www.daisy.org/z3986/2005/ncx/")
737 tocDoc.appendChild(ncx)
738
739 tocHead = tocDoc.createElement("head")
740
741 # NCX also has some meta values that are in the head.
742 # They are shared with the opf metadata document.
743
744 uuid = str(configDictionary["uuid"])
745 uuid = uuid.strip("{")
746 uuid = uuid.strip("}")
747 metaID = tocDoc.createElement("meta")
748 metaID.setAttribute("content", uuid)
749 metaID.setAttribute("name", "dtb:uid")
750 tocHead.appendChild(metaID)
751 metaDepth = tocDoc.createElement("meta")
752 metaDepth.setAttribute("content", str(1))
753 metaDepth.setAttribute("name", "dtb:depth")
754 tocHead.appendChild(metaDepth)
755 metaTotal = tocDoc.createElement("meta")
756 metaTotal.setAttribute("content", str(len(htmlFiles)))
757 metaTotal.setAttribute("name", "dtb:totalPageCount")
758 tocHead.appendChild(metaTotal)
759 metaMax = tocDoc.createElement("meta")
760 metaMax.setAttribute("content", str(len(htmlFiles)))
761 metaMax.setAttribute("name", "dtb:maxPageNumber")
762 tocHead.appendChild(metaDepth)
763 ncx.appendChild(tocHead)
764
765 docTitle = tocDoc.createElement("docTitle")
766 text = tocDoc.createElement("text")
767 if "title" in configDictionary.keys():
768 text.appendChild(tocDoc.createTextNode(str(configDictionary["title"])))
769 else:
770 text.appendChild(tocDoc.createTextNode("Comic with no Name"))
771 docTitle.appendChild(text)
772 ncx.appendChild(docTitle)
773
774 # The navmap is a table of contents.
775
776 navmap = tocDoc.createElement("navMap")
777 navPoint = tocDoc.createElement("navPoint")
778 navPoint.setAttribute("id", "navPoint-1")
779 navPoint.setAttribute("playOrder", "1")
780 navLabel = tocDoc.createElement("navLabel")
781 navLabelText = tocDoc.createElement("text")
782 navLabelText.appendChild(tocDoc.createTextNode("Start"))
783 navLabel.appendChild(navLabelText)
784 navContent = tocDoc.createElement("content")
785 navContent.setAttribute("src", os.path.relpath(htmlFiles[0], str(path)))
786 navPoint.appendChild(navLabel)
787 navPoint.appendChild(navContent)
788 navmap.appendChild(navPoint)
789 entry = 1
790 for fileName in listOfNavItems.keys():
791 entry +=1
792 navPointT = tocDoc.createElement("navPoint")
793 navPointT.setAttribute("id", "navPoint-"+str(entry))
794 navPointT.setAttribute("playOrder", str(entry))
795 navLabelT = tocDoc.createElement("navLabel")
796 navLabelTText = tocDoc.createElement("text")
797 navLabelTText.appendChild(tocDoc.createTextNode(listOfNavItems[fileName]))
798 navLabelT.appendChild(navLabelTText)
799 navContentT = tocDoc.createElement("content")
800 navContentT.setAttribute("src", os.path.relpath(fileName, str(path)))
801 navPointT.appendChild(navLabelT)
802 navPointT.appendChild(navContentT)
803 navmap.appendChild(navPointT)
804 ncx.appendChild(navmap)
805
806 # The pages list on the other hand just lists all pages.
807
808 pagesList = tocDoc.createElement("pageList")
809 navLabelPages = tocDoc.createElement("navLabel")
810 navLabelPagesText = tocDoc.createElement("text")
811 navLabelPagesText.appendChild(tocDoc.createTextNode("Pages"))
812 navLabelPages.appendChild(navLabelPagesText)
813 pagesList.appendChild(navLabelPages)
814 for i in range(len(htmlFiles)):
815 pageTarget = tocDoc.createElement("pageTarget")
816 pageTarget.setAttribute("type", "normal")
817 pageTarget.setAttribute("id", "page-"+str(i))
818 pageTarget.setAttribute("value", str(i))
819 navLabelPagesTarget = tocDoc.createElement("navLabel")
820 navLabelPagesTargetText = tocDoc.createElement("text")
821 navLabelPagesTargetText.appendChild(tocDoc.createTextNode(str(i+1)))
822 navLabelPagesTarget.appendChild(navLabelPagesTargetText)
823 pageTarget.appendChild(navLabelPagesTarget)
824 pageTargetContent = tocDoc.createElement("content")
825 pageTargetContent.setAttribute("src", os.path.relpath(htmlFiles[i], str(path)))
826 pageTarget.appendChild(pageTargetContent)
827 pagesList.appendChild(pageTarget)
828 ncx.appendChild(pagesList)
829
830 # Save the document.
831
832 docFile = open(str(Path(path / "toc.ncx")), 'w', newline="", encoding="utf-8")
833 docFile.write(tocDoc.toString(indent=2))
834 docFile.close()
835 return str(Path(path / "toc.ncx"))
write_opf_file(path, configDictionary, htmlFiles, pagesList, coverpageurl, coverpagehtml, listofSpreads)
write_ncx_file(path, configDictionary, htmlFiles, listOfNavItems)
write_nav_file(path, configDictionary, htmlFiles, listOfNavItems)
write_region_nav_file(path, configDictionary, htmlFiles, regions=[])
export(configDictionary={}, projectURL=str(), pagesLocationList=[], pageData=[])