Created
October 7, 2024 09:13
-
-
Save ssokolow/7eeef34cd7143e052b30bc8a691b9445 to your computer and use it in GitHub Desktop.
Proof of Concept for OpenRaster-based "Use external editor" option
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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