Skip to content

Instantly share code, notes, and snippets.

@ssokolow
Created October 7, 2024 09:13
Show Gist options
  • Save ssokolow/7eeef34cd7143e052b30bc8a691b9445 to your computer and use it in GitHub Desktop.
Save ssokolow/7eeef34cd7143e052b30bc8a691b9445 to your computer and use it in GitHub Desktop.
Proof of Concept for OpenRaster-based "Use external editor" option
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Proof of concept code for generating and opening an OpenRaster image in the
user's preferred OpenRaster editor.
"""
__author__ = "Stephan Sokolow (deitarion/SSokolow)"
__appname__ = "OpenRaster Proof of Concept"
__version__ = "0.1"
__license__ = "MIT"
import logging
import os
import subprocess
import sys
import tempfile
import zipfile
import xml.etree.ElementTree as ET
from PIL import Image
log = logging.getLogger(__name__)
def to_ora(img2img_src, mask=None, out_path=None):
"""Combine the given file and, optionally, a pre-existing mask into an
OpenRaster file suitable for a "use external editor" option.
"""
outdir = os.path.dirname(out_path) if out_path else None
img = Image.open(img2img_src)
try:
(ora_fd, ora_path) = tempfile.mkstemp(dir=outdir,
prefix='ext_ed-', suffix='.ora')
with open(ora_fd, 'wb') as fh:
with zipfile.ZipFile(fh,
mode='w',
# ORA supports STORE or DEFLATE but, since we're using this
# to exchange data between two apps on the same machine
# through a transient file, don't waste the CPU time doing
# DEFLATE compression beyond what's already internal to the
# layer files we're embedding.
compression=zipfile.ZIP_STORED) as ora_zh:
# Required pseudo-"magic number". Must always be STOREd
ora_zh.writestr('mimetype', 'image/openraster',
zipfile.ZIP_STORED)
# img2img source
#
# Write it as mergedimage.png to save space and time. The spec
# says it's just convention that the layers are under /data
# and mergedimage.png is mandatory, so let's just show the
# img2img layer.
with ora_zh.open('mergedimage.png', 'w') as fh:
img.save(fh, format='png')
# Mask
# (Empty for proof of concept, but would be
# persisted in practice)
with ora_zh.open('data/mask.png', 'w') as fh:
if mask:
Image.open(mask).save(fh, format='png')
else:
Image.new('1', img.size).save(fh, format='png')
# Layer stack definition
with ora_zh.open('stack.xml', 'w') as fh:
image_node = ET.Element('image')
image_node.set('version', '0.0.3')
image_node.set('w', str(img.size[0]))
image_node.set('h', str(img.size[1]))
stack_node = ET.SubElement(image_node, 'stack')
ET.SubElement(stack_node, 'layer', name='Mask',
src='data/mask.png', opacity=str(0.5), selected="true")
ET.SubElement(stack_node, 'layer', name='Base Image',
src='mergedimage.png')
stack_xml = ET.ElementTree(image_node)
stack_xml.write(fh, encoding='UTF-8', xml_declaration=True)
# We create a technically-usable thumbnail because *some* kind
# of thumbnail is required by the ORA spec and we don't want to
# risk confusing the user if it displays the cached thumbnail
# somewhere.
#
# Must be written after stack.xml since it modifies the image
# in-place before the dimensions are read.
#
# TODO: Check if it's possible to request to prefer reuse of
# any embedded thumbnail in the source image.
with ora_zh.open('Thumbnails/thumbnail.png', 'w') as fh:
img.thumbnail((128, 128), resample=0)
img.save(fh, 'png')
except BaseException:
os.remove(ora_path)
else:
if out_path:
os.rename(ora_path, out_path)
return out_path
else:
return ora_path
# TODO: from_ora
def process_arg(path):
ora_path = to_ora(path)
print("Generated .ora file at ", ora_path)
if os.name == 'nt':
os.startfile(ora_path.replace('/', '\\'))
elif sys.platform == 'darwin':
subprocess.call(('open', ora_path))
else:
subprocess.call(('xdg-open', ora_path))
def main():
"""The main entry point, compatible with setuptools entry points."""
from argparse import ArgumentParser, RawDescriptionHelpFormatter
parser = ArgumentParser(formatter_class=RawDescriptionHelpFormatter,
description=__doc__.replace('\r\n', '\n').split('\n--snip--\n')[0])
parser.add_argument('-V', '--version', action='version',
version="%%(prog)s v%s" % __version__)
parser.add_argument('-v', '--verbose', action="count",
default=2, help="Increase the verbosity. Use twice for extra effect.")
parser.add_argument('-q', '--quiet', action="count",
default=0, help="Decrease the verbosity. Use twice for extra effect.")
parser.add_argument('path', action="store", nargs="+",
help="Path to operate on")
# Reminder: %(default)s can be used in help strings.
args = parser.parse_args()
# Set up clean logging to stderr
log_levels = [logging.CRITICAL, logging.ERROR, logging.WARNING,
logging.INFO, logging.DEBUG]
args.verbose = min(args.verbose - args.quiet, len(log_levels) - 1)
args.verbose = max(args.verbose, 0)
logging.basicConfig(level=log_levels[args.verbose],
format='%(levelname)s: %(message)s')
for path in args.path:
process_arg(path)
if __name__ == '__main__': # pragma: nocover
main()
# vim: set sw=4 sts=4 expandtab :
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment