* add __all__ to files "adjustmentsinfo.py" and "albuminfo.py" * add __all__ to file "cli.py" * add __all__ to all files that misses except files with prefix "_"
128 lines
4.9 KiB
Python
128 lines
4.9 KiB
Python
""" ImageConverter class
|
|
Convert an image to JPEG using CoreImage --
|
|
for example, RAW to JPEG. Only works if Mac equipped with GPU. """
|
|
|
|
# reference: https://stackoverflow.com/questions/59330149/coreimage-ciimage-write-jpg-is-shifting-colors-macos/59334308#59334308
|
|
|
|
import pathlib
|
|
|
|
import objc
|
|
import Metal
|
|
import Quartz
|
|
from Cocoa import NSURL
|
|
from Foundation import NSDictionary
|
|
|
|
# needed to capture system-level stderr
|
|
from wurlitzer import pipes
|
|
|
|
__all__ = ["ImageConversionError", "ImageConverter"]
|
|
|
|
|
|
class ImageConversionError(Exception):
|
|
"""Base class for exceptions in this module."""
|
|
|
|
pass
|
|
|
|
|
|
class ImageConverter:
|
|
"""Convert images to jpeg. This class is a singleton
|
|
which will re-use the Core Image CIContext to avoid
|
|
creating a new context for every conversion."""
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
"""create new object or return instance of already created singleton"""
|
|
if not hasattr(cls, "instance") or not cls.instance:
|
|
cls.instance = super().__new__(cls)
|
|
|
|
return cls.instance
|
|
|
|
def __init__(self):
|
|
"""return existing singleton or create a new one"""
|
|
|
|
if hasattr(self, "context"):
|
|
return
|
|
|
|
""" initialize CIContext """
|
|
context_options = NSDictionary.dictionaryWithDictionary_(
|
|
{
|
|
"workingColorSpace": Quartz.CoreGraphics.kCGColorSpaceExtendedSRGB,
|
|
"workingFormat": Quartz.kCIFormatRGBAh,
|
|
}
|
|
)
|
|
self.context = Quartz.CIContext.contextWithOptions_(context_options)
|
|
|
|
def write_jpeg(self, input_path, output_path, compression_quality=1.0):
|
|
"""convert image to jpeg and write image to output_path
|
|
|
|
Args:
|
|
input_path: path to input image (e.g. '/path/to/import/file.CR2') as str or pathlib.Path
|
|
output_path: path to exported jpeg (e.g. '/path/to/export/file.jpeg') as str or pathlib.Path
|
|
compression_quality: JPEG compression quality, float in range 0.0 to 1.0; default is 1.0 (best quality)
|
|
|
|
Return:
|
|
True if conversion successful, else False
|
|
|
|
Raises:
|
|
ValueError if compression quality not in range 0.0 to 1.0
|
|
FileNotFoundError if input_path doesn't exist
|
|
ImageConversionError if error during conversion
|
|
"""
|
|
|
|
# Set up a dedicated objc autorelease pool for this function call.
|
|
# This is to ensure that all the NSObjects are cleaned up after each
|
|
# call to prevent memory leaks.
|
|
# https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html
|
|
# https://pyobjc.readthedocs.io/en/latest/api/module-objc.html#memory-management
|
|
with objc.autorelease_pool():
|
|
# accept input_path or output_path as pathlib.Path
|
|
if not isinstance(input_path, str):
|
|
input_path = str(input_path)
|
|
|
|
if not isinstance(output_path, str):
|
|
output_path = str(output_path)
|
|
|
|
if not pathlib.Path(input_path).is_file():
|
|
raise FileNotFoundError(f"could not find {input_path}")
|
|
|
|
if not (0.0 <= compression_quality <= 1.0):
|
|
raise ValueError(
|
|
"illegal value for compression_quality: {compression_quality}"
|
|
)
|
|
|
|
input_url = NSURL.fileURLWithPath_(input_path)
|
|
output_url = NSURL.fileURLWithPath_(output_path)
|
|
|
|
with pipes() as (out, err):
|
|
# capture stdout and stderr from system calls
|
|
# otherwise, Quartz.CIImage.imageWithContentsOfURL_
|
|
# prints to stderr something like:
|
|
# 2020-09-20 20:55:25.538 python[73042:5650492] Creating client/daemon connection: B8FE995E-3F27-47F4-9FA8-559C615FD774
|
|
# 2020-09-20 20:55:25.652 python[73042:5650492] Got the query meta data reply for: com.apple.MobileAsset.RawCamera.Camera, response: 0
|
|
input_image = Quartz.CIImage.imageWithContentsOfURL_(input_url)
|
|
|
|
if input_image is None:
|
|
raise ImageConversionError(f"Could not create CIImage for {input_path}")
|
|
|
|
output_colorspace = (
|
|
input_image.colorSpace()
|
|
or Quartz.CGColorSpaceCreateWithName(
|
|
Quartz.CoreGraphics.kCGColorSpaceSRGB
|
|
)
|
|
)
|
|
|
|
output_options = NSDictionary.dictionaryWithDictionary_(
|
|
{"kCGImageDestinationLossyCompressionQuality": compression_quality}
|
|
)
|
|
(
|
|
_,
|
|
error,
|
|
) = self.context.writeJPEGRepresentationOfImage_toURL_colorSpace_options_error_(
|
|
input_image, output_url, output_colorspace, output_options, None
|
|
)
|
|
if not error:
|
|
return True
|
|
else:
|
|
raise ImageConversionError(
|
|
f"Error converting file {input_path} to jpeg at {output_path}: {error}"
|
|
)
|