mirror of https://github.com/GNOME/gimp.git
393 lines
15 KiB
Python
Executable File
393 lines
15 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# GIMP 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('Gimp', '3.0')
|
|
from gi.repository import Gimp
|
|
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": Gimp.LayerMode.NORMAL,
|
|
"svg:multiply": Gimp.LayerMode.MULTIPLY_LEGACY,
|
|
"svg:screen": Gimp.LayerMode.SCREEN_LEGACY,
|
|
"svg:overlay": Gimp.LayerMode.OVERLAY,
|
|
"svg:darken": Gimp.LayerMode.DARKEN_ONLY_LEGACY,
|
|
"svg:lighten": Gimp.LayerMode.LIGHTEN_ONLY_LEGACY,
|
|
"svg:color-dodge": Gimp.LayerMode.DODGE_LEGACY,
|
|
"svg:color-burn": Gimp.LayerMode.BURN_LEGACY,
|
|
"svg:hard-light": Gimp.LayerMode.HARDLIGHT_LEGACY,
|
|
"svg:soft-light": Gimp.LayerMode.SOFTLIGHT_LEGACY,
|
|
"svg:difference": Gimp.LayerMode.DIFFERENCE_LEGACY,
|
|
"svg:color": Gimp.LayerMode.HSL_COLOR_LEGACY,
|
|
"svg:luminosity": Gimp.LayerMode.HSV_VALUE_LEGACY,
|
|
"svg:hue": Gimp.LayerMode.HSV_HUE_LEGACY,
|
|
"svg:saturation": Gimp.LayerMode.HSV_SATURATION_LEGACY,
|
|
"svg:plus": Gimp.LayerMode.ADDITION_LEGACY,
|
|
}
|
|
|
|
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, Gimp.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, NORMAL_MODE)
|
|
|
|
return name, 0, 0, opac, visible, layer_mode
|
|
|
|
def thumbnail_ora(procedure, file, thumb_size, args, data):
|
|
tempdir = tempfile.mkdtemp('gimp-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'))
|
|
|
|
img = Gimp.get_pdb().run_procedure('file-png-load', [
|
|
GObject.Value(Gimp.RunMode, Gimp.RunMode.NONINTERACTIVE),
|
|
GObject.Value(GObject.TYPE_STRING, tmp),
|
|
])
|
|
img = img.index(1)
|
|
# TODO: scaling
|
|
os.remove(tmp)
|
|
os.rmdir(tempdir)
|
|
|
|
return Gimp.ValueArray.new_from_values([
|
|
GObject.Value(Gimp.PDBStatusType, Gimp.PDBStatusType.SUCCESS),
|
|
GObject.Value(Gimp.Image, img),
|
|
GObject.Value(GObject.TYPE_INT, w),
|
|
GObject.Value(GObject.TYPE_INT, h),
|
|
GObject.Value(Gimp.ImageType, Gimp.ImageType.RGB_IMAGE),
|
|
GObject.Value(GObject.TYPE_INT, 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, args, 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)
|
|
|
|
tempdir = tempfile.mkdtemp('gimp-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.width())
|
|
a['h'] = str(image.height())
|
|
|
|
def store_layer(image, drawable, path):
|
|
tmp = os.path.join(tempdir, 'tmp.png')
|
|
interlace, compression = 0, 2
|
|
|
|
Gimp.get_pdb().run_procedure('file-png-save', [
|
|
GObject.Value(Gimp.RunMode, Gimp.RunMode.NONINTERACTIVE),
|
|
GObject.Value(Gimp.Image, image),
|
|
GObject.Value(GObject.TYPE_INT, 1),
|
|
GObject.Value(Gimp.ObjectArray, Gimp.ObjectArray.new(Gimp.Drawable, [drawable], 1)),
|
|
GObject.Value(Gio.File, Gio.File.new_for_path(tmp)),
|
|
GObject.Value(GObject.TYPE_BOOLEAN, interlace),
|
|
GObject.Value(GObject.TYPE_INT, compression),
|
|
# write all PNG chunks except oFFs(ets)
|
|
GObject.Value(GObject.TYPE_BOOLEAN, True),
|
|
GObject.Value(GObject.TYPE_BOOLEAN, True),
|
|
GObject.Value(GObject.TYPE_BOOLEAN, False),
|
|
GObject.Value(GObject.TYPE_BOOLEAN, True),
|
|
])
|
|
if (os.path.exists(tmp)):
|
|
orafile.write(tmp, path)
|
|
os.remove(tmp)
|
|
else:
|
|
print("Error removing ", tmp)
|
|
|
|
def add_layer(parent, x, y, opac, gimp_layer, path, visible=True):
|
|
store_layer(image, gimp_layer, path)
|
|
# create layer attributes
|
|
layer = ET.Element('layer')
|
|
parent.append(layer)
|
|
a = layer.attrib
|
|
a['src'] = path
|
|
a['name'] = gimp_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'] = reverse_map(layermodes_map).get(gimp_layer.get_mode(), 'svg:src-over')
|
|
return layer
|
|
|
|
def add_group_layer(parent, opac, gimp_layer, visible=True):
|
|
# create layer attributes
|
|
group_layer = ET.Element('stack')
|
|
parent.append(group_layer)
|
|
a = group_layer.attrib
|
|
a['name'] = gimp_layer.name
|
|
a['opacity'] = str(opac)
|
|
a['visibility'] = 'visible' if visible else 'hidden'
|
|
a['composite-op'] = reverse_map(layermodes_map).get(gimp_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.get_children()):
|
|
yield sublayer
|
|
yield NESTED_STACK_END
|
|
|
|
# save layers
|
|
parent_groups = []
|
|
i = 0
|
|
for lay in enumerate_layers(image.get_layers()):
|
|
if lay is NESTED_STACK_END:
|
|
parent_groups.pop()
|
|
continue
|
|
_, x, y = lay.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())
|
|
|
|
# save mergedimage
|
|
thumb = image.duplicate()
|
|
thumb_layer = thumb.merge_visible_layers (Gimp.MergeType.CLIP_TO_IMAGE)
|
|
store_layer (thumb, thumb_layer, 'mergedimage.png')
|
|
|
|
# save thumbnail
|
|
w, h = image.width(), image.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() != Gimp.Precision.U8_GAMMA:
|
|
thumb.convert_precision (Gimp.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())
|
|
|
|
return Gimp.ValueArray.new_from_values([
|
|
GObject.Value(Gimp.PDBStatusType, Gimp.PDBStatusType.SUCCESS)
|
|
])
|
|
|
|
def load_ora(procedure, run_mode, file, args, data):
|
|
tempdir = tempfile.mkdtemp('gimp-plugin-file-openraster')
|
|
orafile = zipfile.ZipFile(file.peek_path())
|
|
stack, w, h = get_image_attributes(orafile)
|
|
|
|
img = Gimp.Image.new(w, h, Gimp.ImageBaseType.RGB)
|
|
img.set_file (file)
|
|
|
|
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 = []
|
|
|
|
layer_no = 0
|
|
for item in get_layers(stack):
|
|
|
|
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)
|
|
gimp_layer = img.layer_group_new()
|
|
|
|
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 gimp 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
|
|
gimp_layer = Gimp.get_pdb().run_procedure('gimp-file-load-layer', [
|
|
GObject.Value(Gimp.RunMode, Gimp.RunMode.NONINTERACTIVE),
|
|
GObject.Value(Gimp.Image, img),
|
|
GObject.Value(Gio.File, Gio.File.new_for_path(tmp)),
|
|
])
|
|
gimp_layer = gimp_layer.index(1)
|
|
os.remove(tmp)
|
|
gimp_layer.set_name(name)
|
|
gimp_layer.set_mode(layer_mode)
|
|
gimp_layer.set_offsets(x, y) # move to correct position
|
|
gimp_layer.set_opacity(opac * 100) # a float between 0 and 100
|
|
gimp_layer.set_visible(visible)
|
|
|
|
img.insert_layer(gimp_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 gimp_layer.is_group():
|
|
parent_groups.append([gimp_layer, 0])
|
|
|
|
os.rmdir(tempdir)
|
|
|
|
return Gimp.ValueArray.new_from_values([
|
|
GObject.Value(Gimp.PDBStatusType, Gimp.PDBStatusType.SUCCESS),
|
|
GObject.Value(Gimp.Image, img),
|
|
])
|
|
|
|
|
|
class FileOpenRaster (Gimp.PlugIn):
|
|
## GimpPlugIn virtual methods ##
|
|
def do_query_procedures(self):
|
|
self.set_translation_domain("gimp30-python",
|
|
Gio.file_new_for_path(Gimp.locale_directory()))
|
|
|
|
return [ 'file-openraster-load-thumb',
|
|
'file-openraster-load',
|
|
'file-openraster-save' ]
|
|
|
|
def do_create_procedure(self, name):
|
|
if name == 'file-openraster-save':
|
|
procedure = Gimp.SaveProcedure.new(self, name,
|
|
Gimp.PDBProcType.PLUGIN,
|
|
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 = Gimp.LoadProcedure.new (self, name,
|
|
Gimp.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 = Gimp.ThumbnailProcedure.new (self, name,
|
|
Gimp.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)
|
|
pass
|
|
procedure.set_attribution('Jon Nordby', #author
|
|
'Jon Nordby', #copyright
|
|
'2009') #year
|
|
return procedure
|
|
|
|
Gimp.main(FileOpenRaster.__gtype__, sys.argv)
|