-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support reading from stdin, writing to stdout in HEIC2PNG #5
base: main
Are you sure you want to change the base?
Changes from all commits
fc6278e
f96499c
95f0439
a6e9d3d
f1f3fde
fd90fb4
8d5ff94
29d60b4
8076697
4eff59e
6df1c17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
__version__ = "1.1.4" | ||
|
||
from .heic2png import * | ||
from .cli import main | ||
#from .heic2png import * | ||
#from .cli import main |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,67 @@ | ||
import os | ||
import sys | ||
import subprocess | ||
from pathlib import Path | ||
from typing import Optional | ||
|
||
from PIL import Image | ||
from pillow_heif import register_heif_opener | ||
|
||
register_heif_opener() | ||
|
||
class HEIC2PNG: | ||
def __init__(self, image_file_path: str, quality: Optional[int] = None, overwrite: bool = False): | ||
def __init__(self, image_file_path: Optional[str] = "", | ||
quality: Optional[int] = None, | ||
overwrite: bool = False): | ||
""" | ||
Initializes the HEIC2PNG converter. | ||
|
||
:param image_file_path: Path to the HEIC image file. | ||
:param quality: Quality of the converted PNG image (1-100). | ||
:param overwrite: Whether to overwrite the file if it already exists. | ||
""" | ||
self.image_file_path: Path = Path(image_file_path) | ||
if not image_file_path: | ||
stdin = (os.fdopen(sys.stdin.fileno(), "rb") | ||
if "b" not in sys.stdin.mode else sys.stdin) | ||
self.image_file_path = stdin | ||
# TODO: I'm not a Python type hints maven. The image_file_path type | ||
# needs to reflect that an open file object is also acceptable. | ||
elif hasattr("fileno", image_file_path): | ||
pass # already open file | ||
else: | ||
self.image_file_path: Path = Path(image_file_path) | ||
if self.image_file_path.suffix.lower() != '.heic': | ||
raise ValueError("The provided file is not a HEIC image.") | ||
self.quality: Optional[int] = quality | ||
self.overwrite: bool = overwrite | ||
|
||
if not self.image_file_path.is_file(): | ||
raise FileNotFoundError(f"The file {image_file_path} does not exist.") | ||
|
||
if self.image_file_path.suffix.lower() != '.heic': | ||
raise ValueError("The provided file is not a HEIC image.") | ||
|
||
self.image: Image.Image = Image.open(self.image_file_path) | ||
|
||
def save(self, output_image_file_path: Optional[str] = None, extension: str = '.png') -> Path: | ||
def save(self, output_image_file_path: Optional[str] = "", | ||
extension: str = '.png') -> Path: | ||
""" | ||
Converts and saves the HEIC image to PNG format. | ||
Converts and saves the input image to another format. | ||
|
||
The default output format is PNG. | ||
|
||
:param output_image_file_path: Path to save the converted PNG image. | ||
:param extension: The file extension of the converted image. | ||
:return: Path where the converted image is saved. | ||
""" | ||
if output_image_file_path: | ||
output_path: Path = Path(output_image_file_path) | ||
if output_path.suffix.lower() != extension: | ||
raise ValueError("The output file extension does not match the specified extension.") | ||
if not output_image_file_path: | ||
stdout = (os.fdopen(sys.stdout.fileno(), "wb") | ||
if "b" not in sys.stdout.mode else sys.stdout) | ||
output_path = stdout | ||
else: | ||
output_path: Path = self.image_file_path.with_suffix(extension) | ||
|
||
if not self.overwrite and output_path.exists(): | ||
raise FileExistsError(f"The file {output_path} already exists.") | ||
output_path: Path = Path(output_image_file_path) | ||
|
||
self.image.save(output_path) | ||
# TODO: Not sure this is correct, there's probably a better mapping | ||
# between image file extensions and file formats. For example, see the | ||
# output of `python -m PIL`. | ||
self.image.save(output_path, format=extension.replace(".", "").upper()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could refer to this link to retrieve a list of supported formats with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tested it in Python 3.12, but I'm unsure if this function works across all versions of PIL. >>> from PIL import Image
>>> exts = Image.registered_extensions()
>>> supported_extensions = {ex for ex, f in exts.items() if f in Image.OPEN}
>>> supported_extensions
{'.wmf', '.icns', '.gif', '.grib', '.pnm', '.ftu', '.jpe', '.rgb', '.dib', '.tif', '.jfif', '.emf', '.jpc', '.pxr', '.ppm', '.dds', '.fit', '.icb', '.cur', '.pcx', '.bmp', '.h5', '.pfm', '.qoi', '.dcx', '.apng', '.iim', '.pbm', '.png', '.jpx', '.bw', '.psd', '.pcd', '.sgi', '.vda', '.im', '.mpeg', '.bufr', '.mpg', '.rgba', '.xpm', '.fli', '.fits', '.ps', '.flc', '.gbr', '.msp', '.j2c', '.jpg', '.tga', '.ico', '.xbm', '.pgm', '.j2k', '.jpf', '.jpeg', '.vst', '.hdf', '.ras', '.tiff', '.ftc', '.jp2', '.eps', '.webp', '.blp'} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be nice if there was a mapping from file extensions to formats. I think ultimately, that's what really matters. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe we can select a few from within and create a map like this: class HEIC2PNG:
FORMAT_MAPPING: Dict[str, str] = {
'.png': 'PNG',
'.jpg': 'JPEG',
'.jpeg': 'JPEG',
'.webp': 'WEBP',
'.bmp': 'BMP',
'.gif': 'GIF',
'.tiff': 'TIFF',
}
def __init__(...):
... We can make adjustments at this point. self.image.save(output_path, format=extension.replace(".", "").upper()) This line can be adjusted using the following approach: the variable The snippet appears as follows: ...
ext = extension.lower()
if ext not in self.FORMAT_MAPPING:
supported_formats = ', '.join(self.FORMAT_MAPPING.keys())
raise ValueError(f"Unsupported format: {extension}. Supported formats are: {supported_formats}")
image_format = self.FORMAT_MAPPING[ext]
self.image.save(output_path, format=image_format)
... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just a method I came up with on short notice. It may not be the best approach, but feel free to use it as a reference. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just thinking out loud here, but Python has a standard
Getting from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Unfortunately the HEIF opener doesn't register Still, this avenue might be worth pursuing. |
||
|
||
# TODO: Skip this step if the output format isn't PNG or pngquant can't | ||
# be found? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the other format specify parameters like quality or compression ratio? If not, we can skip this check. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some formats (like JPEG) support image quality. Generally speaking, this is only applicable to lossy formats. I wasn't aware that PNG could be lossy. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apologies if my wording wasn’t clear. PNG is a lossless format. |
||
# Optimize PNG with pngquant if quality is specified | ||
if self.quality and self.quality != 100: | ||
quality_str: str = f'{self.quality}-{self.quality}' | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
[tox] | ||
envlist = py{37,38,39} | ||
envlist = py{37,38,39,310,311,312,313} | ||
minversion = 3.3.0 | ||
isolated_build = true | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you clarify the rationale behind removing the required flag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the input comes from
sys.stdin
, there's not going to be an explicit input file, so requiring one doesn't make a ton of sense.I'm not going to hold it up as best practice, but I have another package on GitHub,
csvprogs
which goes the other way. I had always supported using stdin and stdout for IO, only supporting filled in a hit-or-miss fashion. It has grown organically though, so different tools were organized differently. In a fit of virtual housecleaning, I tackled this inconsistency. I was able to come up with a relatively clean way to support both styles of consuming input and generating output (see theopenio
function and its usage).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood. I believe it is necessary to add a check within the CLI function to verify whether
input_path
isNone
. Without this validation, the program might fail to locate the file and become unresponsive.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's how Unix-style filters work. This isn't quite how
heic2png
is organized, but many Unix/Linux commands will read from input files or stdin. Effectively, you have a CLI which looks like this:Both the input file and output file are optional (though if you want to specify an output file you must also specify an input file, hence the nesting brackets). Ignoring the fact that your current CLI explicitly calls for
-i
and/or-o
, I could run a more traditional Unix command like this:or
or even
If you look at the
openio
function in mycsvprogs
package, you'll see that's what it sorts out. If the input file has afileno
attribute, it assumes it's an open file, likesys.stdin
:The same goes for the output file. If not, it assumes it's a file which needs to be opened.
Clearly, if the user runs
without any files or I/O redirection, it will appear to hang. It's not really hung though, just waiting for input from stdin. Sending in a
KeyboardInterrupt
solves that problem, after which hopefully the user will think to run it with the-h
option. (I will tweak the help messages to make it clear what happens in the absence of the-i
or-o
options.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you very much for your explanation. For this revision, I have only tested it in a non-Unix environment like Windows. I will test it later on the Mac.