PIKApp/plug-ins/python/file-openraster.py

500 lines
20 KiB
Python

#!/usr/bin/env python3
# PIKA Plug-in for the OpenRaster file format
# http://create.freedesktop.org/wiki/OpenRaster
# Copyright (C) 2009 by Jon Nordby <jononor@gmail.com>
#
# 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.
#
# Based on MyPaint source code by Martin Renold
# http://gitorious.org/mypaint/mypaint/blobs/edd84bcc1e091d0d56aa6d26637aa8a925987b6a/lib/document.py
import gi
gi.require_version('Pika', '3.0')
from gi.repository import Pika
gi.require_version('Gegl', '0.4')
from gi.repository import Gegl
from gi.repository import GObject
from gi.repository import GLib
from gi.repository import Gio
import os, sys, tempfile, zipfile
import xml.etree.ElementTree as ET
NESTED_STACK_END = object()
layermodes_map = {
"svg:src-over": Pika.LayerMode.NORMAL,
"svg:multiply": Pika.LayerMode.MULTIPLY,
"svg:screen": Pika.LayerMode.SCREEN,
"svg:overlay": Pika.LayerMode.OVERLAY,
"svg:darken": Pika.LayerMode.DARKEN_ONLY,
"svg:lighten": Pika.LayerMode.LIGHTEN_ONLY,
"svg:color-dodge": Pika.LayerMode.DODGE,
"svg:color-burn": Pika.LayerMode.BURN,
"svg:hard-light": Pika.LayerMode.HARDLIGHT,
"svg:soft-light": Pika.LayerMode.SOFTLIGHT,
"svg:difference": Pika.LayerMode.DIFFERENCE,
"svg:color": Pika.LayerMode.HSL_COLOR,
"svg:luminosity": Pika.LayerMode.HSV_VALUE,
"svg:hue": Pika.LayerMode.HSV_HUE,
"svg:saturation": Pika.LayerMode.HSV_SATURATION,
"svg:plus": Pika.LayerMode.ADDITION,
}
# There are less svg blending ops than we have PIKA blend modes.
# We are going to map them as closely as possible.
pika_layermodes_map = {
Pika.LayerMode.NORMAL: "svg:src-over",
Pika.LayerMode.NORMAL_LEGACY: "svg:src-over",
Pika.LayerMode.MULTIPLY: "svg:multiply",
Pika.LayerMode.MULTIPLY_LEGACY: "svg:multiply",
Pika.LayerMode.SCREEN: "svg:screen",
Pika.LayerMode.SCREEN_LEGACY: "svg:screen",
Pika.LayerMode.OVERLAY: "svg:overlay",
Pika.LayerMode.OVERLAY_LEGACY: "svg:overlay",
Pika.LayerMode.DARKEN_ONLY: "svg:darken",
Pika.LayerMode.DARKEN_ONLY_LEGACY: "svg:darken",
Pika.LayerMode.LIGHTEN_ONLY: "svg:lighten",
Pika.LayerMode.LIGHTEN_ONLY_LEGACY: "svg:lighten",
Pika.LayerMode.DODGE: "svg:color-dodge",
Pika.LayerMode.DODGE_LEGACY: "svg:color-dodge",
Pika.LayerMode.BURN: "svg:color-burn",
Pika.LayerMode.BURN_LEGACY: "svg:color-burn",
Pika.LayerMode.HARDLIGHT: "svg:hard-light",
Pika.LayerMode.HARDLIGHT_LEGACY: "svg:hard-light",
Pika.LayerMode.SOFTLIGHT: "svg:soft-light",
Pika.LayerMode.SOFTLIGHT_LEGACY: "svg:soft-light",
Pika.LayerMode.DIFFERENCE: "svg:difference",
Pika.LayerMode.DIFFERENCE_LEGACY: "svg:difference",
Pika.LayerMode.HSL_COLOR: "svg:color",
Pika.LayerMode.HSL_COLOR_LEGACY: "svg:color",
Pika.LayerMode.HSV_VALUE: "svg:luminosity",
Pika.LayerMode.HSV_VALUE_LEGACY: "svg:luminosity",
Pika.LayerMode.HSV_HUE: "svg:hue",
Pika.LayerMode.HSV_HUE_LEGACY: "svg:hue",
Pika.LayerMode.HSV_SATURATION: "svg:saturation",
Pika.LayerMode.HSV_SATURATION_LEGACY: "svg:saturation",
Pika.LayerMode.ADDITION: "svg:plus",
Pika.LayerMode.ADDITION_LEGACY: "svg:plus",
# FIXME Determine the closest available layer mode
# Alternatively we could add additional modes
# e.g. something like "pika:dissolve", this
# is what Krita seems to do too
Pika.LayerMode.DISSOLVE: "svg:src-over",
Pika.LayerMode.DIVIDE: "svg:src-over",
Pika.LayerMode.DIVIDE_LEGACY: "svg:src-over",
Pika.LayerMode.BEHIND: "svg:src-over",
Pika.LayerMode.BEHIND_LEGACY: "svg:src-over",
Pika.LayerMode.GRAIN_EXTRACT: "svg:src-over",
Pika.LayerMode.GRAIN_EXTRACT_LEGACY: "svg:src-over",
Pika.LayerMode.GRAIN_MERGE: "svg:src-over",
Pika.LayerMode.GRAIN_MERGE_LEGACY: "svg:src-over",
Pika.LayerMode.COLOR_ERASE: "svg:src-over",
Pika.LayerMode.COLOR_ERASE_LEGACY: "svg:src-over",
Pika.LayerMode.LCH_HUE: "svg:src-over",
Pika.LayerMode.LCH_CHROMA: "svg:src-over",
Pika.LayerMode.LCH_COLOR: "svg:src-over",
Pika.LayerMode.LCH_LIGHTNESS: "svg:src-over",
Pika.LayerMode.SUBTRACT: "svg:src-over",
Pika.LayerMode.SUBTRACT_LEGACY: "svg:src-over",
Pika.LayerMode.VIVID_LIGHT: "svg:src-over",
Pika.LayerMode.PIN_LIGHT: "svg:src-over",
Pika.LayerMode.LINEAR_LIGHT: "svg:src-over",
Pika.LayerMode.HARD_MIX: "svg:src-over",
Pika.LayerMode.EXCLUSION: "svg:src-over",
Pika.LayerMode.LINEAR_BURN: "svg:src-over",
Pika.LayerMode.LUMA_DARKEN_ONLY: "svg:src-over",
Pika.LayerMode.LUMA_LIGHTEN_ONLY: "svg:src-over",
Pika.LayerMode.LUMINANCE: "svg:src-over",
Pika.LayerMode.COLOR_ERASE: "svg:src-over",
Pika.LayerMode.ERASE: "svg:src-over",
Pika.LayerMode.MERGE: "svg:src-over",
Pika.LayerMode.SPLIT: "svg:src-over",
Pika.LayerMode.PASS_THROUGH: "svg:src-over",
}
def reverse_map(mapping):
return dict((v,k) for k, v in mapping.items())
def get_image_attributes(orafile):
xml = orafile.read('stack.xml')
image = ET.fromstring(xml)
stack = image.find('stack')
w = int(image.attrib.get('w', ''))
h = int(image.attrib.get('h', ''))
return stack, w, h
def get_layer_attributes(layer):
a = layer.attrib
path = a.get('src', '')
name = a.get('name', '')
x = int(a.get('x', '0'))
y = int(a.get('y', '0'))
opac = float(a.get('opacity', '1.0'))
visible = a.get('visibility', 'visible') != 'hidden'
m = a.get('composite-op', 'svg:src-over')
layer_mode = layermodes_map.get(m, Pika.LayerMode.NORMAL)
return path, name, x, y, opac, visible, layer_mode
def get_group_layer_attributes(layer):
a = layer.attrib
name = a.get('name', '')
opac = float(a.get('opacity', '1.0'))
visible = a.get('visibility', 'visible') != 'hidden'
m = a.get('composite-op', 'svg:src-over')
layer_mode = layermodes_map.get(m, Pika.LayerMode.NORMAL)
return name, 0, 0, opac, visible, layer_mode
def thumbnail_ora(procedure, file, thumb_size, args, data):
tempdir = tempfile.mkdtemp('pika-plugin-file-openraster')
orafile = zipfile.ZipFile(file.peek_path())
stack, w, h = get_image_attributes(orafile)
# create temp file
tmp = os.path.join(tempdir, 'tmp.png')
with open(tmp, 'wb') as fid:
fid.write(orafile.read('Thumbnails/thumbnail.png'))
thumb_file = Gio.file_new_for_path(tmp)
pdb_proc = Pika.get_pdb().lookup_procedure('file-png-load')
pdb_config = pdb_proc.create_config()
pdb_config.set_property('run-mode', Pika.RunMode.NONINTERACTIVE)
pdb_config.set_property('file', thumb_file)
result = pdb_proc.run(pdb_config)
os.remove(tmp)
os.rmdir(tempdir)
if (result.index(0) == Pika.PDBStatusType.SUCCESS):
img = result.index(1)
# TODO: scaling
return Pika.ValueArray.new_from_values([
GObject.Value(Pika.PDBStatusType, Pika.PDBStatusType.SUCCESS),
GObject.Value(Pika.Image, img),
GObject.Value(GObject.TYPE_INT, w),
GObject.Value(GObject.TYPE_INT, h),
GObject.Value(Pika.ImageType, Pika.ImageType.RGB_IMAGE),
GObject.Value(GObject.TYPE_INT, 1)
])
else:
return procedure.new_return_values(result.index(0), GLib.Error(result.index(1)))
# We would expect the n_drawables parameter to not be there with introspection but
# currently that isn't working, see issue #5312. Until that is resolved we keep
# this parameter here or else saving would fail.
def save_ora(procedure, run_mode, image, n_drawables, drawables, file, metadata, config, data):
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)
Pika.progress_init("Exporting openraster image")
tempdir = tempfile.mkdtemp('pika-plugin-file-openraster')
# use .tmpsave extension, so we don't overwrite a valid file if
# there is an exception
orafile = zipfile.ZipFile(file.peek_path() + '.tmpsave', 'w', compression=zipfile.ZIP_STORED)
write_file_str(orafile, 'mimetype', 'image/openraster') # must be the first file written
# build image attributes
xml_image = ET.Element('image')
stack = ET.SubElement(xml_image, 'stack')
a = xml_image.attrib
a['w'] = str(image.get_width())
a['h'] = str(image.get_height())
def store_layer(image, drawable, path):
tmp = os.path.join(tempdir, 'tmp.png')
interlace, compression = 0, 2
pdb_proc = Pika.get_pdb().lookup_procedure('file-png-save')
pdb_config = pdb_proc.create_config()
pdb_config.set_property('run-mode', Pika.RunMode.NONINTERACTIVE)
pdb_config.set_property('image', image)
pdb_config.set_property('num-drawables', 1)
pdb_config.set_property('drawables', Pika.ObjectArray.new(Pika.Drawable, [drawable], False))
pdb_config.set_property('file', Gio.File.new_for_path(tmp))
pdb_config.set_property('interlaced', interlace)
pdb_config.set_property('compression', compression)
# write all PNG chunks except oFFs(ets)
pdb_config.set_property('bkgd', True)
pdb_config.set_property('offs', False)
pdb_config.set_property('phys', True)
pdb_config.set_property('time', True)
pdb_config.set_property('save-transparent', True)
pdb_proc.run(pdb_config)
if (os.path.exists(tmp)):
orafile.write(tmp, path)
os.remove(tmp)
else:
print("Error removing ", tmp)
def add_layer(parent, x, y, opac, pika_layer, path, visible=True):
store_layer(image, pika_layer, path)
# create layer attributes
layer = ET.Element('layer')
parent.append(layer)
a = layer.attrib
a['src'] = path
a['name'] = pika_layer.get_name()
a['x'] = str(x)
a['y'] = str(y)
a['opacity'] = str(opac)
a['visibility'] = 'visible' if visible else 'hidden'
a['composite-op'] = pika_layermodes_map.get(pika_layer.get_mode(), 'svg:src-over')
return layer
def add_group_layer(parent, opac, pika_layer, visible=True):
# create layer attributes
group_layer = ET.Element('stack')
parent.append(group_layer)
a = group_layer.attrib
a['name'] = pika_layer.get_name()
a['opacity'] = str(opac)
a['visibility'] = 'visible' if visible else 'hidden'
a['composite-op'] = pika_layermodes_map.get(pika_layer.get_mode(), 'svg:src-over')
return group_layer
def enumerate_layers(layers):
for layer in layers:
if not layer.is_group():
yield layer
else:
yield layer
for sublayer in enumerate_layers(layer.list_children()):
yield sublayer
yield NESTED_STACK_END
# save layers
parent_groups = []
i = 0
layer_stack = image.list_layers()
# Number of top level layers for tracking progress
lay_cnt = len(layer_stack)
for lay in enumerate_layers(layer_stack):
prev_lay = i
if lay is NESTED_STACK_END:
parent_groups.pop()
continue
_, x, y = lay.get_offsets()
opac = lay.get_opacity () / 100.0 # needs to be between 0.0 and 1.0
if not parent_groups:
path_name = 'data/{:03d}.png'.format(i)
i += 1
else:
path_name = 'data/{}-{:03d}.png'.format(
parent_groups[-1][1], parent_groups[-1][2])
parent_groups[-1][2] += 1
parent = stack if not parent_groups else parent_groups[-1][0]
if lay.is_group():
group = add_group_layer(parent, opac, lay, lay.get_visible())
group_path = ("{:03d}".format(i) if not parent_groups else
parent_groups[-1][1] + "-{:03d}".format(parent_groups[-1][2]))
parent_groups.append([group, group_path , 0])
else:
add_layer(parent, x, y, opac, lay, path_name, lay.get_visible())
if (i > prev_lay):
Pika.progress_update(i/lay_cnt)
# save mergedimage
thumb = image.duplicate()
thumb_layer = thumb.merge_visible_layers (Pika.MergeType.CLIP_TO_IMAGE)
store_layer (thumb, thumb_layer, 'mergedimage.png')
# save thumbnail
w, h = image.get_width(), image.get_height()
if max (w, h) > 256:
# should be at most 256x256, without changing aspect ratio
if w > h:
w, h = 256, max(h*256/w, 1)
else:
w, h = max(w*256/h, 1), 256
thumb_layer.scale(w, h, False)
if thumb.get_precision() != Pika.Precision.U8_GAMMA:
thumb.convert_precision (Pika.Precision.U8_GAMMA)
store_layer(thumb, thumb_layer, 'Thumbnails/thumbnail.png')
thumb.delete()
# write stack.xml
xml = ET.tostring(xml_image, encoding='UTF-8')
write_file_str(orafile, 'stack.xml', xml)
# finish up
orafile.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())
Pika.progress_end()
return Pika.ValueArray.new_from_values([
GObject.Value(Pika.PDBStatusType, Pika.PDBStatusType.SUCCESS)
])
def load_ora(procedure, run_mode, file, metadata, flags, config, data):
tempdir = tempfile.mkdtemp('pika-plugin-file-openraster')
orafile = zipfile.ZipFile(file.peek_path())
stack, w, h = get_image_attributes(orafile)
Pika.progress_init("Loading openraster image")
img = Pika.Image.new(w, h, Pika.ImageBaseType.RGB)
def get_layers(root):
"""iterates over layers and nested stacks"""
for item in root:
if item.tag == 'layer':
yield item
elif item.tag == 'stack':
yield item
for subitem in get_layers(item):
yield subitem
yield NESTED_STACK_END
parent_groups = []
# Number of top level layers for tracking progress
lay_cnt = len(stack)
layer_no = 0
for item in get_layers(stack):
prev_lay = layer_no
if item is NESTED_STACK_END:
parent_groups.pop()
continue
if item.tag == 'stack':
name, x, y, opac, visible, layer_mode = get_group_layer_attributes(item)
pika_layer = Pika.Layer.group_new(img)
else:
path, name, x, y, opac, visible, layer_mode = get_layer_attributes(item)
if not path.lower().endswith('.png'):
continue
if not name:
# use the filename without extension as name
n = os.path.basename(path)
name = os.path.splitext(n)[0]
# create temp file. Needed because pika cannot load files from inside a zip file
tmp = os.path.join(tempdir, 'tmp.png')
with open(tmp, 'wb') as fid:
try:
data = orafile.read(path)
except KeyError:
# support for bad zip files (saved by old versions of this plugin)
data = orafile.read(path.encode('utf-8'))
print('WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(path))
fid.write(data)
# import layer, set attributes and add to image
pdb_proc = Pika.get_pdb().lookup_procedure('pika-file-load-layer')
pdb_config = pdb_proc.create_config()
pdb_config.set_property('run-mode', Pika.RunMode.NONINTERACTIVE)
pdb_config.set_property('image', img)
pdb_config.set_property('file', Gio.File.new_for_path(tmp))
result = pdb_proc.run(pdb_config)
if (result.index(0) == Pika.PDBStatusType.SUCCESS):
pika_layer = result.index(1)
os.remove(tmp)
else:
print("Error loading layer from openraster image.")
pika_layer.set_name(name)
pika_layer.set_mode(layer_mode)
pika_layer.set_offsets(x, y) # move to correct position
pika_layer.set_opacity(opac * 100) # a float between 0 and 100
pika_layer.set_visible(visible)
img.insert_layer(pika_layer,
parent_groups[-1][0] if parent_groups else None,
parent_groups[-1][1] if parent_groups else layer_no)
if parent_groups:
parent_groups[-1][1] += 1
else:
layer_no += 1
if pika_layer.is_group():
parent_groups.append([pika_layer, 0])
if (layer_no > prev_lay):
Pika.progress_update(layer_no/lay_cnt)
Pika.progress_end()
os.rmdir(tempdir)
return Pika.ValueArray.new_from_values([
GObject.Value(Pika.PDBStatusType, Pika.PDBStatusType.SUCCESS),
GObject.Value(Pika.Image, img),
]), flags
class FileOpenRaster (Pika.PlugIn):
## PikaPlugIn virtual methods ##
def do_set_i18n(self, procname):
return True, 'pika30-python', None
def do_query_procedures(self):
return [ 'file-openraster-load-thumb',
'file-openraster-load',
'file-openraster-save' ]
def do_create_procedure(self, name):
if name == 'file-openraster-save':
procedure = Pika.SaveProcedure.new(self, name,
Pika.PDBProcType.PLUGIN,
False, save_ora, None)
procedure.set_image_types("*");
procedure.set_documentation ('save an OpenRaster (.ora) file',
'save an OpenRaster (.ora) file',
name)
procedure.set_menu_label('OpenRaster')
procedure.set_extensions ("ora");
elif name == 'file-openraster-load':
procedure = Pika.LoadProcedure.new (self, name,
Pika.PDBProcType.PLUGIN,
load_ora, None)
procedure.set_menu_label('OpenRaster')
procedure.set_documentation ('load an OpenRaster (.ora) file',
'load an OpenRaster (.ora) file',
name)
procedure.set_mime_types ("image/openraster");
procedure.set_extensions ("ora");
procedure.set_thumbnail_loader ('file-openraster-load-thumb');
else: # 'file-openraster-load-thumb'
procedure = Pika.ThumbnailProcedure.new (self, name,
Pika.PDBProcType.PLUGIN,
thumbnail_ora, None)
procedure.set_documentation ('loads a thumbnail from an OpenRaster (.ora) file',
'loads a thumbnail from an OpenRaster (.ora) file',
name)
procedure.set_attribution('Jon Nordby', #author
'Jon Nordby', #copyright
'2009') #year
return procedure
Pika.main(FileOpenRaster.__gtype__, sys.argv)