ソースを参照

Add Export Layers Enhanced Extension

The Export Layers plugin that comes with Krita is great... except it
does not allow us to set export settings in batch mode. This version of
the plugin exposes common export options.
cinaeco 4 年 前
コミット
61c1042faa

+ 8 - 0
export_layers_enhanced.desktop

@@ -0,0 +1,8 @@
+[Desktop Entry]
+Type=Service
+ServiceTypes=Krita/PythonPlugin
+X-KDE-Library=export_layers_enhanced
+X-Krita-Manual=Manual.html
+X-Python-2-Compatible=true
+Name=Export Layers Enhanced
+Comment=Plugin to export layers from a document. Now with a couple more PNG export options exposed.

+ 42 - 0
export_layers_enhanced/Manual.html

@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE html>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head><title>Export Layers Enhanced Plugin Manual</title>
+</head>
+<body>
+<h1 id="export-layers">Export Layers Enhanced</h1>
+<p>The Export Layers Enhanced Plugin allows you to select a document and export its layers in an ordered and sensible manner.</p>
+<h2 id="basic-options">Basic Options</h2>
+<dl>
+<dt>Documents</dt>
+<dd>A the list of open documents to choose from.
+</dd>
+<dt>Refresh</dt>
+<dd>Refresh the list of documents.
+</dd>
+<dt>Initial Directory</dt>
+<dd>The place where the layers will be saved. Called ‘Initial Directory’ because each group layer will have its own directory.
+</dd>
+</dl>
+<h2 id="export-options">Export Options</h2>
+<dl>
+<dt>Export Filter Layers</dt>
+<dd>Export filter layers’ masks.
+</dd>
+<dt>Export in batchmode</dt>
+<dd>Don’t show the options dialog for each function.
+</dd>
+<dt>Ignore invisible Layers</dt>
+<dd>Don’t save out layers that are invisible
+</dd>
+<dt>Resolution</dt>
+<dd>The resolution of the whole image. Layers that are smaller are scaled accordingly.
+</dd>
+<dt>Image Extensions</dt>
+<dd>The choice is either png or jpeg.
+</dd>
+</dl>
+<p>Press OK and the export layers plugin will export all the layers!</p>
+</body>
+</html>

+ 4 - 0
export_layers_enhanced/__init__.py

@@ -0,0 +1,4 @@
+import krita
+from .export_layers_enhanced import ExportLayersEnhancedExtension
+
+Scripter.addExtension(ExportLayersEnhancedExtension(krita.Krita.instance()))

+ 34 - 0
export_layers_enhanced/export_layers_enhanced.py

@@ -0,0 +1,34 @@
+# This script is licensed CC 0 1.0, so that you can learn from it.
+
+# ------ CC 0 1.0 ---------------
+
+# The person who associated a work with this deed has dedicated the
+# work to the public domain by waiving all of his or her rights to the
+# work worldwide under copyright law, including all related and
+# neighboring rights, to the extent allowed by law.
+
+# You can copy, modify, distribute and perform the work, even for
+# commercial purposes, all without asking permission.
+
+# https://creativecommons.org/publicdomain/zero/1.0/legalcode
+
+import krita
+from . import uiexportlayers
+
+
+class ExportLayersEnhancedExtension(krita.Extension):
+
+    def __init__(self, parent):
+        super(ExportLayersEnhancedExtension, self).__init__(parent)
+
+    def setup(self):
+        pass
+
+    def createActions(self, window):
+        action = window.createAction("export_layers_enhanced", i18n("Export Layers Enhanced"))
+        action.setToolTip(i18n("Plugin to export layers from a document."))
+        action.triggered.connect(self.initialize)
+
+    def initialize(self):
+        self.uiexportlayers = uiexportlayers.UIExportLayers()
+        self.uiexportlayers.initialize()

+ 24 - 0
export_layers_enhanced/exportlayersdialog.py

@@ -0,0 +1,24 @@
+# This script is licensed CC 0 1.0, so that you can learn from it.
+
+# ------ CC 0 1.0 ---------------
+
+# The person who associated a work with this deed has dedicated the
+# work to the public domain by waiving all of his or her rights to the
+# work worldwide under copyright law, including all related and
+# neighboring rights, to the extent allowed by law.
+
+# You can copy, modify, distribute and perform the work, even for
+# commercial purposes, all without asking permission.
+
+# https://creativecommons.org/publicdomain/zero/1.0/legalcode
+
+from PyQt5.QtWidgets import QDialog
+
+
+class ExportLayersDialog(QDialog):
+
+    def __init__(self, parent=None):
+        super(ExportLayersDialog, self).__init__(parent)
+
+    def closeEvent(self, event):
+        event.accept()

+ 241 - 0
export_layers_enhanced/uiexportlayers.py

@@ -0,0 +1,241 @@
+# This script is licensed CC 0 1.0, so that you can learn from it.
+
+# ------ CC 0 1.0 ---------------
+
+# The person who associated a work with this deed has dedicated the
+# work to the public domain by waiving all of his or her rights to the
+# work worldwide under copyright law, including all related and
+# neighboring rights, to the extent allowed by law.
+
+# You can copy, modify, distribute and perform the work, even for
+# commercial purposes, all without asking permission.
+
+# https://creativecommons.org/publicdomain/zero/1.0/legalcode
+
+from . import exportlayersdialog
+from PyQt5.QtCore import (Qt, QRect)
+from PyQt5.QtWidgets import (QFormLayout, QListWidget, QHBoxLayout,
+                             QDialogButtonBox, QVBoxLayout, QFrame,
+                             QPushButton, QAbstractScrollArea, QLineEdit,
+                             QMessageBox, QFileDialog, QCheckBox, QSpinBox,
+                             QComboBox)
+import os
+import krita
+
+
+class UIExportLayers(object):
+
+    def __init__(self):
+        self.mainDialog = exportlayersdialog.ExportLayersDialog()
+        self.mainLayout = QVBoxLayout(self.mainDialog)
+        self.formLayout = QFormLayout()
+        self.resSpinBoxLayout = QFormLayout()
+        self.documentLayout = QVBoxLayout()
+        self.directorySelectorLayout = QHBoxLayout()
+        self.optionsLayout = QVBoxLayout()
+        self.rectSizeLayout = QHBoxLayout()
+
+        self.refreshButton = QPushButton(i18n("Refresh"))
+        self.widgetDocuments = QListWidget()
+        self.directoryTextField = QLineEdit()
+        self.directoryDialogButton = QPushButton(i18n("..."))
+        self.exportFilterLayersCheckBox = QCheckBox(
+            i18n("Export filter layers"))
+        self.batchmodeCheckBox = QCheckBox(i18n("Export in batchmode"))
+        self.ignoreInvisibleLayersCheckBox = QCheckBox(
+            i18n("Ignore invisible layers"))
+        self.cropToImageBounds = QCheckBox(
+                i18n("Adjust export size to layer content"))
+
+        self.rectWidthSpinBox = QSpinBox()
+        self.rectHeightSpinBox = QSpinBox()
+        self.formatsComboBox = QComboBox()
+        self.resSpinBox = QSpinBox()
+
+        self.buttonBox = QDialogButtonBox(
+            QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+
+        self.kritaInstance = krita.Krita.instance()
+        self.documentsList = []
+
+        self.directoryTextField.setReadOnly(True)
+        self.batchmodeCheckBox.setChecked(True)
+        self.directoryDialogButton.clicked.connect(self._selectDir)
+        self.widgetDocuments.currentRowChanged.connect(self._setResolution)
+        self.refreshButton.clicked.connect(self.refreshButtonClicked)
+        self.buttonBox.accepted.connect(self.confirmButton)
+        self.buttonBox.rejected.connect(self.mainDialog.close)
+        self.cropToImageBounds.stateChanged.connect(self._toggleCropSize)
+
+        self.mainDialog.setWindowModality(Qt.NonModal)
+        self.widgetDocuments.setSizeAdjustPolicy(
+            QAbstractScrollArea.AdjustToContents)
+
+    def initialize(self):
+        self.loadDocuments()
+
+        self.rectWidthSpinBox.setRange(1, 10000)
+        self.rectHeightSpinBox.setRange(1, 10000)
+        self.resSpinBox.setRange(20, 1200)
+
+        self.formatsComboBox.addItem(i18n("JPEG"))
+        self.formatsComboBox.addItem(i18n("PNG"))
+
+        self.documentLayout.addWidget(self.widgetDocuments)
+        self.documentLayout.addWidget(self.refreshButton)
+
+        self.directorySelectorLayout.addWidget(self.directoryTextField)
+        self.directorySelectorLayout.addWidget(self.directoryDialogButton)
+
+        self.optionsLayout.addWidget(self.exportFilterLayersCheckBox)
+        self.optionsLayout.addWidget(self.batchmodeCheckBox)
+        self.optionsLayout.addWidget(self.ignoreInvisibleLayersCheckBox)
+        self.optionsLayout.addWidget(self.cropToImageBounds)
+
+        self.resSpinBoxLayout.addRow(i18n("dpi:"), self.resSpinBox)
+
+        self.rectSizeLayout.addWidget(self.rectWidthSpinBox)
+        self.rectSizeLayout.addWidget(self.rectHeightSpinBox)
+        self.rectSizeLayout.addLayout(self.resSpinBoxLayout)
+
+        self.formLayout.addRow(i18n("Documents:"), self.documentLayout)
+        self.formLayout.addRow(
+            i18n("Initial directory:"), self.directorySelectorLayout)
+        self.formLayout.addRow(i18n("Export options:"), self.optionsLayout)
+        self.formLayout.addRow(i18n("Export size:"), self.rectSizeLayout)
+        self.formLayout.addRow(
+            i18n("Images extensions:"), self.formatsComboBox)
+
+        self.line = QFrame()
+        self.line.setFrameShape(QFrame.HLine)
+        self.line.setFrameShadow(QFrame.Sunken)
+
+        self.mainLayout.addLayout(self.formLayout)
+        self.mainLayout.addWidget(self.line)
+        self.mainLayout.addWidget(self.buttonBox)
+
+        self.mainDialog.resize(500, 300)
+        self.mainDialog.setWindowTitle(i18n("Export Layers"))
+        self.mainDialog.setSizeGripEnabled(True)
+        self.mainDialog.show()
+        self.mainDialog.activateWindow()
+
+    def loadDocuments(self):
+        self.widgetDocuments.clear()
+
+        self.documentsList = [
+            document for document in self.kritaInstance.documents()
+            if document.fileName()
+        ]
+
+        for document in self.documentsList:
+            self.widgetDocuments.addItem(document.fileName())
+
+    def refreshButtonClicked(self):
+        self.loadDocuments()
+
+    def confirmButton(self):
+        selectedPaths = [
+            item.text() for item in self.widgetDocuments.selectedItems()]
+        selectedDocuments = [
+            document for document in self.documentsList
+            for path in selectedPaths if path == document.fileName()
+        ]
+
+        self.msgBox = QMessageBox(self.mainDialog)
+        if not selectedDocuments:
+            self.msgBox.setText(i18n("Select one document."))
+        elif not self.directoryTextField.text():
+            self.msgBox.setText(i18n("Select the initial directory."))
+        else:
+            self.export(selectedDocuments[0])
+            self.msgBox.setText(i18n("All layers has been exported."))
+        self.msgBox.exec_()
+
+    def mkdir(self, directory):
+        target_directory = self.directoryTextField.text() + directory
+        if (os.path.exists(target_directory)
+                and os.path.isdir(target_directory)):
+            return
+
+        try:
+            os.makedirs(target_directory)
+        except OSError as e:
+            raise e
+
+    def export(self, document):
+        Application.setBatchmode(self.batchmodeCheckBox.isChecked())
+
+        documentName = document.fileName() if document.fileName() else 'Untitled'  # noqa: E501
+        fileName, extension = os.path.splitext(os.path.basename(documentName))
+        self.mkdir('/' + fileName)
+
+        self._exportLayers(
+            document.rootNode(),
+            self.formatsComboBox.currentText(),
+            '/' + fileName)
+        Application.setBatchmode(True)
+
+    def _exportLayers(self, parentNode, fileFormat, parentDir):
+        """ This method get all sub-nodes from the current node and export then in
+            the defined format."""
+
+        for node in parentNode.childNodes():
+            newDir = ''
+            if node.type() == 'grouplayer':
+                newDir = os.path.join(parentDir, node.name())
+                self.mkdir(newDir)
+            elif (not self.exportFilterLayersCheckBox.isChecked()
+                  and 'filter' in node.type()):
+                continue
+            elif (self.ignoreInvisibleLayersCheckBox.isChecked()
+                  and not node.visible()):
+                continue
+            else:
+                nodeName = node.name()
+                _fileFormat = self.formatsComboBox.currentText()
+                exportConfig = krita.InfoObject()
+                if '[jpeg]' in nodeName:
+                    _fileFormat = 'jpeg'
+                elif '[png]' in nodeName:
+                    _fileFormat = 'png'
+
+                if _fileFormat.lower() == 'png':
+                    # Values from https://api.kde.org/appscomplete-api/krita-apidocs/libs/libkis/html/classDocument.html#abda324b9beee9384879af8b7e7807cfa
+                    # https://api.kde.org/appscomplete-api/krita-apidocs/libs/libkis/html/classInfoObject.html#a45fd7051d88d4d02221cb81d54189bc3
+                    exportConfig.setProperty('alpha', False)
+                    exportConfig.setProperty('compression', 9)
+                    # exportConfig.setProperty('transparencyFillcolor', [255, 255, 255])
+
+                if self.cropToImageBounds.isChecked():
+                    bounds = QRect()
+                else:
+                    bounds = QRect(0, 0, self.rectWidthSpinBox.value(), self.rectHeightSpinBox.value())
+
+                layerFileName = '{0}{1}/{2}.{3}'.format(
+                    self.directoryTextField.text(),
+                    parentDir, node.name(), _fileFormat)
+                node.save(layerFileName, self.resSpinBox.value() / 72.,
+                          self.resSpinBox.value() / 72., exportConfig, bounds)
+
+            if node.childNodes():
+                self._exportLayers(node, fileFormat, newDir)
+
+    def _selectDir(self):
+        directory = QFileDialog.getExistingDirectory(
+            self.mainDialog,
+            i18n("Select a Folder"),
+            os.path.expanduser("~"),
+            QFileDialog.ShowDirsOnly)
+        self.directoryTextField.setText(directory)
+
+    def _setResolution(self, index):
+        document = self.documentsList[index]
+        self.rectWidthSpinBox.setValue(document.width())
+        self.rectHeightSpinBox.setValue(document.height())
+        self.resSpinBox.setValue(document.resolution())
+
+    def _toggleCropSize(self):
+        cropToLayer = self.cropToImageBounds.isChecked()
+        self.rectWidthSpinBox.setDisabled(cropToLayer)
+        self.rectHeightSpinBox.setDisabled(cropToLayer)