diff --git a/libs/ui/KisReferenceImage.cpp b/libs/ui/KisReferenceImage.cpp index b1c0a774bc..a7312b1f02 100644 --- a/libs/ui/KisReferenceImage.cpp +++ b/libs/ui/KisReferenceImage.cpp @@ -1,370 +1,380 @@ /* * Copyright (C) 2017 Boudewijn Rempt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KisReferenceImage.h" #include #include #include #include #include #include +#include +#include + #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) #include #endif #include #include #include #include #include #include #include #include #include #include #include + struct KisReferenceImage::Private : public QSharedData { // Filename within .kra (for embedding) QString internalFilename; // File on disk (for linking) QString externalFilename; QImage image; QImage cachedImage; KisQImagePyramid mipmap; qreal saturation{1.0}; int id{-1}; bool embed{true}; bool loadFromFile() { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!externalFilename.isEmpty(), false); - bool r = image.load(externalFilename); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(QFileInfo(externalFilename).exists(), false); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(QFileInfo(externalFilename).isReadable(), false); + + QImageReader reader(externalFilename); + reader.setDecideFormatFromContent(true); + image = reader.read(); + // See https://bugs.kde.org/show_bug.cgi?id=416515 -- a jpeg image // loaded into a qimage cannot be saved to png unless we explicitly // convert the colorspace of the QImage #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) image.convertToColorSpace(QColorSpace(QColorSpace::SRgb)); #endif - return r; + return (!image.isNull()); } bool loadFromClipboard() { image = KisClipboardUtil::getImageFromClipboard(); return !image.isNull(); } void updateCache() { if (saturation < 1.0) { cachedImage = KritaUtils::convertQImageToGrayA(image); if (saturation > 0.0) { QPainter gc2(&cachedImage); gc2.setOpacity(saturation); gc2.drawImage(QPoint(), image); } } else { cachedImage = image; } mipmap = KisQImagePyramid(cachedImage); } }; KisReferenceImage::SetSaturationCommand::SetSaturationCommand(const QList &shapes, qreal newSaturation, KUndo2Command *parent) : KUndo2Command(kundo2_i18n("Set saturation"), parent) , newSaturation(newSaturation) { images.reserve(shapes.count()); Q_FOREACH(auto *shape, shapes) { auto *reference = dynamic_cast(shape); KIS_SAFE_ASSERT_RECOVER_BREAK(reference); images.append(reference); } Q_FOREACH(auto *image, images) { oldSaturations.append(image->saturation()); } } void KisReferenceImage::SetSaturationCommand::undo() { auto saturationIterator = oldSaturations.begin(); Q_FOREACH(auto *image, images) { image->setSaturation(*saturationIterator); image->update(); saturationIterator++; } } void KisReferenceImage::SetSaturationCommand::redo() { Q_FOREACH(auto *image, images) { image->setSaturation(newSaturation); image->update(); } } KisReferenceImage::KisReferenceImage() : d(new Private()) { setKeepAspectRatio(true); } KisReferenceImage::KisReferenceImage(const KisReferenceImage &rhs) : KoTosContainer(rhs) , d(rhs.d) {} KisReferenceImage::~KisReferenceImage() {} KisReferenceImage * KisReferenceImage::fromFile(const QString &filename, const KisCoordinatesConverter &converter, QWidget *parent) { KisReferenceImage *reference = new KisReferenceImage(); reference->d->externalFilename = filename; bool ok = reference->d->loadFromFile(); if (ok) { QRect r = QRect(QPoint(), reference->d->image.size()); QSizeF shapeSize = converter.imageToDocument(r).size(); reference->setSize(shapeSize); } else { delete reference; if (parent) { QMessageBox::critical(parent, i18nc("@title:window", "Krita"), i18n("Could not load %1.", filename)); } return nullptr; } return reference; } KisReferenceImage *KisReferenceImage::fromClipboard(const KisCoordinatesConverter &converter) { KisReferenceImage *reference = new KisReferenceImage(); bool ok = reference->d->loadFromClipboard(); if (ok) { QRect r = QRect(QPoint(), reference->d->image.size()); QSizeF size = converter.imageToDocument(r).size(); reference->setSize(size); } else { delete reference; reference = nullptr; } return reference; } void KisReferenceImage::paint(QPainter &gc, KoShapePaintingContext &/*paintcontext*/) const { if (!parent()) return; gc.save(); QSizeF shapeSize = size(); QTransform transform = QTransform::fromScale(shapeSize.width() / d->image.width(), shapeSize.height() / d->image.height()); if (d->cachedImage.isNull()) { // detach the data const_cast(this)->d->updateCache(); } qreal scale; QImage prescaled = d->mipmap.getClosest(transform * gc.transform(), &scale); transform.scale(1.0 / scale, 1.0 / scale); gc.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform); gc.setClipRect(QRectF(QPointF(), shapeSize), Qt::IntersectClip); gc.setTransform(transform, true); gc.drawImage(QPoint(), prescaled); gc.restore(); } void KisReferenceImage::setSaturation(qreal saturation) { d->saturation = saturation; d->cachedImage = QImage(); } qreal KisReferenceImage::saturation() const { return d->saturation; } void KisReferenceImage::setEmbed(bool embed) { KIS_SAFE_ASSERT_RECOVER_RETURN(embed || !d->externalFilename.isEmpty()); d->embed = embed; } bool KisReferenceImage::embed() { return d->embed; } bool KisReferenceImage::hasLocalFile() { return !d->externalFilename.isEmpty(); } QString KisReferenceImage::filename() const { return d->externalFilename; } QString KisReferenceImage::internalFile() const { return d->internalFilename; } void KisReferenceImage::setFilename(const QString &filename) { d->externalFilename = filename; d->embed = false; } QColor KisReferenceImage::getPixel(QPointF position) { if (transparency() == 1.0) return Qt::transparent; const QSizeF shapeSize = size(); const QTransform scale = QTransform::fromScale(d->image.width() / shapeSize.width(), d->image.height() / shapeSize.height()); const QTransform transform = absoluteTransformation().inverted() * scale; const QPointF localPosition = position * transform; if (d->cachedImage.isNull()) { d->updateCache(); } return d->cachedImage.pixelColor(localPosition.toPoint()); } void KisReferenceImage::saveXml(QDomDocument &document, QDomElement &parentElement, int id) { d->id = id; QDomElement element = document.createElement("referenceimage"); if (d->embed) { d->internalFilename = QString("reference_images/%1.png").arg(id); } const QString src = d->embed ? d->internalFilename : (QString("file://") + d->externalFilename); element.setAttribute("src", src); const QSizeF &shapeSize = size(); element.setAttribute("width", KisDomUtils::toString(shapeSize.width())); element.setAttribute("height", KisDomUtils::toString(shapeSize.height())); element.setAttribute("keepAspectRatio", keepAspectRatio() ? "true" : "false"); element.setAttribute("transform", SvgUtil::transformToString(transform())); element.setAttribute("opacity", KisDomUtils::toString(1.0 - transparency())); element.setAttribute("saturation", KisDomUtils::toString(d->saturation)); parentElement.appendChild(element); } KisReferenceImage * KisReferenceImage::fromXml(const QDomElement &elem) { auto *reference = new KisReferenceImage(); const QString &src = elem.attribute("src"); if (src.startsWith("file://")) { reference->d->externalFilename = src.mid(7); reference->d->embed = false; } else { reference->d->internalFilename = src; reference->d->embed = true; } qreal width = KisDomUtils::toDouble(elem.attribute("width", "100")); qreal height = KisDomUtils::toDouble(elem.attribute("height", "100")); reference->setSize(QSizeF(width, height)); reference->setKeepAspectRatio(elem.attribute("keepAspectRatio", "true").toLower() == "true"); auto transform = SvgTransformParser(elem.attribute("transform")).transform(); reference->setTransformation(transform); qreal opacity = KisDomUtils::toDouble(elem.attribute("opacity", "1")); reference->setTransparency(1.0 - opacity); qreal saturation = KisDomUtils::toDouble(elem.attribute("saturation", "1")); reference->setSaturation(saturation); return reference; } bool KisReferenceImage::saveImage(KoStore *store) const { if (!d->embed) return true; if (!store->open(d->internalFilename)) { return false; } bool saved = false; KoStoreDevice storeDev(store); if (storeDev.open(QIODevice::WriteOnly)) { saved = d->image.save(&storeDev, "PNG"); } return store->close() && saved; } bool KisReferenceImage::loadImage(KoStore *store) { if (!d->embed) { return d->loadFromFile(); } if (!store->open(d->internalFilename)) { return false; } KoStoreDevice storeDev(store); if (!storeDev.open(QIODevice::ReadOnly)) { return false; } if (!d->image.load(&storeDev, "PNG")) { return false; } return store->close(); } KoShape *KisReferenceImage::cloneShape() const { return new KisReferenceImage(*this); } diff --git a/plugins/python/comics_project_management_tools/comics_export_dialog.py b/plugins/python/comics_project_management_tools/comics_export_dialog.py index b821644d37..0c58b61a6f 100644 --- a/plugins/python/comics_project_management_tools/comics_export_dialog.py +++ b/plugins/python/comics_project_management_tools/comics_export_dialog.py @@ -1,763 +1,763 @@ """ 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 . """ """ A dialog for editing the exporter settings. """ from enum import IntEnum from PyQt5.QtGui import QStandardItem, QStandardItemModel, QColor, QFont, QIcon, QPixmap from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QGroupBox, QFormLayout, QCheckBox, QComboBox, QSpinBox, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget, QPushButton, QLineEdit, QLabel, QListView, QTableView, QFontComboBox, QSpacerItem, QColorDialog, QStyledItemDelegate from PyQt5.QtCore import Qt, QUuid from krita import * """ A generic widget to make selecting size easier. It works by initialising with a config name(like "scale"), and then optionally setting the config with a dictionary. Then, afterwards, you get the config with a dictionary, with the config name being the entry the values are under. """ class comic_export_resize_widget(QGroupBox): configName = "" def __init__(self, configName, batch=False, fileType=True): super().__init__() self.configName = configName self.setTitle(i18n("Adjust Working File")) formLayout = QFormLayout() self.setLayout(formLayout) self.crop = QCheckBox(i18n("Crop files before resize")) self.cmbFile = QComboBox() self.cmbFile.addItems(["png", "jpg", "webp"]) self.resizeMethod = QComboBox() self.resizeMethod.addItems([i18n("Percentage"), i18n("DPI"), i18n("Maximum Width"), i18n("Maximum Height")]) self.resizeMethod.currentIndexChanged.connect(self.slot_set_enabled) self.spn_DPI = QSpinBox() self.spn_DPI.setMaximum(1200) self.spn_DPI.setSuffix(i18n(" DPI")) self.spn_DPI.setValue(72) self.spn_PER = QSpinBox() if batch is True: self.spn_PER.setMaximum(1000) else: self.spn_PER.setMaximum(100) self.spn_PER.setSuffix(" %") self.spn_PER.setValue(100) self.spn_width = QSpinBox() self.spn_width.setMaximum(99999) self.spn_width.setSuffix(" px") self.spn_width.setValue(800) self.spn_height = QSpinBox() self.spn_height.setMaximum(99999) self.spn_height.setSuffix(" px") self.spn_height.setValue(800) if batch is False: formLayout.addRow("", self.crop) if fileType is True and configName != "TIFF": formLayout.addRow(i18n("File Type"), self.cmbFile) formLayout.addRow(i18n("Method:"), self.resizeMethod) formLayout.addRow(i18n("DPI:"), self.spn_DPI) formLayout.addRow(i18n("Percentage:"), self.spn_PER) formLayout.addRow(i18n("Width:"), self.spn_width) formLayout.addRow(i18n("Height:"), self.spn_height) self.slot_set_enabled() def slot_set_enabled(self): method = self.resizeMethod.currentIndex() self.spn_DPI.setEnabled(False) self.spn_PER.setEnabled(False) self.spn_width.setEnabled(False) self.spn_height.setEnabled(False) - if method is 0: + if method == 0: self.spn_PER.setEnabled(True) - if method is 1: + if method == 1: self.spn_DPI.setEnabled(True) - if method is 2: + if method == 2: self.spn_width.setEnabled(True) - if method is 3: + if method == 3: self.spn_height.setEnabled(True) def set_config(self, config): if self.configName in config.keys(): mConfig = config[self.configName] if "Method" in mConfig.keys(): self.resizeMethod.setCurrentIndex(mConfig["Method"]) if "FileType" in mConfig.keys(): self.cmbFile.setCurrentText(mConfig["FileType"]) if "Crop" in mConfig.keys(): self.crop.setChecked(mConfig["Crop"]) if "DPI" in mConfig.keys(): self.spn_DPI.setValue(mConfig["DPI"]) if "Percentage" in mConfig.keys(): self.spn_PER.setValue(mConfig["Percentage"]) if "Width" in mConfig.keys(): self.spn_width.setValue(mConfig["Width"]) if "Height" in mConfig.keys(): self.spn_height.setValue(mConfig["Height"]) self.slot_set_enabled() def get_config(self, config): mConfig = {} mConfig["Method"] = self.resizeMethod.currentIndex() if self.configName == "TIFF": mConfig["FileType"] = "tiff" else: mConfig["FileType"] = self.cmbFile.currentText() mConfig["Crop"] = self.crop.isChecked() mConfig["DPI"] = self.spn_DPI.value() mConfig["Percentage"] = self.spn_PER.value() mConfig["Width"] = self.spn_width.value() mConfig["Height"] = self.spn_height.value() config[self.configName] = mConfig return config """ Quick combobox for selecting the color label. """ class labelSelector(QComboBox): def __init__(self): super(labelSelector, self).__init__() lisOfColors = [] lisOfColors.append(Qt.transparent) lisOfColors.append(QColor(91, 173, 220)) lisOfColors.append(QColor(151, 202, 63)) lisOfColors.append(QColor(247, 229, 61)) lisOfColors.append(QColor(255, 170, 63)) lisOfColors.append(QColor(177, 102, 63)) lisOfColors.append(QColor(238, 50, 51)) lisOfColors.append(QColor(191, 106, 209)) lisOfColors.append(QColor(118, 119, 114)) self.itemModel = QStandardItemModel() for color in lisOfColors: item = QStandardItem() item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) item.setCheckState(Qt.Unchecked) item.setText(" ") item.setData(color, Qt.BackgroundColorRole) self.itemModel.appendRow(item) self.setModel(self.itemModel) def getLabels(self): listOfIndexes = [] for i in range(self.itemModel.rowCount()): index = self.itemModel.index(i, 0) item = self.itemModel.itemFromIndex(index) if item.checkState(): listOfIndexes.append(i) return listOfIndexes def setLabels(self, listOfIndexes): for i in listOfIndexes: index = self.itemModel.index(i, 0) item = self.itemModel.itemFromIndex(index) item.setCheckState(True) """ Little Enum to keep track of where in the item we add styles. """ class styleEnum(IntEnum): FONT = Qt.UserRole + 1 FONTLIST = Qt.UserRole + 2 FONTGENERIC = Qt.UserRole + 3 BOLD = Qt.UserRole + 4 ITALIC = Qt.UserRole + 5 """ A simple delegate to allows editing fonts with a QFontComboBox """ class font_list_delegate(QStyledItemDelegate): def __init__(self, parent=None): super(QStyledItemDelegate, self).__init__(parent) def createEditor(self, parent, option, index): editor = QFontComboBox(parent) return editor """ The comic export settings dialog will allow configuring the export. This config consists of... * Crop settings. for removing bleeds. * Selecting layer labels to remove. * Choosing which formats to export to. * Choosing how to resize these * Whether to crop. * Which file type to use. And for ACBF, it gives the ability to edit acbf document info. """ class comic_export_setting_dialog(QDialog): acbfStylesList = ["speech", "commentary", "formal", "letter", "code", "heading", "audio", "thought", "sign", "sound", "emphasis", "strong"] def __init__(self): super().__init__() self.setLayout(QVBoxLayout()) self.setWindowTitle(i18n("Export Settings")) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) mainWidget = QTabWidget() self.layout().addWidget(mainWidget) self.layout().addWidget(buttons) # Set basic crop settings # Set which layers to remove before export. mainExportSettings = QWidget() mainExportSettings.setLayout(QVBoxLayout()) groupExportCrop = QGroupBox(i18n("Crop Settings")) formCrop = QFormLayout() groupExportCrop.setLayout(formCrop) self.chk_toOutmostGuides = QCheckBox(i18n("Crop to outmost guides")) self.chk_toOutmostGuides.setChecked(True) self.chk_toOutmostGuides.setToolTip(i18n("This will crop to the outmost guides if possible and otherwise use the underlying crop settings.")) formCrop.addRow("", self.chk_toOutmostGuides) btn_fromSelection = QPushButton(i18n("Set Margins from Active Selection")) btn_fromSelection.clicked.connect(self.slot_set_margin_from_selection) # This doesn't work. formCrop.addRow("", btn_fromSelection) self.spn_marginLeft = QSpinBox() self.spn_marginLeft.setMaximum(99999) self.spn_marginLeft.setSuffix(" px") formCrop.addRow(i18n("Left:"), self.spn_marginLeft) self.spn_marginTop = QSpinBox() self.spn_marginTop.setMaximum(99999) self.spn_marginTop.setSuffix(" px") formCrop.addRow(i18n("Top:"), self.spn_marginTop) self.spn_marginRight = QSpinBox() self.spn_marginRight.setMaximum(99999) self.spn_marginRight.setSuffix(" px") formCrop.addRow(i18n("Right:"), self.spn_marginRight) self.spn_marginBottom = QSpinBox() self.spn_marginBottom.setMaximum(99999) self.spn_marginBottom.setSuffix(" px") formCrop.addRow(i18n("Bottom:"), self.spn_marginBottom) groupExportLayers = QGroupBox(i18n("Layers")) formLayers = QFormLayout() groupExportLayers.setLayout(formLayers) self.cmbLabelsRemove = labelSelector() formLayers.addRow(i18n("Label for removal:"), self.cmbLabelsRemove) self.ln_text_layer_name = QLineEdit() self.ln_text_layer_name.setToolTip(i18n("These are keywords that can be used to identify text layers. A layer only needs to contain the keyword to be recognized. Keywords should be comma separated.")) self.ln_panel_layer_name = QLineEdit() self.ln_panel_layer_name.setToolTip(i18n("These are keywords that can be used to identify panel layers. A layer only needs to contain the keyword to be recognized. Keywords should be comma separated.")) formLayers.addRow(i18n("Text Layer Key:"), self.ln_text_layer_name) formLayers.addRow(i18n("Panel Layer Key:"), self.ln_panel_layer_name) mainExportSettings.layout().addWidget(groupExportCrop) mainExportSettings.layout().addWidget(groupExportLayers) mainWidget.addTab(mainExportSettings, i18n("General")) # CBZ, crop, resize, which metadata to add. CBZexportSettings = QWidget() CBZexportSettings.setLayout(QVBoxLayout()) self.CBZactive = QCheckBox(i18n("Export to CBZ")) CBZexportSettings.layout().addWidget(self.CBZactive) self.CBZgroupResize = comic_export_resize_widget("CBZ") CBZexportSettings.layout().addWidget(self.CBZgroupResize) self.CBZactive.clicked.connect(self.CBZgroupResize.setEnabled) CBZgroupMeta = QGroupBox(i18n("Metadata to Add")) # CBZexportSettings.layout().addWidget(CBZgroupMeta) CBZgroupMeta.setLayout(QFormLayout()) mainWidget.addTab(CBZexportSettings, i18n("CBZ")) # ACBF, crop, resize, creator name, version history, panel layer, text layers. ACBFExportSettings = QWidget() ACBFform = QFormLayout() ACBFExportSettings.setLayout(QVBoxLayout()) ACBFdocInfo = QGroupBox() ACBFdocInfo.setTitle(i18n("ACBF Document Info")) ACBFdocInfo.setLayout(ACBFform) self.lnACBFID = QLabel() self.lnACBFID.setToolTip(i18n("By default this will be filled with a generated universal unique identifier. The ID by itself is merely so that comic book library management programs can figure out if this particular comic is already in their database and whether it has been rated. Of course, the UUID can be changed into something else by manually changing the JSON, but this is advanced usage.")) self.spnACBFVersion = QSpinBox() self.ACBFhistoryModel = QStandardItemModel() acbfHistoryList = QListView() acbfHistoryList.setModel(self.ACBFhistoryModel) btn_add_history = QPushButton(i18n("Add History Entry")) btn_add_history.clicked.connect(self.slot_add_history_item) self.chkIncludeTranslatorComments = QCheckBox() self.chkIncludeTranslatorComments.setText(i18n("Include translator's comments")) self.chkIncludeTranslatorComments.setToolTip(i18n("A PO file can contain translator's comments. If this is checked, the translations comments will be added as references into the ACBF file.")) self.lnTranslatorHeader = QLineEdit() ACBFform.addRow(i18n("ACBF UID:"), self.lnACBFID) ACBFform.addRow(i18n("Version:"), self.spnACBFVersion) ACBFform.addRow(i18n("Version history:"), acbfHistoryList) ACBFform.addRow("", btn_add_history) ACBFform.addRow("", self.chkIncludeTranslatorComments) ACBFform.addRow(i18n("Translator header:"), self.lnTranslatorHeader) ACBFAuthorInfo = QWidget() acbfAVbox = QVBoxLayout(ACBFAuthorInfo) infoLabel = QLabel(i18n("The people responsible for the generation of the CBZ/ACBF files.")) infoLabel.setWordWrap(True) ACBFAuthorInfo.layout().addWidget(infoLabel) self.ACBFauthorModel = QStandardItemModel(0, 6) labels = [i18n("Nick Name"), i18n("Given Name"), i18n("Middle Name"), i18n("Family Name"), i18n("Email"), i18n("Homepage")] self.ACBFauthorModel.setHorizontalHeaderLabels(labels) self.ACBFauthorTable = QTableView() acbfAVbox.addWidget(self.ACBFauthorTable) self.ACBFauthorTable.setModel(self.ACBFauthorModel) self.ACBFauthorTable.verticalHeader().setDragEnabled(True) self.ACBFauthorTable.verticalHeader().setDropIndicatorShown(True) self.ACBFauthorTable.verticalHeader().setSectionsMovable(True) self.ACBFauthorTable.verticalHeader().sectionMoved.connect(self.slot_reset_author_row_visual) AuthorButtons = QHBoxLayout() btn_add_author = QPushButton(i18n("Add Author")) btn_add_author.clicked.connect(self.slot_add_author) AuthorButtons.addWidget(btn_add_author) btn_remove_author = QPushButton(i18n("Remove Author")) btn_remove_author.clicked.connect(self.slot_remove_author) AuthorButtons.addWidget(btn_remove_author) acbfAVbox.addLayout(AuthorButtons) ACBFStyle = QWidget() ACBFStyle.setLayout(QHBoxLayout()) self.ACBFStylesModel = QStandardItemModel() self.ACBFStyleClass = QListView() self.ACBFStyleClass.setModel(self.ACBFStylesModel) ACBFStyle.layout().addWidget(self.ACBFStyleClass) ACBFStyleEdit = QWidget() ACBFStyleEditVB = QVBoxLayout(ACBFStyleEdit) self.ACBFuseFont = QCheckBox(i18n("Use font")) self.ACBFFontList = QListView() self.ACBFFontList.setItemDelegate(font_list_delegate()) self.ACBFuseFont.toggled.connect(self.font_slot_enable_font_view) self.ACBFFontListModel = QStandardItemModel() self.ACBFFontListModel.rowsRemoved.connect(self.slot_font_current_style) self.ACBFFontListModel.itemChanged.connect(self.slot_font_current_style) self.btnAcbfAddFont = QPushButton() self.btnAcbfAddFont.setIcon(Application.icon("list-add")) self.btnAcbfAddFont.clicked.connect(self.font_slot_add_font) self.btn_acbf_remove_font = QPushButton() self.btn_acbf_remove_font.setIcon(Application.icon("edit-delete")) self.btn_acbf_remove_font.clicked.connect(self.font_slot_remove_font) self.ACBFFontList.setModel(self.ACBFFontListModel) self.ACBFdefaultFont = QComboBox() self.ACBFdefaultFont.addItems(["sans-serif", "serif", "monospace", "cursive", "fantasy"]) acbfFontButtons = QHBoxLayout() acbfFontButtons.addWidget(self.btnAcbfAddFont) acbfFontButtons.addWidget(self.btn_acbf_remove_font) self.ACBFBold = QCheckBox(i18n("Bold")) self.ACBFItal = QCheckBox(i18n("Italic")) self.ACBFStyleClass.clicked.connect(self.slot_set_style) self.ACBFStyleClass.selectionModel().selectionChanged.connect(self.slot_set_style) self.ACBFStylesModel.itemChanged.connect(self.slot_set_style) self.ACBFBold.toggled.connect(self.slot_font_current_style) self.ACBFItal.toggled.connect(self.slot_font_current_style) colorWidget = QGroupBox(self) colorWidget.setTitle(i18n("Text Colors")) colorWidget.setLayout(QVBoxLayout()) self.regularColor = QColorDialog() self.invertedColor = QColorDialog() self.btn_acbfRegColor = QPushButton(i18n("Regular Text"), self) self.btn_acbfRegColor.clicked.connect(self.slot_change_regular_color) self.btn_acbfInvColor = QPushButton(i18n("Inverted Text"), self) self.btn_acbfInvColor.clicked.connect(self.slot_change_inverted_color) colorWidget.layout().addWidget(self.btn_acbfRegColor) colorWidget.layout().addWidget(self.btn_acbfInvColor) ACBFStyleEditVB.addWidget(colorWidget) ACBFStyleEditVB.addWidget(self.ACBFuseFont) ACBFStyleEditVB.addWidget(self.ACBFFontList) ACBFStyleEditVB.addLayout(acbfFontButtons) ACBFStyleEditVB.addWidget(self.ACBFdefaultFont) ACBFStyleEditVB.addWidget(self.ACBFBold) ACBFStyleEditVB.addWidget(self.ACBFItal) ACBFStyleEditVB.addStretch() ACBFStyle.layout().addWidget(ACBFStyleEdit) ACBFTabwidget = QTabWidget() ACBFTabwidget.addTab(ACBFdocInfo, i18n("Document Info")) ACBFTabwidget.addTab(ACBFAuthorInfo, i18n("Author Info")) ACBFTabwidget.addTab(ACBFStyle, i18n("Style Sheet")) ACBFExportSettings.layout().addWidget(ACBFTabwidget) mainWidget.addTab(ACBFExportSettings, i18n("ACBF")) # Epub export, crop, resize, other questions. EPUBexportSettings = QWidget() EPUBexportSettings.setLayout(QVBoxLayout()) self.EPUBactive = QCheckBox(i18n("Export to EPUB")) EPUBexportSettings.layout().addWidget(self.EPUBactive) self.EPUBgroupResize = comic_export_resize_widget("EPUB") EPUBexportSettings.layout().addWidget(self.EPUBgroupResize) self.EPUBactive.clicked.connect(self.EPUBgroupResize.setEnabled) mainWidget.addTab(EPUBexportSettings, i18n("EPUB")) # For Print. Crop, no resize. TIFFExportSettings = QWidget() TIFFExportSettings.setLayout(QVBoxLayout()) self.TIFFactive = QCheckBox(i18n("Export to TIFF")) TIFFExportSettings.layout().addWidget(self.TIFFactive) self.TIFFgroupResize = comic_export_resize_widget("TIFF") TIFFExportSettings.layout().addWidget(self.TIFFgroupResize) self.TIFFactive.clicked.connect(self.TIFFgroupResize.setEnabled) mainWidget.addTab(TIFFExportSettings, i18n("TIFF")) # SVG, crop, resize, embed vs link. #SVGExportSettings = QWidget() #mainWidget.addTab(SVGExportSettings, i18n("SVG")) """ Add a history item to the acbf version history list. """ def slot_add_history_item(self): newItem = QStandardItem() newItem.setText(str(i18n("v{version}-in this version...")).format(version=str(self.spnACBFVersion.value()))) self.ACBFhistoryModel.appendRow(newItem) """ Get the margins by treating the active selection in a document as the trim area. This allows people to snap selections to a vector or something, and then get the margins. """ def slot_set_margin_from_selection(self): doc = Application.activeDocument() if doc is not None: if doc.selection() is not None: self.spn_marginLeft.setValue(doc.selection().x()) self.spn_marginTop.setValue(doc.selection().y()) self.spn_marginRight.setValue(doc.width() - (doc.selection().x() + doc.selection().width())) self.spn_marginBottom.setValue(doc.height() - (doc.selection().y() + doc.selection().height())) """ Add an author with default values initialised. """ def slot_add_author(self): listItems = [] listItems.append(QStandardItem(i18n("Anon"))) # Nick name listItems.append(QStandardItem(i18n("John"))) # First name listItems.append(QStandardItem()) # Middle name listItems.append(QStandardItem(i18n("Doe"))) # Last name listItems.append(QStandardItem()) # email listItems.append(QStandardItem()) # homepage self.ACBFauthorModel.appendRow(listItems) """ Remove the selected author from the author list. """ def slot_remove_author(self): self.ACBFauthorModel.removeRow(self.ACBFauthorTable.currentIndex().row()) """ Ensure that the drag and drop of authors doesn't mess up the labels. """ def slot_reset_author_row_visual(self): headerLabelList = [] for i in range(self.ACBFauthorTable.verticalHeader().count()): headerLabelList.append(str(i)) for i in range(self.ACBFauthorTable.verticalHeader().count()): logicalI = self.ACBFauthorTable.verticalHeader().logicalIndex(i) headerLabelList[logicalI] = str(i + 1) self.ACBFauthorModel.setVerticalHeaderLabels(headerLabelList) """ Set the style item to the gui item's style. """ def slot_set_style(self): index = self.ACBFStyleClass.currentIndex() if index.isValid(): item = self.ACBFStylesModel.item(index.row()) fontUsed = item.data(role=styleEnum.FONT) if fontUsed is not None: self.ACBFuseFont.setChecked(fontUsed) else: self.ACBFuseFont.setChecked(False) self.font_slot_enable_font_view() fontList = item.data(role=styleEnum.FONTLIST) self.ACBFFontListModel.clear() for font in fontList: NewItem = QStandardItem(font) NewItem.setEditable(True) self.ACBFFontListModel.appendRow(NewItem) self.ACBFdefaultFont.setCurrentText(str(item.data(role=styleEnum.FONTGENERIC))) bold = item.data(role=styleEnum.BOLD) if bold is not None: self.ACBFBold.setChecked(bold) else: self.ACBFBold.setChecked(False) italic = item.data(role=styleEnum.ITALIC) if italic is not None: self.ACBFItal.setChecked(italic) else: self.ACBFItal.setChecked(False) """ Set the gui items to the currently selected style. """ def slot_font_current_style(self): index = self.ACBFStyleClass.currentIndex() if index.isValid(): item = self.ACBFStylesModel.item(index.row()) fontList = [] for row in range(self.ACBFFontListModel.rowCount()): font = self.ACBFFontListModel.item(row) fontList.append(font.text()) item.setData(self.ACBFuseFont.isChecked(), role=styleEnum.FONT) item.setData(fontList, role=styleEnum.FONTLIST) item.setData(self.ACBFdefaultFont.currentText(), role=styleEnum.FONTGENERIC) item.setData(self.ACBFBold.isChecked(), role=styleEnum.BOLD) item.setData(self.ACBFItal.isChecked(), role=styleEnum.ITALIC) self.ACBFStylesModel.setItem(index.row(), item) """ Change the regular color """ def slot_change_regular_color(self): if (self.regularColor.exec_() == QDialog.Accepted): square = QPixmap(32, 32) square.fill(self.regularColor.currentColor()) self.btn_acbfRegColor.setIcon(QIcon(square)) """ change the inverted color """ def slot_change_inverted_color(self): if (self.invertedColor.exec_() == QDialog.Accepted): square = QPixmap(32, 32) square.fill(self.invertedColor.currentColor()) self.btn_acbfInvColor.setIcon(QIcon(square)) def font_slot_enable_font_view(self): self.ACBFFontList.setEnabled(self.ACBFuseFont.isChecked()) self.btn_acbf_remove_font.setEnabled(self.ACBFuseFont.isChecked()) self.btnAcbfAddFont.setEnabled(self.ACBFuseFont.isChecked()) self.ACBFdefaultFont.setEnabled(self.ACBFuseFont.isChecked()) if self.ACBFFontListModel.rowCount() < 2: self.btn_acbf_remove_font.setEnabled(False) def font_slot_add_font(self): NewItem = QStandardItem(QFont().family()) NewItem.setEditable(True) self.ACBFFontListModel.appendRow(NewItem) def font_slot_remove_font(self): index = self.ACBFFontList.currentIndex() if index.isValid(): self.ACBFFontListModel.removeRow(index.row()) if self.ACBFFontListModel.rowCount() < 2: self.btn_acbf_remove_font.setEnabled(False) """ Load the UI values from the config dictionary given. """ def setConfig(self, config): if "cropToGuides" in config.keys(): self.chk_toOutmostGuides.setChecked(config["cropToGuides"]) if "cropLeft" in config.keys(): self.spn_marginLeft.setValue(config["cropLeft"]) if "cropTop" in config.keys(): self.spn_marginTop.setValue(config["cropTop"]) if "cropRight" in config.keys(): self.spn_marginRight.setValue(config["cropRight"]) if "cropBottom" in config.keys(): self.spn_marginBottom.setValue(config["cropBottom"]) if "labelsToRemove" in config.keys(): self.cmbLabelsRemove.setLabels(config["labelsToRemove"]) if "textLayerNames" in config.keys(): self.ln_text_layer_name.setText(", ".join(config["textLayerNames"])) else: self.ln_text_layer_name.setText("text") if "panelLayerNames" in config.keys(): self.ln_panel_layer_name.setText(", ".join(config["panelLayerNames"])) else: self.ln_panel_layer_name.setText("panels") self.CBZgroupResize.set_config(config) if "CBZactive" in config.keys(): self.CBZactive.setChecked(config["CBZactive"]) self.EPUBgroupResize.set_config(config) if "EPUBactive" in config.keys(): self.EPUBactive.setChecked(config["EPUBactive"]) self.TIFFgroupResize.set_config(config) if "TIFFactive" in config.keys(): self.TIFFactive.setChecked(config["TIFFactive"]) if "acbfAuthor" in config.keys(): if isinstance(config["acbfAuthor"], list): for author in config["acbfAuthor"]: listItems = [] listItems.append(QStandardItem(author.get("nickname", ""))) listItems.append(QStandardItem(author.get("first-name", ""))) listItems.append(QStandardItem(author.get("initials", ""))) listItems.append(QStandardItem(author.get("last-name", ""))) listItems.append(QStandardItem(author.get("email", ""))) listItems.append(QStandardItem(author.get("homepage", ""))) self.ACBFauthorModel.appendRow(listItems) pass else: listItems = [] listItems.append(QStandardItem(config["acbfAuthor"])) # Nick name for i in range(0, 5): listItems.append(QStandardItem()) # First name self.ACBFauthorModel.appendRow(listItems) if "uuid" in config.keys(): self.lnACBFID.setText(config["uuid"]) elif "acbfID" in config.keys(): self.lnACBFID.setText(config["acbfID"]) else: config["uuid"] = QUuid.createUuid().toString() self.lnACBFID.setText(config["uuid"]) if "acbfVersion" in config.keys(): self.spnACBFVersion.setValue(config["acbfVersion"]) if "acbfHistory" in config.keys(): for h in config["acbfHistory"]: item = QStandardItem() item.setText(h) self.ACBFhistoryModel.appendRow(item) if "acbfStyles" in config.keys(): styleDict = config.get("acbfStyles", {}) for key in self.acbfStylesList: keyDict = styleDict.get(key, {}) style = QStandardItem(key.title()) style.setCheckable(True) if key in styleDict.keys(): style.setCheckState(Qt.Checked) else: style.setCheckState(Qt.Unchecked) fontOn = False if "font" in keyDict.keys() or "genericfont" in keyDict.keys(): fontOn = True style.setData(fontOn, role=styleEnum.FONT) if "font" in keyDict: fontlist = keyDict["font"] if isinstance(fontlist, list): font = keyDict.get("font", QFont().family()) style.setData(font, role=styleEnum.FONTLIST) else: style.setData([fontlist], role=styleEnum.FONTLIST) else: style.setData([QFont().family()], role=styleEnum.FONTLIST) style.setData(keyDict.get("genericfont", "sans-serif"), role=styleEnum.FONTGENERIC) style.setData(keyDict.get("bold", False), role=styleEnum.BOLD) style.setData(keyDict.get("ital", False), role=styleEnum.ITALIC) self.ACBFStylesModel.appendRow(style) keyDict = styleDict.get("general", {}) self.regularColor.setCurrentColor(QColor(keyDict.get("color", "#000000"))) square = QPixmap(32, 32) square.fill(self.regularColor.currentColor()) self.btn_acbfRegColor.setIcon(QIcon(square)) keyDict = styleDict.get("inverted", {}) self.invertedColor.setCurrentColor(QColor(keyDict.get("color", "#FFFFFF"))) square.fill(self.invertedColor.currentColor()) self.btn_acbfInvColor.setIcon(QIcon(square)) else: for key in self.acbfStylesList: style = QStandardItem(key.title()) style.setCheckable(True) style.setCheckState(Qt.Unchecked) style.setData(False, role=styleEnum.FONT) style.setData(QFont().family(), role=styleEnum.FONTLIST) style.setData("sans-serif", role=styleEnum.FONTGENERIC) style.setData(False, role=styleEnum.BOLD) #Bold style.setData(False, role=styleEnum.ITALIC) #Italic self.ACBFStylesModel.appendRow(style) self.CBZgroupResize.setEnabled(self.CBZactive.isChecked()) self.lnTranslatorHeader.setText(config.get("translatorHeader", "Translator's Notes")) self.chkIncludeTranslatorComments.setChecked(config.get("includeTranslComment", False)) """ Store the GUI values into the config dictionary given. @return the config diactionary filled with new values. """ def getConfig(self, config): config["cropToGuides"] = self.chk_toOutmostGuides.isChecked() config["cropLeft"] = self.spn_marginLeft.value() config["cropTop"] = self.spn_marginTop.value() config["cropBottom"] = self.spn_marginRight.value() config["cropRight"] = self.spn_marginBottom.value() config["labelsToRemove"] = self.cmbLabelsRemove.getLabels() config["CBZactive"] = self.CBZactive.isChecked() config = self.CBZgroupResize.get_config(config) config["EPUBactive"] = self.EPUBactive.isChecked() config = self.EPUBgroupResize.get_config(config) config["TIFFactive"] = self.TIFFactive.isChecked() config = self.TIFFgroupResize.get_config(config) authorList = [] for row in range(self.ACBFauthorTable.verticalHeader().count()): logicalIndex = self.ACBFauthorTable.verticalHeader().logicalIndex(row) listEntries = ["nickname", "first-name", "initials", "last-name", "email", "homepage"] author = {} for i in range(len(listEntries)): entry = self.ACBFauthorModel.data(self.ACBFauthorModel.index(logicalIndex, i)) if entry is None: entry = " " if entry.isspace() is False and len(entry) > 0: author[listEntries[i]] = entry elif listEntries[i] in author.keys(): author.pop(listEntries[i]) authorList.append(author) config["acbfAuthor"] = authorList config["acbfVersion"] = self.spnACBFVersion.value() versionList = [] for r in range(self.ACBFhistoryModel.rowCount()): index = self.ACBFhistoryModel.index(r, 0) versionList.append(self.ACBFhistoryModel.data(index, Qt.DisplayRole)) config["acbfHistory"] = versionList acbfStylesDict = {} for row in range(0, self.ACBFStylesModel.rowCount()): entry = self.ACBFStylesModel.item(row) if entry.checkState() == Qt.Checked: key = entry.text().lower() style = {} if entry.data(role=styleEnum.FONT): font = entry.data(role=styleEnum.FONTLIST) if font is not None: style["font"] = font genericfont = entry.data(role=styleEnum.FONTGENERIC) if font is not None: style["genericfont"] = genericfont bold = entry.data(role=styleEnum.BOLD) if bold is not None: style["bold"] = bold italic = entry.data(role=styleEnum.ITALIC) if italic is not None: style["ital"] = italic acbfStylesDict[key] = style acbfStylesDict["general"] = {"color": self.regularColor.currentColor().name()} acbfStylesDict["inverted"] = {"color": self.invertedColor.currentColor().name()} config["acbfStyles"] = acbfStylesDict config["translatorHeader"] = self.lnTranslatorHeader.text() config["includeTranslComment"] = self.chkIncludeTranslatorComments.isChecked() # Turn this into something that retrieves from a line-edit when string freeze is over. config["textLayerNames"] = self.ln_text_layer_name.text().split(",") config["panelLayerNames"] = self.ln_panel_layer_name.text().split(",") return config diff --git a/plugins/python/comics_project_management_tools/comics_exporter.py b/plugins/python/comics_project_management_tools/comics_exporter.py index f616f92feb..a5f43e6ef5 100644 --- a/plugins/python/comics_project_management_tools/comics_exporter.py +++ b/plugins/python/comics_project_management_tools/comics_exporter.py @@ -1,651 +1,651 @@ """ 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: + if method == 0: # percentage percentage = config["Percentage"] / 100 listScaleTo[0] = round(oldWidth * percentage) listScaleTo[1] = round(oldHeight * percentage) - if method is 1: + if method == 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: + if method == 2: # maximum width width = config["Width"] listScaleTo[0] = width listScaleTo[1] = round((oldHeight / oldWidth) * width) - if method is 3: + if method == 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", "epub_spread"] def __init__(self): pass """ The configuration of the exporter. @param config: A dictionary containing all the config. @param projectUrl: the main location of the project folder. """ def set_config(self, config, projectURL): self.configDictionary = config self.projectURL = projectURL self.pagesLocationList = {} self.acbfLocation = str() self.acbfPageData = [] self.cometLocation = str() self.comicRackInfo = str() """ Export everything according to config and get yourself a coffee. This won't work if the config hasn't been set. """ def export(self): export_success = False path = Path(self.projectURL) if path.exists(): # Make a meta-data folder so we keep the export folder nice and clean. exportPath = path / self.configDictionary["exportLocation"] if Path(exportPath / "metadata").exists() is False: Path(exportPath / "metadata").mkdir() # Get to which formats to export, and set the sizeslist. lengthProcess = len(self.configDictionary["pages"]) sizesList = {} if "CBZ" in self.configDictionary.keys(): if self.configDictionary["CBZactive"]: lengthProcess += 5 sizesList["CBZ"] = self.configDictionary["CBZ"] if "EPUB" in self.configDictionary.keys(): if self.configDictionary["EPUBactive"]: lengthProcess += 1 sizesList["EPUB"] = self.configDictionary["EPUB"] if "TIFF" in self.configDictionary.keys(): if self.configDictionary["TIFFactive"]: sizesList["TIFF"] = self.configDictionary["TIFF"] # Export the pngs according to the sizeslist. # Create a progress dialog. self.progress = QProgressDialog(i18n("Preparing export."), str(), 0, lengthProcess) self.progress.setWindowTitle(i18n("Exporting Comic...")) self.progress.setCancelButton(None) self.timer = QElapsedTimer() self.timer.start() self.progress.show() qApp.processEvents() export_success = self.save_out_pngs(sizesList) # Export acbf metadata. if export_success: if "CBZ" in sizesList.keys(): title = self.configDictionary["projectName"] if "title" in self.configDictionary.keys(): title = str(self.configDictionary["title"]).replace(" ", "_") self.acbfLocation = str(exportPath / "metadata" / str(title + ".acbf")) locationStandAlone = str(exportPath / str(title + ".acbf")) self.progress.setLabelText(i18n("Saving out ACBF and\nACBF standalone")) self.progress.setValue(self.progress.value()+2) export_success = exporters.ACBF.write_xml(self.configDictionary, self.acbfPageData, self.pagesLocationList["CBZ"], self.acbfLocation, locationStandAlone, self.projectURL) print("CPMT: Exported to ACBF", export_success) # Export and package CBZ and Epub. if export_success: if "CBZ" in sizesList.keys(): export_success = self.export_to_cbz(exportPath) print("CPMT: Exported to CBZ", export_success) if "EPUB" in sizesList.keys(): self.progress.setLabelText(i18n("Saving out EPUB")) self.progress.setValue(self.progress.value()+1) export_success = exporters.EPUB.export(self.configDictionary, self.projectURL, self.pagesLocationList["EPUB"], self.acbfPageData) print("CPMT: Exported to EPUB", export_success) else: QMessageBox.warning(None, i18n("Export not Possible"), i18n("Nothing to export, URL not set."), QMessageBox.Ok) print("CPMT: Nothing to export, url not set.") return export_success """ This calls up all the functions necessary for making a cbz. """ def export_to_cbz(self, exportPath): title = self.configDictionary["projectName"] if "title" in self.configDictionary.keys(): title = str(self.configDictionary["title"]).replace(" ", "_") self.progress.setLabelText(i18n("Saving out CoMet\nmetadata file")) self.progress.setValue(self.progress.value()+1) self.cometLocation = str(exportPath / "metadata" / str(title + " CoMet.xml")) export_success = exporters.CoMet.write_xml(self.configDictionary, self.pagesLocationList["CBZ"], self.cometLocation) self.comicRackInfo = str(exportPath / "metadata" / "ComicInfo.xml") self.progress.setLabelText(i18n("Saving out Comicrack\nmetadata file")) self.progress.setValue(self.progress.value()+1) export_success = exporters.comic_rack_xml.write_xml(self.configDictionary, self.pagesLocationList["CBZ"], self.comicRackInfo) self.package_cbz(exportPath) return export_success def save_out_pngs(self, sizesList): # A small fix to ensure crop to guides is set. if "cropToGuides" not in self.configDictionary.keys(): self.configDictionary["cropToGuides"] = False # Check if we have pages at all... if "pages" in self.configDictionary.keys(): # Check if there's export methods, and if so make sure the appropriate dictionaries are initialised. if len(sizesList.keys()) < 1: QMessageBox.warning(None, i18n("Export not Possible"), i18n("Export failed because there's no export settings configured."), QMessageBox.Ok) print("CPMT: Export failed because there's no export methods set.") return False else: for key in sizesList.keys(): self.pagesLocationList[key] = [] # Get the appropriate paths. path = Path(self.projectURL) exportPath = path / self.configDictionary["exportLocation"] pagesList = self.configDictionary["pages"] fileName = str(exportPath) """ Mini function to handle the setup of this string. """ def timeString(timePassed, timeEstimated): return str(i18n("Time passed: {passedString}\n Estimated: {estimated}")).format(passedString=timePassed, estimated=timeEstimated) for p in range(0, len(pagesList)): pagesDone = str(i18n("{pages} of {pagesTotal} done.")).format(pages=p, pagesTotal=len(pagesList)) # Update the label in the progress dialog. self.progress.setValue(p) timePassed = self.timer.elapsed() if p > 0: timeEstimated = (len(pagesList) - p) * (timePassed / p) estimatedString = self.parseTime(timeEstimated) else: estimatedString = str(u"\u221E") passedString = self.parseTime(timePassed) self.progress.setLabelText("\n".join([pagesDone, timeString(passedString, estimatedString), i18n("Opening next page")])) qApp.processEvents() # Get the appropriate url and open the page. url = str(Path(self.projectURL) / pagesList[p]) page = Application.openDocument(url) page.waitForDone() # Update the progress bar a little self.progress.setLabelText("\n".join([pagesDone, timeString(self.parseTime(self.timer.elapsed()), estimatedString), i18n("Cleaning up page")])) # remove layers and flatten. labelList = self.configDictionary["labelsToRemove"] panelsAndText = [] # These three lines are what is causing the page not to close. root = page.rootNode() self.getPanelsAndText(root, panelsAndText) self.removeLayers(labelList, root) page.refreshProjection() # We'll need the offset and scale for aligning the panels and text correctly. We're getting this from the CBZ pageData = {} pageData["vector"] = panelsAndText tree = ET.fromstring(page.documentInfo()) pageData["title"] = page.name() calligra = "{http://www.calligra.org/DTD/document-info}" about = tree.find(calligra + "about") keywords = about.find(calligra + "keyword") keys = str(keywords.text).split(",") pKeys = [] for key in keys: if key in self.pageKeys: pKeys.append(key) pageData["keys"] = pKeys page.flatten() page.waitForDone() batchsave = Application.batchmode() Application.setBatchmode(True) # Start making the format specific copy. for key in sizesList.keys(): # Update the progress bar a little self.progress.setLabelText("\n".join([pagesDone, timeString(self.parseTime(self.timer.elapsed()), estimatedString), str(i18n("Exporting for {key}")).format(key=key)])) w = sizesList[key] # copy over data projection = page.clone() projection.setBatchmode(True) # Crop. Cropping per guide only happens if said guides have been found. if w["Crop"] is True: listHGuides = [] listHGuides = page.horizontalGuides() listHGuides.sort() for i in range(len(listHGuides) - 1, 0, -1): if listHGuides[i] < 0 or listHGuides[i] > page.height(): listHGuides.pop(i) listVGuides = page.verticalGuides() listVGuides.sort() for i in range(len(listVGuides) - 1, 0, -1): if listVGuides[i] < 0 or listVGuides[i] > page.width(): listVGuides.pop(i) if self.configDictionary["cropToGuides"] and len(listVGuides) > 1: cropx = listVGuides[0] cropw = listVGuides[-1] - cropx else: cropx = self.configDictionary["cropLeft"] cropw = page.width() - self.configDictionary["cropRight"] - cropx if self.configDictionary["cropToGuides"] and len(listHGuides) > 1: cropy = listHGuides[0] croph = listHGuides[-1] - cropy else: cropy = self.configDictionary["cropTop"] croph = page.height() - self.configDictionary["cropBottom"] - cropy projection.crop(cropx, cropy, cropw, croph) projection.waitForDone() qApp.processEvents() # resize appropriately else: cropx = 0 cropy = 0 res = page.resolution() listScales = [projection.width(), projection.height(), res, res] projectionOldSize = [projection.width(), projection.height()] sizesCalc = sizesCalculator() listScales = sizesCalc.get_scale_from_resize_config(config=w, listSizes=listScales) projection.scaleImage(listScales[0], listScales[1], listScales[2], listScales[3], "bicubic") projection.waitForDone() qApp.processEvents() # png, gif and other webformats should probably be in 8bit srgb at maximum. if key != "TIFF": if (projection.colorModel() != "RGBA" and projection.colorModel() != "GRAYA") or projection.colorDepth() != "U8": projection.setColorSpace("RGBA", "U8", "sRGB built-in") else: # Tiff on the other hand can handle all the colormodels, but can only handle integer bit depths. # Tiff is intended for print output, and 16 bit integer will be sufficient. if projection.colorDepth() != "U8" or projection.colorDepth() != "U16": projection.setColorSpace(page.colorModel(), "U16", page.colorProfile()) # save # Make sure the folder name for this export exists. It'll allow us to keep the # export folders nice and clean. folderName = str(key + "-" + w["FileType"]) if Path(exportPath / folderName).exists() is False: Path.mkdir(exportPath / folderName) # Get a nice and descriptive fle name. fn = str(Path(exportPath / folderName) / str("page_" + format(p, "03d") + "_" + str(listScales[0]) + "x" + str(listScales[1]) + "." + w["FileType"])) # Finally save and add the page to a list of pages. This will make it easy for the packaging function to # find the pages and store them. projection.exportImage(fn, InfoObject()) projection.waitForDone() qApp.processEvents() if key == "CBZ" or key == "EPUB": transform = {} transform["offsetX"] = cropx transform["offsetY"] = cropy transform["resDiff"] = page.resolution() / 72 transform["scaleWidth"] = projection.width() / projectionOldSize[0] transform["scaleHeight"] = projection.height() / projectionOldSize[1] pageData["transform"] = transform self.pagesLocationList[key].append(fn) projection.close() self.acbfPageData.append(pageData) page.close() self.progress.setValue(len(pagesList)) Application.setBatchmode(batchsave) # TODO: Check what or whether memory leaks are still caused and otherwise remove the entry below. print("CPMT: Export has finished. If there are memory leaks, they are caused by file layers.") return True print("CPMT: Export not happening because there aren't any pages.") QMessageBox.warning(None, i18n("Export not Possible"), i18n("Export not happening because there are no pages."), QMessageBox.Ok) return False """ Function to get the panel and text data. """ def getPanelsAndText(self, node, list): textLayersToSearch = ["text"] panelLayersToSearch = ["panels"] if "textLayerNames" in self.configDictionary.keys(): textLayersToSearch = self.configDictionary["textLayerNames"] if "panelLayerNames" in self.configDictionary.keys(): panelLayersToSearch = self.configDictionary["panelLayerNames"] if node.type() == "vectorlayer": for name in panelLayersToSearch: if str(name).lower() in str(node.name()).lower(): for shape in node.shapes(): if (shape.type() == "groupshape"): self.getPanelsAndTextVector(shape, list) else: self.handleShapeDescription(shape, list) for name in textLayersToSearch: if str(name).lower() in str(node.name()).lower(): for shape in node.shapes(): if (shape.type() == "groupshape"): self.getPanelsAndTextVector(shape, list, True) else: self.handleShapeDescription(shape, list, True) else: if node.childNodes(): for child in node.childNodes(): self.getPanelsAndText(node=child, list=list) def parseTime(self, time = 0): timeList = [] timeList.append(str(int(time / 60000))) timeList.append(format(int((time%60000) / 1000), "02d")) timeList.append(format(int(time % 1000), "03d")) return ":".join(timeList) """ Function to get the panel and text data from a group shape """ def getPanelsAndTextVector(self, group, list, textOnly=False): for shape in group.shapes(): if (shape.type() == "groupshape"): self.getPanelsAndTextVector(shape, list, textOnly) else: self.handleShapeDescription(shape, list, textOnly) """ Function to get text and panels in a format that acbf will accept """ def handleShapeDescription(self, shape, list, textOnly=False): if (shape.type() != "KoSvgTextShapeID" and textOnly is True): return shapeDesc = {} shapeDesc["name"] = shape.name() rect = shape.boundingBox() listOfPoints = [rect.topLeft(), rect.topRight(), rect.bottomRight(), rect.bottomLeft()] shapeDoc = minidom.parseString(shape.toSvg()) docElem = shapeDoc.documentElement svgRegExp = re.compile('[MLCSQHVATmlzcqshva]\d+\.?\d* \d+\.?\d*') transform = docElem.getAttribute("transform") coord = [] adjust = QTransform() # TODO: If we get global transform api, use that instead of parsing manually. if "translate" in transform: transform = transform.replace('translate(', '') for c in transform[:-1].split(" "): if "," in c: c = c.replace(",", "") 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(" "): if "," in c: c = c.replace(",", "") coord.append(float(c)) adjust = QTransform(coord[0], coord[1], coord[2], coord[3], coord[4], coord[5]) path = QPainterPath() if docElem.localName == "path": dVal = docElem.getAttribute("d") listOfSvgStrings = [" "] listOfSvgStrings = svgRegExp.findall(dVal) if listOfSvgStrings: listOfPoints = [] for l in listOfSvgStrings: line = l[1:] coordinates = line.split(" ") if len(coordinates) < 2: coordinates.append(coordinates[0]) x = float(coordinates[-2]) y = float(coordinates[-1]) offset = QPointF() if l.islower(): offset = listOfPoints[0] if l.lower().startswith("m"): path.moveTo(QPointF(x, y) + offset) elif l.lower().startswith("h"): y = listOfPoints[-1].y() path.lineTo(QPointF(x, y) + offset) elif l.lower().startswith("v"): x = listOfPoints[-1].x() path.lineTo(QPointF(x, y) + offset) elif l.lower().startswith("c"): path.cubicTo(coordinates[0], coordinates[1], coordinates[2], coordinates[3], x, y) else: path.lineTo(QPointF(x, y) + offset) path.setFillRule(Qt.WindingFill) for polygon in path.simplified().toSubpathPolygons(adjust): for point in polygon: listOfPoints.append(point) elif docElem.localName == "rect": listOfPoints = [] if (docElem.hasAttribute("x")): x = float(docElem.getAttribute("x")) else: x = 0 if (docElem.hasAttribute("y")): y = float(docElem.getAttribute("y")) else: y = 0 w = float(docElem.getAttribute("width")) h = float(docElem.getAttribute("height")) path.addRect(QRectF(x, y, w, h)) for point in path.toFillPolygon(adjust): listOfPoints.append(point) elif docElem.localName == "ellipse": listOfPoints = [] if (docElem.hasAttribute("cx")): x = float(docElem.getAttribute("cx")) else: x = 0 if (docElem.hasAttribute("cy")): y = float(docElem.getAttribute("cy")) else: y = 0 ry = float(docElem.getAttribute("ry")) rx = float(docElem.getAttribute("rx")) path.addEllipse(QPointF(x, y), rx, ry) for point in path.toFillPolygon(adjust): listOfPoints.append(point) elif docElem.localName == "text": # NOTE: This only works for horizontal preformated text. Vertical text needs a different # ordering of the rects, and wraparound should try to take the shape it is wrapped in. family = "sans-serif" if docElem.hasAttribute("font-family"): family = docElem.getAttribute("font-family") size = "11" if docElem.hasAttribute("font-size"): size = docElem.getAttribute("font-size") multilineText = True for el in docElem.childNodes: if el.nodeType == minidom.Node.TEXT_NODE: multilineText = False if multilineText: listOfPoints = [] listOfRects = [] # First we collect all the possible line-rects. for el in docElem.childNodes: if docElem.hasAttribute("font-family"): family = docElem.getAttribute("font-family") if docElem.hasAttribute("font-size"): size = docElem.getAttribute("font-size") fontsize = int(size) font = QFont(family, fontsize) string = el.toxml() string = re.sub("\<.*?\>", " ", string) string = string.replace(" ", " ") width = min(QFontMetrics(font).width(string.strip()), rect.width()) height = QFontMetrics(font).height() anchor = "start" if docElem.hasAttribute("text-anchor"): anchor = docElem.getAttribute("text-anchor") top = rect.top() if len(listOfRects)>0: top = listOfRects[-1].bottom() if anchor == "start": spanRect = QRectF(rect.left(), top, width, height) listOfRects.append(spanRect) elif anchor == "end": spanRect = QRectF(rect.right()-width, top, width, height) listOfRects.append(spanRect) else: # Middle spanRect = QRectF(rect.center().x()-(width*0.5), top, width, height) listOfRects.append(spanRect) # Now we have all the rects, we can check each and draw a # polygon around them. heightAdjust = (rect.height()-(listOfRects[-1].bottom()-rect.top()))/len(listOfRects) for i in range(len(listOfRects)): span = listOfRects[i] addtionalHeight = i*heightAdjust if i == 0: listOfPoints.append(span.topLeft()) listOfPoints.append(span.topRight()) else: if listOfRects[i-1].width()< span.width(): listOfPoints.append(QPointF(span.right(), span.top()+addtionalHeight)) listOfPoints.insert(0, QPointF(span.left(), span.top()+addtionalHeight)) else: bottom = listOfRects[i-1].bottom()+addtionalHeight-heightAdjust listOfPoints.append(QPointF(listOfRects[i-1].right(), bottom)) listOfPoints.insert(0, QPointF(listOfRects[i-1].left(), bottom)) listOfPoints.append(QPointF(span.right(), rect.bottom())) listOfPoints.insert(0, QPointF(span.left(), rect.bottom())) path = QPainterPath() path.moveTo(listOfPoints[0]) for p in range(1, len(listOfPoints)): path.lineTo(listOfPoints[p]) path.closeSubpath() listOfPoints = [] for point in path.toFillPolygon(adjust): listOfPoints.append(point) shapeDesc["boundingBox"] = listOfPoints if (shape.type() == "KoSvgTextShapeID" and textOnly is True): shapeDesc["text"] = shape.toSvg() list.append(shapeDesc) """ Function to remove layers when they have the given labels. If not, but the node does have children, check those too. """ def removeLayers(self, labels, node): if node.colorLabel() in labels: node.remove() else: if node.childNodes(): for child in node.childNodes(): self.removeLayers(labels, node=child) """ package cbz puts all the meta-data and relevant files into an zip file ending with ".cbz" """ def package_cbz(self, exportPath): # Use the project name if there's no title to avoid sillyness with unnamed zipfiles. title = self.configDictionary["projectName"] if "title" in self.configDictionary.keys(): title = str(self.configDictionary["title"]).replace(" ", "_") # Get the appropriate path. url = str(exportPath / str(title + ".cbz")) # Create a zip file. cbzArchive = zipfile.ZipFile(url, mode="w", compression=zipfile.ZIP_STORED) # Add all the meta data files. cbzArchive.write(self.acbfLocation, Path(self.acbfLocation).name) cbzArchive.write(self.cometLocation, Path(self.cometLocation).name) cbzArchive.write(self.comicRackInfo, Path(self.comicRackInfo).name) comic_book_info_json_dump = str() self.progress.setLabelText(i18n("Saving out Comicbook\ninfo metadata file")) self.progress.setValue(self.progress.value()+1) comic_book_info_json_dump = exporters.comic_book_info.writeJson(self.configDictionary) cbzArchive.comment = comic_book_info_json_dump.encode("utf-8") # Add the pages. if "CBZ" in self.pagesLocationList.keys(): for page in self.pagesLocationList["CBZ"]: if (Path(page).exists()): cbzArchive.write(page, Path(page).name) self.progress.setLabelText(i18n("Packaging CBZ")) self.progress.setValue(self.progress.value()+1) # Close the zip file when done. cbzArchive.close() diff --git a/plugins/python/comics_project_management_tools/comics_metadata_dialog.py b/plugins/python/comics_project_management_tools/comics_metadata_dialog.py index c73b67fd37..4dc07f9fb4 100644 --- a/plugins/python/comics_project_management_tools/comics_metadata_dialog.py +++ b/plugins/python/comics_project_management_tools/comics_metadata_dialog.py @@ -1,784 +1,784 @@ """ 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 metadata editor that helps out setting the proper metadata """ import sys import os # For finding the script location. import csv import re import types from pathlib import Path # For reading all the files in a directory. from PyQt5.QtGui import QStandardItem, QStandardItemModel, QImage, QIcon, QPixmap, QPainter, QPalette, QFontDatabase from PyQt5.QtWidgets import QComboBox, QCompleter, QStyledItemDelegate, QLineEdit, QDialog, QDialogButtonBox, QVBoxLayout, QFormLayout, QTabWidget, QWidget, QPlainTextEdit, QHBoxLayout, QSpinBox, QDateEdit, QPushButton, QLabel, QTableView from PyQt5.QtCore import QDir, QLocale, QStringListModel, Qt, QDate, QSize, QUuid """ multi entry completer cobbled together from the two examples on stackoverflow:3779720 This allows us to let people type in comma-separated lists and get completion for those. """ class multi_entry_completer(QCompleter): punctuation = "," def __init__(self, parent=None): super(QCompleter, self).__init__(parent) def pathFromIndex(self, index): path = QCompleter.pathFromIndex(self, index) string = str(self.widget().text()) split = string.split(self.punctuation) if len(split) > 1: path = "%s, %s" % (",".join(split[:-1]), path) return path def splitPath(self, path): split = str(path.split(self.punctuation)[-1]) if split.startswith(" "): split = split[1:] if split.endswith(" "): split = split[:-1] return [split] """ Language combobox that can take locale codes and get the right language for it and visa-versa. """ class language_combo_box(QComboBox): languageList = [] codesList = [] def __init__(self, parent=None): super(QComboBox, self).__init__(parent) for i in range(1, 357): locale = QLocale(i) if locale and QLocale.languageToString(locale.language()) != "C": codeName = locale.name().split("_")[0] if codeName not in self.codesList: self.codesList.append(codeName) self.codesList.sort() for lang in self.codesList: locale = QLocale(lang) if locale: languageName = QLocale.languageToString(locale.language()) self.languageList.append(languageName.title()) self.setIconSize(QSize(32, 22)) codeIcon = QImage(self.iconSize(), QImage.Format_ARGB32) painter = QPainter(codeIcon) painter.setBrush(Qt.transparent) codeIcon.fill(Qt.transparent) font = QFontDatabase().systemFont(QFontDatabase.FixedFont) painter.setFont(font) painter.setPen(self.palette().color(QPalette.Text)) painter.drawText(codeIcon.rect(), Qt.AlignCenter,lang) painter.end() self.addItem(QIcon(QPixmap.fromImage(codeIcon)), languageName.title()) def codeForCurrentEntry(self): if self.currentText() in self.languageList: return self.codesList[self.languageList.index(self.currentText())] def setEntryToCode(self, code): if (code == "C" and "en" in self.codesList): self.setCurrentIndex(self.codesList.index("en")) if code in self.codesList: self.setCurrentIndex(self.codesList.index(code)) class country_combo_box(QComboBox): countryList = [] codesList = [] def __init__(self, parent=None): super(QComboBox, self).__init__(parent) def set_country_for_locale(self, languageCode): self.clear() self.codesList = [] self.countryList = [] for locale in QLocale.matchingLocales(QLocale(languageCode).language(), QLocale.AnyScript, QLocale.AnyCountry): codeName = locale.name().split("_")[-1] if codeName not in self.codesList: self.codesList.append(codeName) self.codesList.sort() for country in self.codesList: locale = QLocale(languageCode+"-"+country) if locale: countryName = locale.nativeCountryName() self.countryList.append(countryName.title()) self.setIconSize(QSize(32, 22)) codeIcon = QImage(self.iconSize(), QImage.Format_ARGB32) painter = QPainter(codeIcon) painter.setBrush(Qt.transparent) codeIcon.fill(Qt.transparent) font = QFontDatabase().systemFont(QFontDatabase.FixedFont) painter.setFont(font) painter.setPen(self.palette().color(QPalette.Text)) painter.drawText(codeIcon.rect(), Qt.AlignCenter,country) painter.end() self.addItem(QIcon(QPixmap.fromImage(codeIcon)), countryName.title()) def codeForCurrentEntry(self): if self.currentText() in self.countryList: return self.codesList[self.countryList.index(self.currentText())] def setEntryToCode(self, code): if code == "C": self.setCurrentIndex(0) elif code in self.codesList: self.setCurrentIndex(self.codesList.index(code)) """ A combobox that fills up with licenses from a CSV, and also sets tooltips from that csv. """ class license_combo_box(QComboBox): def __init__(self, parent=None): super(QComboBox, self).__init__(parent) mainP = os.path.dirname(__file__) languageP = os.path.join(mainP, "LicenseList.csv") model = QStandardItemModel() if (os.path.exists(languageP)): file = open(languageP, "r", newline="", encoding="utf8") languageReader = csv.reader(file) for row in languageReader: license = QStandardItem(row[0]) license.setToolTip(row[1]) model.appendRow(license) file.close() self.setModel(model) """ Allows us to set completers on the author roles. """ class author_delegate(QStyledItemDelegate): completerStrings = [] completerColumn = 0 languageColumn = 0 def __init__(self, parent=None): super(QStyledItemDelegate, self).__init__(parent) def setCompleterData(self, completerStrings=[str()], completerColumn=0): self.completerStrings = completerStrings self.completerColumn = completerColumn def setLanguageData(self, languageColumn=0): self.languageColumn = languageColumn def createEditor(self, parent, option, index): if index.column() != self.languageColumn: editor = QLineEdit(parent) else: editor = QComboBox(parent) editor.addItem("") for i in range(2, 356): if QLocale(i, QLocale.AnyScript, QLocale.AnyCountry) is not None: languagecode = QLocale(i, QLocale.AnyScript, QLocale.AnyCountry).name().split("_")[0] if languagecode != "C": editor.addItem(languagecode) editor.model().sort(0) if index.column() == self.completerColumn: editor.setCompleter(QCompleter(self.completerStrings)) editor.completer().setCaseSensitivity(False) return editor """ A comic project metadata editing dialog that can take our config diactionary and set all the relevant information. To help our user, the dialog loads up lists of keywords to populate several autocompletion methods. """ class comic_meta_data_editor(QDialog): configGroup = "ComicsProjectManagementTools" # Translatable genre dictionary that has it's translated entries added to the genrelist and from which the untranslated items are taken. acbfGenreList = {"science_fiction": str(i18n("Science Fiction")), "fantasy": str(i18n("Fantasy")), "adventure": str(i18n("Adventure")), "horror": str(i18n("Horror")), "mystery": str(i18n("Mystery")), "crime": str(i18n("Crime")), "military": str(i18n("Military")), "real_life": str(i18n("Real Life")), "superhero": str(i18n("Superhero")), "humor": str(i18n("Humor")), "western": str(i18n("Western")), "manga": str(i18n("Manga")), "politics": str(i18n("Politics")), "caricature": str(i18n("Caricature")), "sports": str(i18n("Sports")), "history": str(i18n("History")), "biography": str(i18n("Biography")), "education": str(i18n("Education")), "computer": str(i18n("Computer")), "religion": str(i18n("Religion")), "romance": str(i18n("Romance")), "children": str(i18n("Children")), "non-fiction": str(i18n("Non Fiction")), "adult": str(i18n("Adult")), "alternative": str(i18n("Alternative")), "artbook": str(i18n("Artbook")), "other": str(i18n("Other"))} acbfAuthorRolesList = {"Writer": str(i18n("Writer")), "Adapter": str(i18n("Adapter")), "Artist": str(i18n("Artist")), "Penciller": str(i18n("Penciller")), "Inker": str(i18n("Inker")), "Colorist": str(i18n("Colorist")), "Letterer": str(i18n("Letterer")), "Cover Artist": str(i18n("Cover Artist")), "Photographer": str(i18n("Photographer")), "Editor": str(i18n("Editor")), "Assistant Editor": str(i18n("Assistant Editor")), "Designer": str(i18n("Designer")), "Translator": str(i18n("Translator")), "Other": str(i18n("Other"))} def __init__(self): super().__init__() # Get the keys for the autocompletion. self.genreKeysList = [] self.characterKeysList = [] self.ratingKeysList = {} self.formatKeysList = [] self.otherKeysList = [] self.authorRoleList = [] for g in self.acbfGenreList.values(): self.genreKeysList.append(g) for r in self.acbfAuthorRolesList.values(): self.authorRoleList.append(r) mainP = Path(os.path.abspath(__file__)).parent self.get_auto_completion_keys(mainP) extraKeyP = Path(QDir.homePath()) / Application.readSetting(self.configGroup, "extraKeysLocation", str()) self.get_auto_completion_keys(extraKeyP) # Setup the dialog. self.setLayout(QVBoxLayout()) mainWidget = QTabWidget() self.layout().addWidget(mainWidget) self.setWindowTitle(i18n("Comic Metadata")) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) self.layout().addWidget(buttons) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) # Title, concept, summary, genre, characters, format, rating, language, series, other keywords metadataPage = QWidget() mformLayout = QFormLayout() metadataPage.setLayout(mformLayout) self.lnTitle = QLineEdit() self.lnTitle.setToolTip(i18n("The proper title of the comic.")) self.teSummary = QPlainTextEdit() self.teSummary.setToolTip(i18n("What will you tell others to entice them to read your comic?")) self.lnGenre = QLineEdit() genreCompletion = multi_entry_completer() genreCompletion.setModel(QStringListModel(self.genreKeysList)) self.lnGenre.setCompleter(genreCompletion) genreCompletion.setCaseSensitivity(False) self.lnGenre.setToolTip(i18n("The genre of the work. Prefilled values are from the ACBF, but you can fill in your own. Separate genres with commas. Try to limit the amount to about two or three.")) self.lnCharacters = QLineEdit() characterCompletion = multi_entry_completer() characterCompletion.setModel(QStringListModel(self.characterKeysList)) characterCompletion.setCaseSensitivity(False) characterCompletion.setFilterMode(Qt.MatchContains) # So that if there is a list of names with last names, people can type in a last name. self.lnCharacters.setCompleter(characterCompletion) self.lnCharacters.setToolTip(i18n("The names of the characters that this comic revolves around. Comma-separated.")) self.lnFormat = QLineEdit() formatCompletion = multi_entry_completer() formatCompletion.setModel(QStringListModel(self.formatKeysList)) formatCompletion.setCaseSensitivity(False) self.lnFormat.setCompleter(formatCompletion) ratingLayout = QHBoxLayout() self.cmbRatingSystem = QComboBox() self.cmbRatingSystem.addItems(self.ratingKeysList.keys()) self.cmbRatingSystem.setEditable(True) self.cmbRating = QComboBox() self.cmbRating.setEditable(True) self.cmbRatingSystem.currentIndexChanged.connect(self.slot_refill_ratings) ratingLayout.addWidget(self.cmbRatingSystem) ratingLayout.addWidget(self.cmbRating) self.lnSeriesName = QLineEdit() self.lnSeriesName.setToolTip(i18n("If this is part of a series, enter the name of the series and the number.")) self.spnSeriesNumber = QSpinBox() self.spnSeriesNumber.setPrefix(i18n("No. ")) self.spnSeriesVol = QSpinBox() self.spnSeriesVol.setPrefix(i18n("Vol. ")) seriesLayout = QHBoxLayout() seriesLayout.addWidget(self.lnSeriesName) seriesLayout.addWidget(self.spnSeriesVol) seriesLayout.addWidget(self.spnSeriesNumber) otherCompletion = multi_entry_completer() otherCompletion.setModel(QStringListModel(self.otherKeysList)) otherCompletion.setCaseSensitivity(False) otherCompletion.setFilterMode(Qt.MatchContains) self.lnOtherKeywords = QLineEdit() self.lnOtherKeywords.setCompleter(otherCompletion) self.lnOtherKeywords.setToolTip(i18n("Other keywords that do not fit in the previously mentioned sets. As always, comma-separated.")) self.cmbLanguage = language_combo_box() self.cmbCountry = country_combo_box() self.cmbLanguage.currentIndexChanged.connect(self.slot_update_countries) self.cmbReadingMode = QComboBox() self.cmbReadingMode.addItem(i18n("Left to Right")) self.cmbReadingMode.addItem(i18n("Right to Left")) self.cmbCoverPage = QComboBox() self.cmbCoverPage.setToolTip(i18n("Which page is the cover page? This will be empty if there are no pages.")) mformLayout.addRow(i18n("Title:"), self.lnTitle) mformLayout.addRow(i18n("Cover page:"), self.cmbCoverPage) mformLayout.addRow(i18n("Summary:"), self.teSummary) mformLayout.addRow(i18n("Language:"), self.cmbLanguage) mformLayout.addRow("", self.cmbCountry) mformLayout.addRow(i18n("Reading direction:"), self.cmbReadingMode) mformLayout.addRow(i18n("Genre:"), self.lnGenre) mformLayout.addRow(i18n("Characters:"), self.lnCharacters) mformLayout.addRow(i18n("Format:"), self.lnFormat) mformLayout.addRow(i18n("Rating:"), ratingLayout) mformLayout.addRow(i18n("Series:"), seriesLayout) mformLayout.addRow(i18n("Other:"), self.lnOtherKeywords) mainWidget.addTab(metadataPage, i18n("Work")) # The page for the authors. authorPage = QWidget() authorPage.setLayout(QVBoxLayout()) explanation = QLabel(i18n("The following is a table of the authors that contributed to this comic. You can set their nickname, proper names (first, middle, last), role (penciller, inker, etc), email and homepage.")) explanation.setWordWrap(True) self.authorModel = QStandardItemModel(0, 8) labels = [i18n("Nick Name"), i18n("Given Name"), i18n("Middle Name"), i18n("Family Name"), i18n("Role"), i18n("Email"), i18n("Homepage"), i18n("Language")] self.authorModel.setHorizontalHeaderLabels(labels) self.authorTable = QTableView() self.authorTable.setModel(self.authorModel) self.authorTable.verticalHeader().setDragEnabled(True) self.authorTable.verticalHeader().setDropIndicatorShown(True) self.authorTable.verticalHeader().setSectionsMovable(True) self.authorTable.verticalHeader().sectionMoved.connect(self.slot_reset_author_row_visual) delegate = author_delegate() delegate.setCompleterData(self.authorRoleList, 4) delegate.setLanguageData(len(labels) - 1) self.authorTable.setItemDelegate(delegate) author_button_layout = QWidget() author_button_layout.setLayout(QHBoxLayout()) btn_add_author = QPushButton(i18n("Add Author")) btn_add_author.clicked.connect(self.slot_add_author) btn_remove_author = QPushButton(i18n("Remove Author")) btn_remove_author.clicked.connect(self.slot_remove_author) author_button_layout.layout().addWidget(btn_add_author) author_button_layout.layout().addWidget(btn_remove_author) authorPage.layout().addWidget(explanation) authorPage.layout().addWidget(self.authorTable) authorPage.layout().addWidget(author_button_layout) mainWidget.addTab(authorPage, i18n("Authors")) # The page with publisher information. publisherPage = QWidget() publisherLayout = QFormLayout() publisherPage.setLayout(publisherLayout) self.publisherName = QLineEdit() self.publisherName.setToolTip(i18n("The name of the company, group or person who is responsible for the final version the reader gets.")) publishDateLayout = QHBoxLayout() self.publishDate = QDateEdit() self.publishDate.setDisplayFormat(QLocale().system().dateFormat()) currentDate = QPushButton(i18n("Set Today")) currentDate.setToolTip(i18n("Sets the publish date to the current date.")) currentDate.clicked.connect(self.slot_set_date) publishDateLayout.addWidget(self.publishDate) publishDateLayout.addWidget(currentDate) self.publishCity = QLineEdit() self.publishCity.setToolTip(i18n("Traditional publishers are always mentioned in source with the city they are located.")) self.isbn = QLineEdit() self.license = license_combo_box() # Maybe ought to make this a QLineEdit... self.license.setEditable(True) self.license.completer().setCompletionMode(QCompleter.PopupCompletion) dataBaseReference = QVBoxLayout() self.ln_database_name = QLineEdit() self.ln_database_name.setToolTip(i18n("If there is an entry in a comics data base, that should be added here. It is unlikely to be a factor for comics from scratch, but useful when doing a conversion.")) self.cmb_entry_type = QComboBox() self.cmb_entry_type.addItems(["IssueID", "SeriesID", "URL"]) self.cmb_entry_type.setEditable(True) self.ln_source = QLineEdit() self.ln_source.setToolTip(i18n("Whether the comic is an adaptation of an existing source, and if so, how to find information about that source. So for example, for an adapted webcomic, the official website url should go here.")) self.label_uuid = QLabel() self.label_uuid.setToolTip(i18n("By default this will be filled with a generated universal unique identifier. The ID by itself is merely so that comic book library management programs can figure out if this particular comic is already in their database and whether it has been rated. Of course, the UUID can be changed into something else by manually changing the JSON, but this is advanced usage.")) self.ln_database_entry = QLineEdit() dbHorizontal = QHBoxLayout() dbHorizontal.addWidget(self.ln_database_name) dbHorizontal.addWidget(self.cmb_entry_type) dataBaseReference.addLayout(dbHorizontal) dataBaseReference.addWidget(self.ln_database_entry) publisherLayout.addRow(i18n("Name:"), self.publisherName) publisherLayout.addRow(i18n("City:"), self.publishCity) publisherLayout.addRow(i18n("Date:"), publishDateLayout) publisherLayout.addRow(i18n("ISBN:"), self.isbn) publisherLayout.addRow(i18n("Source:"), self.ln_source) publisherLayout.addRow(i18n("UUID:"), self.label_uuid) publisherLayout.addRow(i18n("License:"), self.license) publisherLayout.addRow(i18n("Database:"), dataBaseReference) mainWidget.addTab(publisherPage, i18n("Publisher")) """ Ensure that the drag and drop of authors doesn't mess up the labels. """ def slot_reset_author_row_visual(self): headerLabelList = [] for i in range(self.authorTable.verticalHeader().count()): headerLabelList.append(str(i)) for i in range(self.authorTable.verticalHeader().count()): logicalI = self.authorTable.verticalHeader().logicalIndex(i) headerLabelList[logicalI] = str(i + 1) self.authorModel.setVerticalHeaderLabels(headerLabelList) """ Set the publish date to the current date. """ def slot_set_date(self): self.publishDate.setDate(QDate().currentDate()) def slot_update_countries(self): code = self.cmbLanguage.codeForCurrentEntry() self.cmbCountry.set_country_for_locale(code) """ Append keys to autocompletion lists from the directory mainP. """ def get_auto_completion_keys(self, mainP=Path()): genre = Path(mainP / "key_genre") characters = Path(mainP / "key_characters") rating = Path(mainP / "key_rating") format = Path(mainP / "key_format") keywords = Path(mainP / "key_other") authorRole = Path(mainP / "key_author_roles") if genre.exists(): for t in list(genre.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.genreKeysList: self.genreKeysList.append(str(l).strip("\n")) file.close() if characters.exists(): for t in list(characters.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.characterKeysList: self.characterKeysList.append(str(l).strip("\n")) file.close() if format.exists(): for t in list(format.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.formatKeysList: self.formatKeysList.append(str(l).strip("\n")) file.close() if rating.exists(): for t in list(rating.glob('**/*.csv')): file = open(str(t), "r", newline="", encoding="utf-8") ratings = csv.reader(file) title = os.path.basename(str(t)) r = 0 for row in ratings: listItem = [] - if r is 0: + if r == 0: title = row[1] else: listItem = self.ratingKeysList[title] item = [] item.append(row[0]) item.append(row[1]) listItem.append(item) self.ratingKeysList[title] = listItem r += 1 file.close() if keywords.exists(): for t in list(keywords.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.otherKeysList: self.otherKeysList.append(str(l).strip("\n")) file.close() if authorRole.exists(): for t in list(authorRole.glob('**/*.txt')): file = open(str(t), "r", errors="replace") for l in file: if str(l).strip("\n") not in self.authorRoleList: self.authorRoleList.append(str(l).strip("\n")) file.close() """ Refill the ratings box. This is called whenever the rating system changes. """ def slot_refill_ratings(self): if self.cmbRatingSystem.currentText() in self.ratingKeysList.keys(): self.cmbRating.clear() model = QStandardItemModel() for i in self.ratingKeysList[self.cmbRatingSystem.currentText()]: item = QStandardItem() item.setText(i[0]) item.setToolTip(i[1]) model.appendRow(item) self.cmbRating.setModel(model) """ Add an author with default values initialised. """ def slot_add_author(self): listItems = [] listItems.append(QStandardItem(i18n("Anon"))) # Nick name listItems.append(QStandardItem(i18n("John"))) # First name listItems.append(QStandardItem()) # Middle name listItems.append(QStandardItem(i18n("Doe"))) # Last name listItems.append(QStandardItem()) # role listItems.append(QStandardItem()) # email listItems.append(QStandardItem()) # homepage language = QLocale.system().name().split("_")[0] if language == "C": language = "en" listItems.append(QStandardItem(language)) # Language self.authorModel.appendRow(listItems) """ Remove the selected author from the author list. """ def slot_remove_author(self): self.authorModel.removeRow(self.authorTable.currentIndex().row()) """ Load the UI values from the config dictionary given. """ def setConfig(self, config): if "title" in config.keys(): self.lnTitle.setText(config["title"]) self.teSummary.clear() if "pages" in config.keys(): self.cmbCoverPage.clear() for page in config["pages"]: self.cmbCoverPage.addItem(page) if "cover" in config.keys(): if config["cover"] in config["pages"]: self.cmbCoverPage.setCurrentText(config["cover"]) if "summary" in config.keys(): self.teSummary.appendPlainText(config["summary"]) if "genre" in config.keys(): genreList = [] genreListConf = config["genre"] totalMatch = 100 if isinstance(config["genre"], dict): genreListConf = config["genre"].keys() totalMatch = 0 for genre in genreListConf: genreKey = genre if genre in self.acbfGenreList: genreKey = self.acbfGenreList[genre] if isinstance(config["genre"], dict): genreValue = config["genre"][genre] if genreValue > 0: genreKey = str(genreKey + "(" + str(genreValue) + ")") genreList.append(genreKey) self.lnGenre.setText(", ".join(genreList)) if "characters" in config.keys(): self.lnCharacters.setText(", ".join(config["characters"])) if "format" in config.keys(): self.lnFormat.setText(", ".join(config["format"])) if "rating" in config.keys(): self.cmbRating.setCurrentText(config["rating"]) else: self.cmbRating.setCurrentText("") if "ratingSystem" in config.keys(): self.cmbRatingSystem.setCurrentText(config["ratingSystem"]) else: self.cmbRatingSystem.setCurrentText("") if "otherKeywords" in config.keys(): self.lnOtherKeywords.setText(", ".join(config["otherKeywords"])) if "seriesName" in config.keys(): self.lnSeriesName.setText(config["seriesName"]) if "seriesVolume" in config.keys(): self.spnSeriesVol.setValue(config["seriesVolume"]) if "seriesNumber" in config.keys(): self.spnSeriesNumber.setValue(config["seriesNumber"]) if "language" in config.keys(): code = config["language"] if "_" in code: self.cmbLanguage.setEntryToCode(code.split("_")[0]) self.cmbCountry.setEntryToCode(code.split("_")[-1]) elif "-" in code: self.cmbLanguage.setEntryToCode(code.split("-")[0]) self.cmbCountry.setEntryToCode(code.split("-")[-1]) else: self.cmbLanguage.setEntryToCode(code) if "readingDirection" in config.keys(): - if config["readingDirection"] is "leftToRight": + if config["readingDirection"] == "leftToRight": self.cmbReadingMode.setCurrentIndex(int(Qt.LeftToRight)) else: self.cmbReadingMode.setCurrentIndex(int(Qt.RightToLeft)) else: self.cmbReadingMode.setCurrentIndex(QLocale(self.cmbLanguage.codeForCurrentEntry()).textDirection()) if "publisherName" in config.keys(): self.publisherName.setText(config["publisherName"]) if "publisherCity" in config.keys(): self.publishCity.setText(config["publisherCity"]) if "publishingDate" in config.keys(): self.publishDate.setDate(QDate.fromString(config["publishingDate"], Qt.ISODate)) if "isbn-number" in config.keys(): self.isbn.setText(config["isbn-number"]) if "source" in config.keys(): self.ln_source.setText(config["source"]) elif "acbfSource" in config.keys(): self.ln_source.setText(config["acbfSource"]) if "uuid" in config.keys(): self.label_uuid.setText(config["uuid"]) else: uuid = str() if "acbfID" in config.keys(): uuid = config["acbfID"] uuid = uuid.strip("{") uuid = uuid.strip("}") uuidVerify = uuid.split("-") if len(uuidVerify[0])!=8 or len(uuidVerify[1])!=4 or len(uuidVerify[2])!=4 or len(uuidVerify[3])!=4 or len(uuidVerify[4])!=12: uuid = QUuid.createUuid().toString() self.label_uuid.setText(uuid) config["uuid"] = uuid if "license" in config.keys(): self.license.setCurrentText(config["license"]) else: self.license.setCurrentText("") # I would like to keep it ambiguous whether the artist has thought about the license or not. if "authorList" in config.keys(): authorList = config["authorList"] for i in range(len(authorList)): author = authorList[i] if len(author.keys()) > 0: listItems = [] listItems = [] listItems.append(QStandardItem(author.get("nickname", ""))) listItems.append(QStandardItem(author.get("first-name", ""))) listItems.append(QStandardItem(author.get("initials", ""))) listItems.append(QStandardItem(author.get("last-name", ""))) role = author.get("role", "") if role in self.acbfAuthorRolesList.keys(): role = self.acbfAuthorRolesList[role] listItems.append(QStandardItem(role)) listItems.append(QStandardItem(author.get("email", ""))) listItems.append(QStandardItem(author.get("homepage", ""))) listItems.append(QStandardItem(author.get("language", ""))) self.authorModel.appendRow(listItems) else: self.slot_add_author() dbRef = config.get("databaseReference", {}) self.ln_database_name.setText(dbRef.get("name", "")) self.ln_database_entry.setText(dbRef.get("entry", "")) stringCmbEntryType = self.cmb_entry_type.itemText(0) self.cmb_entry_type.setCurrentText(dbRef.get("type", stringCmbEntryType)) """ Store the GUI values into the config dictionary given. @return the config diactionary filled with new values. """ def getConfig(self, config): text = self.lnTitle.text() if len(text) > 0 and text.isspace() is False: config["title"] = text elif "title" in config.keys(): config.pop("title") config["cover"] = self.cmbCoverPage.currentText() listkeys = self.lnGenre.text() if len(listkeys) > 0 and listkeys.isspace() is False: preSplit = self.lnGenre.text().split(",") genreMatcher = re.compile(r'\((\d+)\)') genreList = {} totalValue = 0 for key in preSplit: m = genreMatcher.search(key) if m: genre = str(genreMatcher.sub("", key)).strip() match = int(m.group()[:-1][1:]) else: genre = key.strip() match = 0 if genre in self.acbfGenreList.values(): i = list(self.acbfGenreList.values()).index(genre) genreList[list(self.acbfGenreList.keys())[i]] = match else: genreList[genre] = match totalValue += match # Normalize the values: for key in genreList.keys(): if genreList[key] > 0: genreList[key] = round(genreList[key] / totalValue * 100) config["genre"] = genreList elif "genre" in config.keys(): config.pop("genre") listkeys = self.lnCharacters.text() if len(listkeys) > 0 and listkeys.isspace() is False: config["characters"] = self.lnCharacters.text().split(", ") elif "characters" in config.keys(): config.pop("characters") listkeys = self.lnFormat.text() if len(listkeys) > 0 and listkeys.isspace() is False: config["format"] = self.lnFormat.text().split(", ") elif "format" in config.keys(): config.pop("format") config["ratingSystem"] = self.cmbRatingSystem.currentText() config["rating"] = self.cmbRating.currentText() listkeys = self.lnOtherKeywords.text() if len(listkeys) > 0 and listkeys.isspace() is False: config["otherKeywords"] = self.lnOtherKeywords.text().split(", ") elif "otherKeywords" in config.keys(): config.pop("otherKeywords") text = self.teSummary.toPlainText() if len(text) > 0 and text.isspace() is False: config["summary"] = text elif "summary" in config.keys(): config.pop("summary") if len(self.lnSeriesName.text()) > 0: config["seriesName"] = self.lnSeriesName.text() config["seriesNumber"] = self.spnSeriesNumber.value() if self.spnSeriesVol.value() > 0: config["seriesVolume"] = self.spnSeriesVol.value() config["language"] = str(self.cmbLanguage.codeForCurrentEntry()+"-"+self.cmbCountry.codeForCurrentEntry()) if self.cmbReadingMode.currentIndex() is int(Qt.LeftToRight): config["readingDirection"] = "leftToRight" else: config["readingDirection"] = "rightToLeft" authorList = [] for row in range(self.authorTable.verticalHeader().count()): logicalIndex = self.authorTable.verticalHeader().logicalIndex(row) listEntries = ["nickname", "first-name", "initials", "last-name", "role", "email", "homepage", "language"] author = {} for i in range(len(listEntries)): entry = self.authorModel.data(self.authorModel.index(logicalIndex, i)) if entry is None: entry = " " if entry.isspace() is False and len(entry) > 0: if listEntries[i] == "role": if entry in self.acbfAuthorRolesList.values(): entryI = list(self.acbfAuthorRolesList.values()).index(entry) entry = list(self.acbfAuthorRolesList.keys())[entryI] author[listEntries[i]] = entry elif listEntries[i] in author.keys(): author.pop(listEntries[i]) authorList.append(author) config["authorList"] = authorList config["publisherName"] = self.publisherName.text() config["publisherCity"] = self.publishCity.text() config["publishingDate"] = self.publishDate.date().toString(Qt.ISODate) config["isbn-number"] = self.isbn.text() config["source"] = self.ln_source.text() config["license"] = self.license.currentText() if self.ln_database_name.text().isalnum() and self.ln_database_entry.text().isalnum(): dbRef = {} dbRef["name"] = self.ln_database_name.text() dbRef["entry"] = self.ln_database_entry.text() dbRef["type"] = self.cmb_entry_type.currentText() config["databaseReference"] = dbRef return config 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 faba161de3..50a93cf7eb 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,968 +1,968 @@ """ 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, QFileSystemWatcher, QTimer 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 focused on actual writing and drawing. """ class comics_project_manager_docker(DockWidget): setupDictionary = {} stringName = i18n("Comics Manager") projecturl = None pagesWatcher = None updateurl = str() 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 does not 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() self.pagesWatcher = QFileSystemWatcher() self.pagesWatcher.fileChanged.connect(self.slot_start_delayed_check_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: if os.access(self.path_to_config, os.W_OK) is False: QMessageBox.warning(None, i18n("Config cannot be used"), i18n("Krita doesn't have write access to this folder, so new files cannot be made. Please configure the folder access or move the project to a folder that can be written to."), QMessageBox.Ok) return 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() if len(self.pagesWatcher.files())>0: self.pagesWatcher.removePaths(self.pagesWatcher.files()) 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) relative = os.path.relpath(absurl, self.projecturl) 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(relative, role = CPE.URL) self.pagesWatcher.addPath(absurl) pageItem.setData(dataList[2], role = CPE.KEYWORDS) pageItem.setData(dataList[3], role = CPE.LASTEDIT) pageItem.setData(dataList[4], role = CPE.EDITOR) pageItem.setToolTip(relative) 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) self.pagesWatcher.addPath(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 accessible, 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() self.pagesWatcher.addPath(absoluteUrl) newPage.close() # add item to page. self.pagesModel.appendRow(newPageItem) """ Write to the json configuration 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: + if index.column() == 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): #ensure there is a unique identifier if "uuid" not in self.setupDictionary.keys(): uuid = str() if "acbfID" in self.setupDictionary.keys(): uuid = str(self.setupDictionary["acbfID"]) else: uuid = QUuid.createUuid().toString() self.setupDictionary["uuid"] = uuid 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, i18n("Export success"), i18n("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() self.path_to_config = os.path.join(setup.projectDirectory, "comicConfig.json") 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() """ 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_start_delayed_check_page_update(self, url): self.updateurl = url QTimer.singleShot(200, Qt.PreciseTimer, self.slot_check_for_page_update) def slot_check_for_page_update(self): url = self.updateurl 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(): if os.path.exists(url) is False: # we cannot check from here whether the file in question has been renamed or deleted. self.pagesModel.removeRow(index.row()) return else: # Krita will trigger the filesystemwatcher when doing backupfiles, # so ensure the file is still watched if it exists. self.pagesWatcher.addPath(url) 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(relUrl, 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) self.updateurl = str() """ 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("Resize 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"), str(i18n("POT file has been written to: {file}")).format(file=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 12c5b7268e..204dd75774 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,802 +1,802 @@ """ 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.acbf.info/xml/acbf/1.1") 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"]).replace("_", "-")) 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) 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) 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) 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", str(configDictionary["language"]).replace("_", "-")) textlayer.setAttribute("show", "false") textlayerNative = document.createElement("text-layer") textlayerNative.setAttribute("lang", str(configDictionary["language"]).replace("_", "-")) 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.insertAfter(bookTitleTr, bookTitle) 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) paragraph = document.createElement("p") paragraph.appendChild(document.createTextNode(translation)) annotationTr.appendChild(paragraph) bookInfo.insertAfter(annotationTr, annotation) 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.insertAfter(keywordsTr, keywords) bookInfo.appendChild(language) bookTitle.setAttribute("lang", str(configDictionary["language"]).replace("_", "-")) annotation.setAttribute("lang", str(configDictionary["language"]).replace("_", "-")) keywords.setAttribute("lang", str(configDictionary["language"]).replace("_", "-")) 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": + if configDictionary["readingDirection"] == "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 apparently uses first/middle/last/nick/email/homepage for the document author 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"]).replace("_", "-")) 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(): if len(configDictionary["acbfHistory"])>0: 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 = str(configDictionary["language"]).replace("_", "-") 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()) if textLayerTr.hasChildNodes(): 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") if textLayer.hasChildNodes(): pg.appendChild(textLayer) pg.setAttribute("bgcolor", pageColor.name()) for n in range(0, textLayerList.childNodes().size()): node = textLayerList.childNodes().at(n) pg.appendChild(node) for f in frameList: frame = document.createElement("frame") frame.setAttribute("points", f["points"]) pg.appendChild(frame) body.appendChild(pg) else: for f in frameList: frame = document.createElement("frame") frame.setAttribute("points", f["points"]) coverpage.appendChild(frame) coverpage.appendChild(textLayer) 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 "+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) # Convert 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 diff --git a/plugins/python/comics_project_management_tools/exporters/CPMT_CoMet_XML_Exporter.py b/plugins/python/comics_project_management_tools/exporters/CPMT_CoMet_XML_Exporter.py index 510fac52e1..dba5ddedd4 100644 --- a/plugins/python/comics_project_management_tools/exporters/CPMT_CoMet_XML_Exporter.py +++ b/plugins/python/comics_project_management_tools/exporters/CPMT_CoMet_XML_Exporter.py @@ -1,150 +1,150 @@ """ 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 a CoMet xml file to url """ import os from xml.dom import minidom def write_xml(configDictionary = {}, pagesLocationList = [], location = str()): document = minidom.Document() root = document.createElement("comet") root.setAttribute("xmlns:comet", "http://www.denvog.com/comet/") root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") root.setAttribute("xsi:schemaLocation", "http://www.denvog.com http://www.denvog.com/comet/comet.xsd") document.appendChild(root) title = document.createElement("title") if "title" in configDictionary.keys(): title.appendChild(document.createTextNode(str(configDictionary["title"]))) else: title.appendChild(document.createTextNode(str("Untitled Comic"))) root.appendChild(title) description = document.createElement("description") if "summary" in configDictionary.keys(): description.appendChild(document.createTextNode(str(configDictionary["summary"]))) else: description.appendChild(document.createTextNode(str("There was no summary upon generation of this file."))) root.appendChild(description) if "seriesName" in configDictionary.keys(): series = document.createElement("series") series.appendChild(document.createTextNode(str(configDictionary["seriesName"]))) root.appendChild(series) if "seriesNumber" in configDictionary.keys(): issue = document.createElement("issue") issue.appendChild(document.createTextNode(str(configDictionary["seriesNumber"]))) root.appendChild(issue) if "seriesVolume" in configDictionary.keys(): volume = document.createElement("volume") volume.appendChild(document.createTextNode(str(configDictionary["seriesVolume"]))) root.appendChild(volume) if "publisherName" in configDictionary.keys(): publisher = document.createElement("publisher") publisher.appendChild(document.createTextNode(str(configDictionary["publisherName"]))) root.appendChild(publisher) if "publishingDate" in configDictionary.keys(): date = document.createElement("date") date.appendChild(document.createTextNode(str(configDictionary["publishingDate"]))) root.appendChild(date) if "genre" in configDictionary.keys(): genreListConf = configDictionary["genre"] if isinstance(configDictionary["genre"], dict): genreListConf = configDictionary["genre"].keys() for genreE in genreListConf: genre = document.createElement("genre") genre.appendChild(document.createTextNode(str(genreE))) root.appendChild(genre) if "characters" in configDictionary.keys(): for char in configDictionary["characters"]: character = document.createElement("character") character.appendChild(document.createTextNode(str(char))) root.appendChild(character) if "format" in configDictionary.keys(): format = document.createElement("format") format.appendChild(document.createTextNode(str(",".join(configDictionary["format"])))) root.appendChild(format) if "language" in configDictionary.keys(): language = document.createElement("language") language.appendChild(document.createTextNode(str(configDictionary["language"]))) root.appendChild(language) if "rating" in configDictionary.keys(): rating = document.createElement("rating") rating.appendChild(document.createTextNode(str(configDictionary["rating"]))) root.appendChild(rating) #rights = document.createElement("rights") if "pages" in configDictionary.keys(): pages = document.createElement("pages") pages.appendChild(document.createTextNode(str(len(configDictionary["pages"])))) root.appendChild(pages) if "isbn-number" in configDictionary.keys(): identifier = document.createElement("identifier") identifier.appendChild(document.createTextNode(str(configDictionary["isbn-number"]))) root.appendChild(identifier) if "authorList" in configDictionary.keys(): for authorE in range(len(configDictionary["authorList"])): author = document.createElement("creator") authorDict = configDictionary["authorList"][authorE] if "role" in authorDict.keys(): if str(authorDict["role"]).lower() in ["writer", "penciller", "editor", "assistant editor", "cover artist", "letterer", "inker", "colorist"]: - if str(authorDict["role"]).lower() is "cover artist": + if str(authorDict["role"]).lower() == "cover artist": author = document.createElement("coverDesigner") - elif str(authorDict["role"]).lower() is "assistant editor": + elif str(authorDict["role"]).lower() == "assistant editor": author = document.createElement("editor") else: author = document.createElement(str(authorDict["role"]).lower()) stringName = [] if "last-name" in authorDict.keys(): stringName.append(authorDict["last-name"]) if "first-name" in authorDict.keys(): stringName.append(authorDict["first-name"]) if "nickname" in authorDict.keys(): stringName.append("(" + authorDict["nickname"] + ")") author.appendChild(document.createTextNode(str(",".join(stringName)))) root.appendChild(author) if "pages" in configDictionary.keys(): if "cover" in configDictionary.keys(): pageList = [] pageList = configDictionary["pages"] coverNumber = pageList.index(configDictionary["cover"]) if len(pagesLocationList) >= coverNumber: coverImage = document.createElement("coverImage") coverImage.appendChild(document.createTextNode(str(os.path.basename(pagesLocationList[coverNumber])))) root.appendChild(coverImage) readingDirection = document.createElement("readingDirection") readingDirection.appendChild(document.createTextNode(str("ltr"))) if "readingDirection" in configDictionary.keys(): - if configDictionary["readingDirection"] is "rightToLeft": + if configDictionary["readingDirection"] == "rightToLeft": readingDirection.appendChild(document.createTextNode(str("rtl"))) root.appendChild(readingDirection) f = open(location, 'w', newline="", encoding="utf-8") f.write(document.toprettyxml(indent=" ")) f.close() return True diff --git a/plugins/python/comics_project_management_tools/exporters/CPMT_Comic_Rack_XML_Exporter.py b/plugins/python/comics_project_management_tools/exporters/CPMT_Comic_Rack_XML_Exporter.py index c6574d7f31..ef6b51c272 100644 --- a/plugins/python/comics_project_management_tools/exporters/CPMT_Comic_Rack_XML_Exporter.py +++ b/plugins/python/comics_project_management_tools/exporters/CPMT_Comic_Rack_XML_Exporter.py @@ -1,169 +1,169 @@ """ 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 . """ """ The comicrack information is sorta... incomplete, so no idea if the following is right... I can't check in any case: It is a windows application. Based off: https://github.com/dickloraine/EmbedComicMetadata/blob/master/comicinfoxml.py ComicRack is also a dead application. Missing: Count (issues) AlternateSeries AlternateNumber StoryArc SeriesGroup AlternateCount Notes Imprint Locations ScanInformation AgeRating - Not sure if this should be added or not... Teams Web """ from xml.dom import minidom from PyQt5.QtCore import QDate, Qt def write_xml(configDictionary = {}, pagesLocationList = [], location = str()): document = minidom.Document() root = document.createElement("ComicInfo") root.setAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance") root.setAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema") title = document.createElement("Title") if "title" in configDictionary.keys(): title.appendChild(document.createTextNode(str(configDictionary["title"]))) else: title.appendChild(document.createTextNode(str("Untitled Comic"))) root.appendChild(title) description = document.createElement("Summary") if "summary" in configDictionary.keys(): description.appendChild(document.createTextNode(str(configDictionary["summary"]))) else: description.appendChild(document.createTextNode(str("There was no summary upon generation of this file."))) root.appendChild(description) if "seriesNumber" in configDictionary.keys(): number = document.createElement("Number") number.appendChild(document.createTextNode(str(configDictionary["seriesNumber"]))) root.appendChild(number) if "seriesName" in configDictionary.keys(): seriesname = document.createElement("Series") seriesname.appendChild(document.createTextNode(str(configDictionary["seriesName"]))) root.appendChild(seriesname) if "publishingDate" in configDictionary.keys(): date = QDate.fromString(configDictionary["publishingDate"], Qt.ISODate) publishYear = document.createElement("Year") publishYear.appendChild(document.createTextNode(str(date.year()))) publishMonth = document.createElement("Month") publishMonth.appendChild(document.createTextNode(str(date.month()))) publishDay = document.createElement("Day") publishDay.appendChild(document.createTextNode(str(date.day()))) root.appendChild(publishYear) root.appendChild(publishMonth) root.appendChild(publishDay) if "format" in configDictionary.keys(): for form in configDictionary["format"]: formattag = document.createElement("Format") formattag.appendChild(document.createTextNode(str(form))) root.appendChild(formattag) if "otherKeywords" in configDictionary.keys(): tags = document.createElement("Tags") tags.appendChild(document.createTextNode(str(", ".join(configDictionary["otherKeywords"])))) root.appendChild(tags) if "authorList" in configDictionary.keys(): for authorE in range(len(configDictionary["authorList"])): author = document.createElement("Writer") authorDict = configDictionary["authorList"][authorE] if "role" in authorDict.keys(): if str(authorDict["role"]).lower() in ["writer", "penciller", "editor", "assistant editor", "cover artist", "letterer", "inker", "colorist"]: - if str(authorDict["role"]).lower() is "cover artist": + if str(authorDict["role"]).lower() == "cover artist": author = document.createElement("CoverArtist") - elif str(authorDict["role"]).lower() is "assistant editor": + elif str(authorDict["role"]).lower() == "assistant editor": author = document.createElement("Editor") else: author = document.createElement(str(authorDict["role"]).title()) stringName = [] if "last-name" in authorDict.keys(): stringName.append(authorDict["last-name"]) if "first-name" in authorDict.keys(): stringName.append(authorDict["first-name"]) if "nickname" in authorDict.keys(): stringName.append("(" + authorDict["nickname"] + ")") author.appendChild(document.createTextNode(str(",".join(stringName)))) root.appendChild(author) if "publisherName" in configDictionary.keys(): publisher = document.createElement("Publisher") publisher.appendChild(document.createTextNode(str(configDictionary["publisherName"]))) root.appendChild(publisher) if "genre" in configDictionary.keys(): genreListConf = configDictionary["genre"] if isinstance(configDictionary["genre"], dict): genreListConf = configDictionary["genre"].keys() for genreE in genreListConf: genre = document.createElement("Genre") genre.appendChild(document.createTextNode(str(genreE))) root.appendChild(genre) blackAndWhite = document.createElement("BlackAndWhite") blackAndWhite.appendChild(document.createTextNode(str("No"))) root.appendChild(blackAndWhite) readingDirection = document.createElement("Manga") readingDirection.appendChild(document.createTextNode(str("No"))) if "readingDirection" in configDictionary.keys(): - if configDictionary["readingDirection"] is "rightToLeft": + if configDictionary["readingDirection"] == "rightToLeft": readingDirection.appendChild(document.createTextNode(str("YesAndRightToLeft"))) root.appendChild(readingDirection) if "characters" in configDictionary.keys(): for char in configDictionary["characters"]: character = document.createElement("Character") character.appendChild(document.createTextNode(str(char))) root.appendChild(character) if "pages" in configDictionary.keys(): pagecount = document.createElement("PageCount") pagecount.appendChild(document.createTextNode(str(len(configDictionary["pages"])))) root.appendChild(pagecount) pages = document.createElement("Pages") covernumber = 0 if "pages" in configDictionary.keys() and "cover" in configDictionary.keys(): covernumber = configDictionary["pages"].index(configDictionary["cover"]) for i in range(len(pagesLocationList)): page = document.createElement("Page") page.setAttribute("Image", str(i)) if i is covernumber: page.setAttribute("Type", "FrontCover") pages.appendChild(page) root.appendChild(pages) document.appendChild(root) f = open(location, 'w', newline="", encoding="utf-8") f.write(document.toprettyxml(indent=" ")) f.close() return True diff --git a/plugins/python/comics_project_management_tools/exporters/CPMT_EPUB_exporter.py b/plugins/python/comics_project_management_tools/exporters/CPMT_EPUB_exporter.py index 97eb117ec7..f5fda60cb8 100644 --- a/plugins/python/comics_project_management_tools/exporters/CPMT_EPUB_exporter.py +++ b/plugins/python/comics_project_management_tools/exporters/CPMT_EPUB_exporter.py @@ -1,840 +1,840 @@ """ Copyright (c) 2018 Wolthera van Hövell tot Westerflier This file is part of the Comics Project Management Tools(CPMT). CPMT is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. CPMT is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with the CPMT. If not, see . """ """ Create an epub folder, finally, package to a epubzip. """ import shutil import os from pathlib import Path import zipfile from PyQt5.QtXml import QDomDocument, QDomElement, QDomText, QDomNodeList from PyQt5.QtCore import Qt, QDateTime, QPointF from PyQt5.QtGui import QImage, QPolygonF, QColor def export(configDictionary = {}, projectURL = str(), pagesLocationList = [], pageData = []): path = Path(os.path.join(projectURL, configDictionary["exportLocation"])) exportPath = path / "EPUB-files" metaInf = exportPath / "META-INF" oebps = exportPath / "OEBPS" imagePath = oebps / "Images" # Don't write empty folders. Epubcheck doesn't like that. # stylesPath = oebps / "Styles" textPath = oebps / "Text" if exportPath.exists() is False: exportPath.mkdir() metaInf.mkdir() oebps.mkdir() imagePath.mkdir() # stylesPath.mkdir() textPath.mkdir() # Due the way EPUB verifies, the mimetype needs to be packaged in first. # Due the way zips are constructed, the only way to ensure that is to # Fill the zip as we go along... # Use the project name if there's no title to avoid sillyness with unnamed zipfiles. title = configDictionary["projectName"] if "title" in configDictionary.keys(): title = str(configDictionary["title"]).replace(" ", "_") # Get the appropriate path. url = str(path / str(title + ".epub")) # Create a zip file. epubArchive = zipfile.ZipFile(url, mode="w", compression=zipfile.ZIP_STORED) mimetype = open(str(Path(exportPath / "mimetype")), mode="w") mimetype.write("application/epub+zip") mimetype.close() # Write to zip. epubArchive.write(Path(exportPath / "mimetype"), Path("mimetype")) container = QDomDocument() cRoot = container.createElement("container") cRoot.setAttribute("version", "1.0") cRoot.setAttribute("xmlns", "urn:oasis:names:tc:opendocument:xmlns:container") container.appendChild(cRoot) rootFiles = container.createElement("rootfiles") rootfile = container.createElement("rootfile") rootfile.setAttribute("full-path", "OEBPS/content.opf") rootfile.setAttribute("media-type", "application/oebps-package+xml") rootFiles.appendChild(rootfile) cRoot.appendChild(rootFiles) containerFileName = str(Path(metaInf / "container.xml")) containerFile = open(containerFileName, 'w', newline="", encoding="utf-8") containerFile.write(container.toString(indent=2)) containerFile.close() # Write to zip. epubArchive.write(containerFileName, os.path.relpath(containerFileName, str(exportPath))) # copyimages to images pagesList = [] if len(pagesLocationList)>0: if "cover" in configDictionary.keys(): coverNumber = configDictionary["pages"].index(configDictionary["cover"]) else: coverNumber = 0 for p in pagesLocationList: if os.path.exists(p): shutil.copy2(p, str(imagePath)) filename = str(Path(imagePath / os.path.basename(p))) pagesList.append(filename) epubArchive.write(filename, os.path.relpath(filename, str(exportPath))) if len(pagesLocationList) >= coverNumber: coverpageurl = pagesList[coverNumber] else: print("CPMT: Couldn't find the location for the epub images.") return False # for each image, make an xhtml file htmlFiles = [] listOfNavItems = {} listofSpreads = [] regions = [] for i in range(len(pagesList)): pageName = "Page" + str(i) + ".xhtml" doc = QDomDocument() html = doc.createElement("html") doc.appendChild(html) html.setAttribute("xmlns", "http://www.w3.org/1999/xhtml") html.setAttribute("xmlns:epub", "http://www.idpf.org/2007/ops") # The viewport is a prerequisite to get pre-paginated # layouts working. We'll make the layout the same size # as the image. head = doc.createElement("head") viewport = doc.createElement("meta") viewport.setAttribute("name", "viewport") img = QImage() img.load(pagesLocationList[i]) w = img.width() h = img.height() widthHeight = "width="+str(w)+", height="+str(h) viewport.setAttribute("content", widthHeight) head.appendChild(viewport) html.appendChild(head) # Here, we process the region navigation data to percentages # because we have access here to the width and height of the viewport. data = pageData[i] transform = data["transform"] for v in data["vector"]: pointsList = [] dominantColor = QColor(Qt.white) listOfColors = [] for point in v["boundingBox"]: offset = QPointF(transform["offsetX"], transform["offsetY"]) pixelPoint = QPointF(point.x() * transform["resDiff"], point.y() * transform["resDiff"]) newPoint = pixelPoint - offset x = max(0, min(w, int(newPoint.x() * transform["scaleWidth"]))) y = max(0, min(h, int(newPoint.y() * transform["scaleHeight"]))) listOfColors.append(img.pixelColor(QPointF(x, y).toPoint())) pointsList.append(QPointF((x/w)*100, (y/h)*100)) regionType = "panel" if "text" in v.keys(): regionType = "text" if len(listOfColors)>0: dominantColor = listOfColors[-1] listOfColors = listOfColors[:-1] for color in listOfColors: dominantColor.setRedF(0.5*(dominantColor.redF()+color.redF())) dominantColor.setGreenF(0.5*(dominantColor.greenF()+color.greenF())) dominantColor.setBlueF(0.5*(dominantColor.blueF()+color.blueF())) region = {} bounds = QPolygonF(pointsList).boundingRect() region["points"] = bounds region["type"] = regionType region["page"] = str(Path(textPath / pageName)) region["primaryColor"] = dominantColor.name() regions.append(region) # We can also figureout here whether the page can be seen as a table of contents entry. if "acbf_title" in data["keys"]: listOfNavItems[str(Path(textPath / pageName))] = data["title"] # Or spreads... if "epub_spread" in data["keys"]: listofSpreads.append(str(Path(textPath / pageName))) body = doc.createElement("body") img = doc.createElement("img") img.setAttribute("src", os.path.relpath(pagesList[i], str(textPath))) body.appendChild(img) html.appendChild(body) filename = str(Path(textPath / pageName)) docFile = open(filename, 'w', newline="", encoding="utf-8") docFile.write(doc.toString(indent=2)) docFile.close() if pagesList[i] == coverpageurl: coverpagehtml = os.path.relpath(filename, str(oebps)) htmlFiles.append(filename) # Write to zip. epubArchive.write(filename, os.path.relpath(filename, str(exportPath))) # metadata filename = write_opf_file(oebps, configDictionary, htmlFiles, pagesList, coverpageurl, coverpagehtml, listofSpreads) epubArchive.write(filename, os.path.relpath(filename, str(exportPath))) filename = write_region_nav_file(oebps, configDictionary, htmlFiles, regions) epubArchive.write(filename, os.path.relpath(filename, str(exportPath))) # toc filename = write_nav_file(oebps, configDictionary, htmlFiles, listOfNavItems) epubArchive.write(filename, os.path.relpath(filename, str(exportPath))) filename = write_ncx_file(oebps, configDictionary, htmlFiles, listOfNavItems) epubArchive.write(filename, os.path.relpath(filename, str(exportPath))) epubArchive.close() return True """ Write OPF metadata file """ def write_opf_file(path, configDictionary, htmlFiles, pagesList, coverpageurl, coverpagehtml, listofSpreads): # marc relators # This has several entries removed to reduce it to the most relevant entries. marcRelators = {"abr":i18n("Abridger"), "acp":i18n("Art copyist"), "act":i18n("Actor"), "adi":i18n("Art director"), "adp":i18n("Adapter"), "ann":i18n("Annotator"), "ant":i18n("Bibliographic antecedent"), "arc":i18n("Architect"), "ard":i18n("Artistic director"), "art":i18n("Artist"), "asn":i18n("Associated name"), "ato":i18n("Autographer"), "att":i18n("Attributed name"), "aud":i18n("Author of dialog"), "aut":i18n("Author"), "bdd":i18n("Binding designer"), "bjd":i18n("Bookjacket designer"), "bkd":i18n("Book designer"), "bkp":i18n("Book producer"), "blw":i18n("Blurb writer"), "bnd":i18n("Binder"), "bpd":i18n("Bookplate designer"), "bsl":i18n("Bookseller"), "cll":i18n("Calligrapher"), "clr":i18n("Colorist"), "cns":i18n("Censor"), "cov":i18n("Cover designer"), "cph":i18n("Copyright holder"), "cre":i18n("Creator"), "ctb":i18n("Contributor"), "cur":i18n("Curator"), "cwt":i18n("Commentator for written text"), "drm":i18n("Draftsman"), "dsr":i18n("Designer"), "dub":i18n("Dubious author"), "edt":i18n("Editor"), "etr":i18n("Etcher"), "exp":i18n("Expert"), "fnd":i18n("Funder"), "ill":i18n("Illustrator"), "ilu":i18n("Illuminator"), "ins":i18n("Inscriber"), "lse":i18n("Licensee"), "lso":i18n("Licensor"), "ltg":i18n("Lithographer"), "mdc":i18n("Metadata contact"), "oth":i18n("Other"), "own":i18n("Owner"), "pat":i18n("Patron"), "pbd":i18n("Publishing director"), "pbl":i18n("Publisher"), "prt":i18n("Printer"), "sce":i18n("Scenarist"), "scr":i18n("Scribe"), "spn":i18n("Sponsor"), "stl":i18n("Storyteller"), "trc":i18n("Transcriber"), "trl":i18n("Translator"), "tyd":i18n("Type designer"), "tyg":i18n("Typographer"), "wac":i18n("Writer of added commentary"), "wal":i18n("Writer of added lyrics"), "wam":i18n("Writer of accompanying material"), "wat":i18n("Writer of added text"), "win":i18n("Writer of introduction"), "wpr":i18n("Writer of preface"), "wst":i18n("Writer of supplementary textual content")} # opf file opfFile = QDomDocument() opfRoot = opfFile.createElement("package") opfRoot.setAttribute("version", "3.0") opfRoot.setAttribute("unique-identifier", "BookId") opfRoot.setAttribute("xmlns", "http://www.idpf.org/2007/opf") opfRoot.setAttribute("prefix", "rendition: http://www.idpf.org/vocab/rendition/#") opfFile.appendChild(opfRoot) opfMeta = opfFile.createElement("metadata") opfMeta.setAttribute("xmlns:dc", "http://purl.org/dc/elements/1.1/") opfMeta.setAttribute("xmlns:dcterms", "http://purl.org/dc/terms/") # EPUB metadata requires a title, language and uuid langString = "en-US" if "language" in configDictionary.keys(): langString = str(configDictionary["language"]).replace("_", "-") bookLang = opfFile.createElement("dc:language") bookLang.appendChild(opfFile.createTextNode(langString)) opfMeta.appendChild(bookLang) bookTitle = opfFile.createElement("dc:title") if "title" in configDictionary.keys(): bookTitle.appendChild(opfFile.createTextNode(str(configDictionary["title"]))) else: bookTitle.appendChild(opfFile.createTextNode("Comic with no Name")) opfMeta.appendChild(bookTitle) # Generate series title and the like here too. if "seriesName" in configDictionary.keys(): bookTitle.setAttribute("id", "main") refine = opfFile.createElement("meta") refine.setAttribute("refines", "#main") refine.setAttribute("property", "title-type") refine.appendChild(opfFile.createTextNode("main")) opfMeta.appendChild(refine) refine2 = opfFile.createElement("meta") refine2.setAttribute("refines", "#main") refine2.setAttribute("property", "display-seq") refine2.appendChild(opfFile.createTextNode("1")) opfMeta.appendChild(refine2) seriesTitle = opfFile.createElement("dc:title") seriesTitle.appendChild(opfFile.createTextNode(str(configDictionary["seriesName"]))) seriesTitle.setAttribute("id", "series") opfMeta.appendChild(seriesTitle) refineS = opfFile.createElement("meta") refineS.setAttribute("refines", "#series") refineS.setAttribute("property", "title-type") refineS.appendChild(opfFile.createTextNode("collection")) opfMeta.appendChild(refineS) refineS2 = opfFile.createElement("meta") refineS2.setAttribute("refines", "#series") refineS2.setAttribute("property", "display-seq") refineS2.appendChild(opfFile.createTextNode("2")) opfMeta.appendChild(refineS2) if "seriesNumber" in configDictionary.keys(): refineS3 = opfFile.createElement("meta") refineS3.setAttribute("refines", "#series") refineS3.setAttribute("property", "group-position") refineS3.appendChild(opfFile.createTextNode(str(configDictionary["seriesNumber"]))) opfMeta.appendChild(refineS3) uuid = str(configDictionary["uuid"]) uuid = uuid.strip("{") uuid = uuid.strip("}") # Append the id, and assign it as the bookID. uniqueID = opfFile.createElement("dc:identifier") uniqueID.appendChild(opfFile.createTextNode("urn:uuid:"+uuid)) uniqueID.setAttribute("id", "BookId") opfMeta.appendChild(uniqueID) if "authorList" in configDictionary.keys(): authorEntry = 0 for authorE in range(len(configDictionary["authorList"])): authorDict = configDictionary["authorList"][authorE] authorType = "dc:creator" if "role" in authorDict.keys(): # This determines if someone was just a contributor, but might need a more thorough version. if str(authorDict["role"]).lower() in ["editor", "assistant editor", "proofreader", "beta", "patron", "funder"]: authorType = "dc:contributor" author = opfFile.createElement(authorType) authorName = [] if "last-name" in authorDict.keys(): authorName.append(authorDict["last-name"]) if "first-name" in authorDict.keys(): authorName.append(authorDict["first-name"]) if "initials" in authorDict.keys(): authorName.append(authorDict["initials"]) if "nickname" in authorDict.keys(): authorName.append("(" + authorDict["nickname"] + ")") author.appendChild(opfFile.createTextNode(", ".join(authorName))) author.setAttribute("id", "cre" + str(authorE)) opfMeta.appendChild(author) if "role" in authorDict.keys(): role = opfFile.createElement("meta") role.setAttribute("refines", "#cre" + str(authorE)) role.setAttribute("scheme", "marc:relators") role.setAttribute("property", "role") roleString = str(authorDict["role"]) if roleString in marcRelators.values() or roleString in marcRelators.keys(): i = list(marcRelators.values()).index(roleString) roleString = list(marcRelators.keys())[i] else: roleString = "oth" role.appendChild(opfFile.createTextNode(roleString)) opfMeta.appendChild(role) refine = opfFile.createElement("meta") refine.setAttribute("refines", "#cre"+str(authorE)) refine.setAttribute("property", "display-seq") refine.appendChild(opfFile.createTextNode(str(authorE+1))) opfMeta.appendChild(refine) if "publishingDate" in configDictionary.keys(): date = opfFile.createElement("dc:date") date.appendChild(opfFile.createTextNode(configDictionary["publishingDate"])) opfMeta.appendChild(date) #Creation date modified = opfFile.createElement("meta") modified.setAttribute("property", "dcterms:modified") modified.appendChild(opfFile.createTextNode(QDateTime.currentDateTimeUtc().toString(Qt.ISODate))) opfMeta.appendChild(modified) if "source" in configDictionary.keys(): if len(configDictionary["source"])>0: source = opfFile.createElement("dc:source") source.appendChild(opfFile.createTextNode(configDictionary["source"])) opfMeta.appendChild(source) description = opfFile.createElement("dc:description") if "summary" in configDictionary.keys(): description.appendChild(opfFile.createTextNode(configDictionary["summary"])) else: description.appendChild(opfFile.createTextNode("There was no summary upon generation of this file.")) opfMeta.appendChild(description) # Type can be dictionary or index, or one of those edupub thingies. Not necessary for comics. # typeE = opfFile.createElement("dc:type") # opfMeta.appendChild(typeE) if "publisherName" in configDictionary.keys(): publisher = opfFile.createElement("dc:publisher") publisher.appendChild(opfFile.createTextNode(configDictionary["publisherName"])) opfMeta.appendChild(publisher) if "isbn-number" in configDictionary.keys(): isbnnumber = configDictionary["isbn-number"] if len(isbnnumber)>0: publishISBN = opfFile.createElement("dc:identifier") publishISBN.appendChild(opfFile.createTextNode(str("urn:isbn:") + isbnnumber)) opfMeta.appendChild(publishISBN) if "license" in configDictionary.keys(): if len(configDictionary["license"])>0: rights = opfFile.createElement("dc:rights") rights.appendChild(opfFile.createTextNode(configDictionary["license"])) opfMeta.appendChild(rights) """ Not handled Relation - This is for whether the work has a relationship with another work. It could be fanart, but also adaptation, an academic work, etc. Coverage - This is for the time/place that the work covers. Typically to determine whether an academic work deals with a certain time period or place. For comics you could use this to mark historical comics, but other than that we'd need a much better ui to define this. """ # These are all dublin core subjects. # 3.1 defines the ability to use an authority, but that # might be a bit too complicated right now. if "genre" in configDictionary.keys(): genreListConf = configDictionary["genre"] if isinstance(configDictionary["genre"], dict): genreListConf = configDictionary["genre"].keys() for g in genreListConf: subject = opfFile.createElement("dc:subject") subject.appendChild(opfFile.createTextNode(g)) opfMeta.appendChild(subject) if "characters" in configDictionary.keys(): for name in configDictionary["characters"]: char = opfFile.createElement("dc:subject") char.appendChild(opfFile.createTextNode(name)) opfMeta.appendChild(char) if "format" in configDictionary.keys(): for formatF in configDictionary["format"]: f = opfFile.createElement("dc:subject") f.appendChild(opfFile.createTextNode(formatF)) opfMeta.appendChild(f) if "otherKeywords" in configDictionary.keys(): for key in configDictionary["otherKeywords"]: word = opfFile.createElement("dc:subject") word.appendChild(opfFile.createTextNode(key)) opfMeta.appendChild(word) # Pre-pagination and layout # Comic are always prepaginated. elLayout = opfFile.createElement("meta") elLayout.setAttribute("property", "rendition:layout") elLayout.appendChild(opfFile.createTextNode("pre-paginated")) opfMeta.appendChild(elLayout) # We should figure out if the pages are portrait or not... elOrientation = opfFile.createElement("meta") elOrientation.setAttribute("property", "rendition:orientation") elOrientation.appendChild(opfFile.createTextNode("portrait")) opfMeta.appendChild(elOrientation) elSpread = opfFile.createElement("meta") elSpread.setAttribute("property", "rendition:spread") elSpread.appendChild(opfFile.createTextNode("landscape")) opfMeta.appendChild(elSpread) opfRoot.appendChild(opfMeta) # Manifest opfManifest = opfFile.createElement("manifest") toc = opfFile.createElement("item") toc.setAttribute("id", "ncx") toc.setAttribute("href", "toc.ncx") toc.setAttribute("media-type", "application/x-dtbncx+xml") opfManifest.appendChild(toc) region = opfFile.createElement("item") region.setAttribute("id", "regions") region.setAttribute("href", "region-nav.xhtml") region.setAttribute("media-type", "application/xhtml+xml") region.setAttribute("properties", "data-nav") # Set the propernavmap to use this later) opfManifest.appendChild(region) nav = opfFile.createElement("item") nav.setAttribute("id", "nav") nav.setAttribute("href", "nav.xhtml") nav.setAttribute("media-type", "application/xhtml+xml") nav.setAttribute("properties", "nav") # Set the propernavmap to use this later) opfManifest.appendChild(nav) ids = 0 for p in pagesList: item = opfFile.createElement("item") item.setAttribute("id", "img"+str(ids)) ids +=1 item.setAttribute("href", os.path.relpath(p, str(path))) item.setAttribute("media-type", "image/png") if os.path.basename(p) == os.path.basename(coverpageurl): item.setAttribute("properties", "cover-image") opfManifest.appendChild(item) ids = 0 for p in htmlFiles: item = opfFile.createElement("item") item.setAttribute("id", "p"+str(ids)) ids +=1 item.setAttribute("href", os.path.relpath(p, str(path))) item.setAttribute("media-type", "application/xhtml+xml") opfManifest.appendChild(item) opfRoot.appendChild(opfManifest) # Spine opfSpine = opfFile.createElement("spine") # this sets the table of contents to use the ncx file opfSpine.setAttribute("toc", "ncx") # Reading Direction: spreadRight = True direction = 0 if "readingDirection" in configDictionary.keys(): - if configDictionary["readingDirection"] is "rightToLeft": + if configDictionary["readingDirection"] == "rightToLeft": opfSpine.setAttribute("page-progression-direction", "rtl") spreadRight = False direction = 1 else: opfSpine.setAttribute("page-progression-direction", "ltr") # Here we'd need to switch between the two and if spread keywrod use neither but combine with spread-none ids = 0 for p in htmlFiles: item = opfFile.createElement("itemref") item.setAttribute("idref", "p"+str(ids)) ids +=1 props = [] if p in listofSpreads: # Put this one in the center. props.append("rendition:page-spread-center") # Reset the spread boolean. # It needs to point at the first side after the spread. # So ltr -> spread-left, rtl->spread-right if direction == 0: spreadRight = False else: spreadRight = True else: if spreadRight: props.append("page-spread-right") spreadRight = False else: props.append("page-spread-left") spreadRight = True item.setAttribute("properties", " ".join(props)) opfSpine.appendChild(item) opfRoot.appendChild(opfSpine) # Guide opfGuide = opfFile.createElement("guide") if coverpagehtml is not None and coverpagehtml.isspace() is False and len(coverpagehtml) > 0: item = opfFile.createElement("reference") item.setAttribute("type", "cover") item.setAttribute("title", "Cover") item.setAttribute("href", coverpagehtml) opfGuide.appendChild(item) opfRoot.appendChild(opfGuide) docFile = open(str(Path(path / "content.opf")), 'w', newline="", encoding="utf-8") docFile.write(opfFile.toString(indent=2)) docFile.close() return str(Path(path / "content.opf")) """ Write a region navmap file. """ def write_region_nav_file(path, configDictionary, htmlFiles, regions = []): navDoc = QDomDocument() navRoot = navDoc.createElement("html") navRoot.setAttribute("xmlns", "http://www.w3.org/1999/xhtml") navRoot.setAttribute("xmlns:epub", "http://www.idpf.org/2007/ops") navDoc.appendChild(navRoot) head = navDoc.createElement("head") title = navDoc.createElement("title") title.appendChild(navDoc.createTextNode("Region Navigation")) head.appendChild(title) navRoot.appendChild(head) body = navDoc.createElement("body") navRoot.appendChild(body) nav = navDoc.createElement("nav") nav.setAttribute("epub:type", "region-based") nav.setAttribute("prefix", "ahl: http://idpf.org/epub/vocab/ahl") body.appendChild(nav) # Let's write the panels and balloons down now. olPanels = navDoc.createElement("ol") for region in regions: if region["type"] == "panel": pageName = os.path.relpath(region["page"], str(path)) print("accessing panel") li = navDoc.createElement("li") li.setAttribute("epub:type", "panel") anchor = navDoc.createElement("a") bounds = region["points"] anchor.setAttribute("href", pageName+"#xywh=percent:"+str(bounds.x())+","+str(bounds.y())+","+str(bounds.width())+","+str(bounds.height())) if len(region["primaryColor"])>0: primaryC = navDoc.createElement("meta") primaryC.setAttribute("property","ahl:primary-color") primaryC.setAttribute("content", region["primaryColor"]) anchor.appendChild(primaryC) li.appendChild(anchor) olBalloons = navDoc.createElement("ol") """ The region nav spec specifies that we should have text-areas/balloons as a refinement on the panel. For each panel, we'll check if there's balloons/text-areas inside, and we'll do that by checking whether the center point is inside the panel because some comics have balloons that overlap the gutters. """ for balloon in regions: if balloon["type"] == "text" and balloon["page"] == region["page"] and bounds.contains(balloon["points"].center()): liBalloon = navDoc.createElement("li") liBalloon.setAttribute("epub:type", "text-area") anchorBalloon = navDoc.createElement("a") BBounds = balloon["points"] anchorBalloon.setAttribute("href", pageName+"#xywh=percent:"+str(BBounds.x())+","+str(BBounds.y())+","+str(BBounds.width())+","+str(BBounds.height())) liBalloon.appendChild(anchorBalloon) olBalloons.appendChild(liBalloon) if olBalloons.hasChildNodes(): li.appendChild(olBalloons) olPanels.appendChild(li) nav.appendChild(olPanels) navFile = open(str(Path(path / "region-nav.xhtml")), 'w', newline="", encoding="utf-8") navFile.write(navDoc.toString(indent=2)) navFile.close() return str(Path(path / "region-nav.xhtml")) """ Write XHTML nav file. This is virtually the same as the NCX file, except that the navigation document can be styled, and is what 3.1 and 3.2 expect as a primary navigation document. This function will both create a table of contents, using the "acbf_title" feature, as well as a regular pageslist. """ def write_nav_file(path, configDictionary, htmlFiles, listOfNavItems): navDoc = QDomDocument() navRoot = navDoc.createElement("html") navRoot.setAttribute("xmlns", "http://www.w3.org/1999/xhtml") navRoot.setAttribute("xmlns:epub", "http://www.idpf.org/2007/ops") navDoc.appendChild(navRoot) head = navDoc.createElement("head") title = navDoc.createElement("title") title.appendChild(navDoc.createTextNode("Table of Contents")) head.appendChild(title) navRoot.appendChild(head) body = navDoc.createElement("body") navRoot.appendChild(body) # The Table of Contents toc = navDoc.createElement("nav") toc.setAttribute("epub:type", "toc") oltoc = navDoc.createElement("ol") li = navDoc.createElement("li") anchor = navDoc.createElement("a") anchor.setAttribute("href", os.path.relpath(htmlFiles[0], str(path))) anchor.appendChild(navDoc.createTextNode("Start")) li.appendChild(anchor) oltoc.appendChild(li) for fileName in listOfNavItems.keys(): li = navDoc.createElement("li") anchor = navDoc.createElement("a") anchor.setAttribute("href", os.path.relpath(fileName, str(path))) anchor.appendChild(navDoc.createTextNode(listOfNavItems[fileName])) li.appendChild(anchor) oltoc.appendChild(li) toc.appendChild(oltoc) body.appendChild(toc) # The Pages List. pageslist = navDoc.createElement("nav") pageslist.setAttribute("epub:type", "page-list") olpages = navDoc.createElement("ol") entry = 1 for i in range(len(htmlFiles)): li = navDoc.createElement("li") anchor = navDoc.createElement("a") anchor.setAttribute("href", os.path.relpath(htmlFiles[1], str(path))) anchor.appendChild(navDoc.createTextNode(str(i))) li.appendChild(anchor) olpages.appendChild(li) pageslist.appendChild(olpages) body.appendChild(pageslist) navFile = open(str(Path(path / "nav.xhtml")), 'w', newline="", encoding="utf-8") navFile.write(navDoc.toString(indent=2)) navFile.close() return str(Path(path / "nav.xhtml")) """ Write a NCX file. This is the same as the navigation document above, but then for 2.0 backward compatibility. """ def write_ncx_file(path, configDictionary, htmlFiles, listOfNavItems): tocDoc = QDomDocument() ncx = tocDoc.createElement("ncx") ncx.setAttribute("version", "2005-1") ncx.setAttribute("xmlns", "http://www.daisy.org/z3986/2005/ncx/") tocDoc.appendChild(ncx) tocHead = tocDoc.createElement("head") # NCX also has some meta values that are in the head. # They are shared with the opf metadata document. uuid = str(configDictionary["uuid"]) uuid = uuid.strip("{") uuid = uuid.strip("}") metaID = tocDoc.createElement("meta") metaID.setAttribute("content", uuid) metaID.setAttribute("name", "dtb:uid") tocHead.appendChild(metaID) metaDepth = tocDoc.createElement("meta") metaDepth.setAttribute("content", str(1)) metaDepth.setAttribute("name", "dtb:depth") tocHead.appendChild(metaDepth) metaTotal = tocDoc.createElement("meta") metaTotal.setAttribute("content", str(len(htmlFiles))) metaTotal.setAttribute("name", "dtb:totalPageCount") tocHead.appendChild(metaTotal) metaMax = tocDoc.createElement("meta") metaMax.setAttribute("content", str(len(htmlFiles))) metaMax.setAttribute("name", "dtb:maxPageNumber") tocHead.appendChild(metaDepth) ncx.appendChild(tocHead) docTitle = tocDoc.createElement("docTitle") text = tocDoc.createElement("text") if "title" in configDictionary.keys(): text.appendChild(tocDoc.createTextNode(str(configDictionary["title"]))) else: text.appendChild(tocDoc.createTextNode("Comic with no Name")) docTitle.appendChild(text) ncx.appendChild(docTitle) # The navmap is a table of contents. navmap = tocDoc.createElement("navMap") navPoint = tocDoc.createElement("navPoint") navPoint.setAttribute("id", "navPoint-1") navPoint.setAttribute("playOrder", "1") navLabel = tocDoc.createElement("navLabel") navLabelText = tocDoc.createElement("text") navLabelText.appendChild(tocDoc.createTextNode("Start")) navLabel.appendChild(navLabelText) navContent = tocDoc.createElement("content") navContent.setAttribute("src", os.path.relpath(htmlFiles[0], str(path))) navPoint.appendChild(navLabel) navPoint.appendChild(navContent) navmap.appendChild(navPoint) entry = 1 for fileName in listOfNavItems.keys(): entry +=1 navPointT = tocDoc.createElement("navPoint") navPointT.setAttribute("id", "navPoint-"+str(entry)) navPointT.setAttribute("playOrder", str(entry)) navLabelT = tocDoc.createElement("navLabel") navLabelTText = tocDoc.createElement("text") navLabelTText.appendChild(tocDoc.createTextNode(listOfNavItems[fileName])) navLabelT.appendChild(navLabelTText) navContentT = tocDoc.createElement("content") navContentT.setAttribute("src", os.path.relpath(fileName, str(path))) navPointT.appendChild(navLabelT) navPointT.appendChild(navContentT) navmap.appendChild(navPointT) ncx.appendChild(navmap) # The pages list on the other hand just lists all pages. pagesList = tocDoc.createElement("pageList") navLabelPages = tocDoc.createElement("navLabel") navLabelPagesText = tocDoc.createElement("text") navLabelPagesText.appendChild(tocDoc.createTextNode("Pages")) navLabelPages.appendChild(navLabelPagesText) pagesList.appendChild(navLabelPages) for i in range(len(htmlFiles)): pageTarget = tocDoc.createElement("pageTarget") pageTarget.setAttribute("type", "normal") pageTarget.setAttribute("id", "page-"+str(i)) pageTarget.setAttribute("value", str(i)) navLabelPagesTarget = tocDoc.createElement("navLabel") navLabelPagesTargetText = tocDoc.createElement("text") navLabelPagesTargetText.appendChild(tocDoc.createTextNode(str(i+1))) navLabelPagesTarget.appendChild(navLabelPagesTargetText) pageTarget.appendChild(navLabelPagesTarget) pageTargetContent = tocDoc.createElement("content") pageTargetContent.setAttribute("src", os.path.relpath(htmlFiles[i], str(path))) pageTarget.appendChild(pageTargetContent) pagesList.appendChild(pageTarget) ncx.appendChild(pagesList) # Save the document. docFile = open(str(Path(path / "toc.ncx")), 'w', newline="", encoding="utf-8") docFile.write(tocDoc.toString(indent=2)) docFile.close() return str(Path(path / "toc.ncx")) diff --git a/plugins/python/mixer_slider_docker/kritapykrita_mixer_slider_docker.desktop b/plugins/python/mixer_slider_docker/kritapykrita_mixer_slider_docker.desktop index e238a57730..ecb3dd734b 100644 --- a/plugins/python/mixer_slider_docker/kritapykrita_mixer_slider_docker.desktop +++ b/plugins/python/mixer_slider_docker/kritapykrita_mixer_slider_docker.desktop @@ -1,40 +1,42 @@ [Desktop Entry] Type=Service ServiceTypes=Krita/PythonPlugin X-KDE-Library=mixer_slider_docker X-Python-2-Compatible=false X-Krita-Manual=Manual.html Name=Mixer Slider docker Name[ca]=Acoblador Control lliscant del mesclador Name[ca@valencia]=Acoblador Control lliscant del mesclador Name[en_GB]=Mixer Slider docker Name[es]=Panel del deslizador del mezclador Name[et]=Mikseri liuguri dokk Name[eu]=Nahasle graduatzaile panela Name[it]=Area di aggancio cursore di miscelazione Name[ko]=믹서 슬라이더 도커 Name[nl]=Vastzetter van schuifregelaar van mixer Name[nn]=Fargeblandingsdokk Name[pt]=Área da Barra de Mistura +Name[pt_BR]=Docker do misturador de barras Name[sv]=Dockningsfönster för blandningsreglage Name[uk]=Бічна панель повзунка мікшера Name[x-test]=xxMixer Slider dockerxx Name[zh_CN]=混色滑动条工具面板 Name[zh_TW]=混色滑動條工具面板 Comment=A color slider. Comment[ca]=Un control lliscant per al color. Comment[ca@valencia]=Un control lliscant per al color. Comment[en_GB]=A colour slider. Comment[es]=Deslizador de color. Comment[et]=Värviliugur. Comment[eu]=Kolore graduatzaile bat. Comment[it]=Cursore del colore. Comment[ko]=컬러 슬라이더입니다. Comment[nl]=Een schuifregelaar voor kleur Comment[nn]=Palett med fargeovergangar Comment[pt]=Uma barra de cores. +Comment[pt_BR]=Uma barra de cores. Comment[sv]=Ett färgreglage. Comment[uk]=Повзунок кольору. Comment[x-test]=xxA color slider.xx Comment[zh_CN]=一个颜色控制滑动条面板。 Comment[zh_TW]=色彩滑動條。