From befacfd0d4d92c50b7f3c1537c3f2a4dfe4688d3 Mon Sep 17 00:00:00 2001 From: Alx Sa Date: Fri, 2 May 2025 11:34:06 +0000 Subject: [PATCH] python: Add export support for Krita .kpl palette Resolves #13889 Adds support for exporting palettes in Krita's .kpl format (zipped .xml data). Colors are currently saved as 32-bit floating point RGB values. Future work includes supporting other color spaces like CMYK, other precisions, and including color profiles for colors. --- plug-ins/python/meson.build | 1 + plug-ins/python/palette-export-as-kpl.py | 196 +++++++++++++++++++++++ po-python/POTFILES.in | 1 + 3 files changed, 198 insertions(+) create mode 100644 plug-ins/python/palette-export-as-kpl.py diff --git a/plug-ins/python/meson.build b/plug-ins/python/meson.build index 66226c2cf3..02897d999b 100644 --- a/plug-ins/python/meson.build +++ b/plug-ins/python/meson.build @@ -4,6 +4,7 @@ plugins = [ { 'name': 'foggify' }, { 'name': 'gradients-save-as-css' }, { 'name': 'histogram-export' }, + { 'name': 'palette-export-as-kpl' }, { 'name': 'palette-offset' }, { 'name': 'palette-sort' }, { 'name': 'palette-to-gradient' }, diff --git a/plug-ins/python/palette-export-as-kpl.py b/plug-ins/python/palette-export-as-kpl.py new file mode 100644 index 0000000000..e9d50bccf7 --- /dev/null +++ b/plug-ins/python/palette-export-as-kpl.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Allows exporting Krita's .kpl palettes +# +# This program 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. +# +# This program 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 this program. If not, see . + +import gi +gi.require_version('Gimp', '3.0') +from gi.repository import Gimp +gi.require_version('GimpUi', '3.0') +from gi.repository import GimpUi +from gi.repository import GObject +from gi.repository import GLib +from gi.repository import Gio +gi.require_version('Gtk', '3.0') +from gi.repository import Gtk +import os, sys, tempfile, zipfile +import xml.etree.ElementTree as ET + +def N_(message): return message +def _(message): return GLib.dgettext(None, message) + +# Taken from file-openraster.py +def write_file_str(zfile, fname, data): + # work around a permission bug in the zipfile library: + # http://bugs.python.org/issue3394 + zi = zipfile.ZipInfo(fname) + zi.external_attr = int("100644", 8) << 16 + zfile.writestr(zi, data) + +def palette_kpl_export(procedure, config, data): + success = True + runmode = config.get_property("run-mode") + + if runmode == Gimp.RunMode.INTERACTIVE: + GimpUi.init('python-fu-palette-export-as-kpl') + dialog = GimpUi.ProcedureDialog(procedure=procedure, config=config) + + # Fill the dialog. + config.set_property("palette", None) + config.set_property("file", None) + + dialog.get_label ("options-label", _("Options"), False, False) + hbox = dialog.fill_box ("options-box", ["comment", "read-only"]) + dialog.fill_frame("options-frame", "options-label", False, "options-box") + + dialog.fill (["palette", "file", "options-frame"]) + dialog.set_ok_label (_("_Export")) + + if not dialog.run(): + dialog.destroy() + return procedure.new_return_values(Gimp.PDBStatusType.CANCEL, GLib.Error()) + + palette = config.get_property("palette") + file = config.get_property("file") + comment = config.get_property("comment") + read_only = config.get_property("read-only") + + if file is None or file.peek_path() is None: + error = 'No file given' + return procedure.new_return_values(Gimp.PDBStatusType.CALLING_ERROR, + GLib.Error(error)) + + tempdir = tempfile.mkdtemp('python-fu-palette-export-as-kpl') + + kplfile = zipfile.ZipFile(file.peek_path() + '.tmpsave', 'w', compression=zipfile.ZIP_STORED) + + write_file_str(kplfile, 'mimetype', 'application/x-krita-palette') # must be the first file written + + # Filling in palette + xml_palette = ET.Element('ColorSet') + a = xml_palette.attrib + if comment != "": + a['comment'] = comment + a['name'] = palette.get_name() + a['version'] = "1.0" + a['readonly'] = "true" if read_only == True else "false" + columns = palette.get_columns() + if columns == 0: + columns = 16 + a['columns'] = str(columns) + + count = palette.get_color_count() + + row = 0 + col = 0 + for i in range(count): + entry = ET.Element('ColorSetEntry') + xml_palette.append(entry) + + success, name = palette.get_entry_name(i) + if success: + a = entry.attrib + a['name'] = name + a['id'] = str(i + 1) + # TODO: Add options for different bit-depths + a['bitdepth'] = "F32" + a['spot'] = "false" + + color = palette.get_entry_color(i) + # TODO: Allow for other color spaces + pal = ET.Element('RGB') + entry.append(pal) + a = pal.attrib + + r, g, b, alpha = color.get_rgba() + a['r'] = str(r) + a['g'] = str(g) + a['b'] = str(b) + + pos = ET.Element('Position') + entry.append(pos) + a = pos.attrib + a['row'] = str(row) + a['column'] = str(col) + + col += 1 + if col >= columns: + col = 0 + row += 1 + else: + break; + + if success: + # Write everything to file + xml = ET.tostring(xml_palette, encoding='UTF-8') + write_file_str(kplfile, 'colorset.xml', xml) + + kplfile.close() + os.rmdir(tempdir) + if os.path.exists(file.peek_path()): + os.remove(file.peek_path()) # win32 needs that + os.rename(file.peek_path() + '.tmpsave', file.peek_path()) + + return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error()) + else: + kplfile.close() + os.rmdir(tempdir) + return procedure.new_return_values(Gimp.PDBStatusType.EXECUTION_ERROR, + GLib.Error('File exporting failed: {}'.format(file.get_path()))) + +class PaletteExportAsKPL (Gimp.PlugIn): + ## GimpPlugIn virtual methods ## + def do_set_i18n(self, procname): + return True, 'gimp30-python', None + + def do_query_procedures(self): + return [ 'python-fu-palette-export-as-kpl' ] + + def do_create_procedure(self, name): + procedure = Gimp.Procedure.new(self, name, + Gimp.PDBProcType.PLUGIN, + palette_kpl_export, None) + if name == 'python-fu-palette-export-as-kpl': + procedure.set_documentation (_("Export palette as Krita .kpl"), + _("Export palette as Krita .kpl"), + name) + procedure.set_menu_label(_("_Krita palette...")) + procedure.set_attribution("Alx Sa", + "(c) GPL V3.0 or later", + "2025") + procedure.add_menu_path('/Palettes Menu/Export as') + + procedure.add_enum_argument ("run-mode", _("Run mode"), + _("The run mode"), Gimp.RunMode, + Gimp.RunMode.NONINTERACTIVE, + GObject.ParamFlags.READWRITE) + procedure.add_palette_argument ("palette", _("_Palette to export"), + "", True, + None, True, # Default to context. + GObject.ParamFlags.READWRITE) + procedure.add_file_argument ("file", _("_File (.kpl)"), "", + Gimp.FileChooserAction.SAVE, + False, None, GObject.ParamFlags.READWRITE) + procedure.add_string_argument ("comment", _("Commen_t"), + _("Optional comment"), "", + GObject.ParamFlags.READWRITE) + procedure.add_boolean_argument ("read-only", + _("Re_ad only"), + _("The palette will be locked on import"), + False, GObject.ParamFlags.READWRITE) + return procedure + +Gimp.main(PaletteExportAsKPL.__gtype__, sys.argv) diff --git a/po-python/POTFILES.in b/po-python/POTFILES.in index 7ccd518f83..cb7d1d4aa6 100644 --- a/po-python/POTFILES.in +++ b/po-python/POTFILES.in @@ -5,6 +5,7 @@ plug-ins/python/colorxhtml.py plug-ins/python/foggify.py plug-ins/python/gradients-save-as-css.py plug-ins/python/histogram-export.py +plug-ins/python/palette-export-as-kpl.py plug-ins/python/palette-offset.py plug-ins/python/palette-sort.py plug-ins/python/palette-to-gradient.py