Merge branch 'jpeg-orientation'

This commit is contained in:
Fabien LOISON 2021-06-29 16:07:36 +02:00
commit 5950b79e83
No known key found for this signature in database
GPG Key ID: FF90CA148348048E
14 changed files with 115 additions and 3 deletions

View File

@ -58,6 +58,7 @@ Changelog
* Python 2.7 support dropped
* Allow to cancel an optimization using Ctrl+C (NOTE: this may not work on Windows)
* Honor JPEG orientation EXIF tag
* **1.0.0:**

27
scripts/lsexif.py Executable file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env python
import sys
from PIL import Image
from PIL.ExifTags import TAGS
def print_exif(input_path):
image = Image.open(input_path)
exif = image.getexif()
print("+-- %s" % input_path)
for key, value in exif.items():
print(" +-- [%i] %s: %s" % (key, TAGS[key], value))
if __name__ == "__main__":
if len(sys.argv) < 2:
print("USAGE:")
print(
" ./scripts/lsexif.py <image.jpg> [image2.jpg ...]"
)
sys.exit(1)
for input_path in sys.argv[1:]:
print_exif(input_path)

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -0,0 +1,39 @@
import pytest
from yoga.image.encoders import jpeg
class Test_open_jpeg(object):
@pytest.mark.parametrize(
"image_path",
[
"test/images/orientation/no-metadata.jpg",
"test/images/orientation/1-rotation-0.jpg",
"test/images/orientation/2-flip-horizontal.jpg",
"test/images/orientation/3-rotation-180.jpg",
"test/images/orientation/4-flip-vertical.jpg",
"test/images/orientation/5-rotation-270-flip-horizontal.jpg",
"test/images/orientation/6-rotation-270.jpg",
"test/images/orientation/7-rotation-90-flip-horizontal.jpg",
"test/images/orientation/8-rotation-90.jpg",
],
)
def test_jpeg_orientation(self, image_path):
with open(image_path, "rb") as image_file:
image = jpeg.open_jpeg(image_file)
# Test image size
assert image.width == 256
assert image.height == 341
# Check if the red square is at top-left corner
r1, g1, b1 = image.getpixel((8, 8))
assert r1 > 250 and g1 < 5 and b1 < 5 # ~red
# Check if the green square is at top-right corner
r2, g2, b2 = image.getpixel((255 - 8, 8))
assert r2 < 5 and g2 > 250 and b2 < 5 # ~lime
# Check if the blue square is at bottom-right corner
r3, g3, b3 = image.getpixel((255 - 8, 340 - 8))
assert r3 < 5 and g3 < 5 and b3 > 250 # ~blue

View File

@ -169,6 +169,7 @@ API
from PIL import Image
from .encoders.jpeg import optimize_jpeg
from .encoders.jpeg import open_jpeg
from .encoders.png import optimize_png
from .encoders.webp import optimize_lossy_webp
from .encoders.webp_lossless import optimize_lossless_webp
@ -195,8 +196,19 @@ def optimize(input_file, output_file, options={}, verbose=False, quiet=False):
else:
raise ValueError("Unsupported parameter type for 'input_file'")
# Determine the input image format
try:
input_format = helpers.guess_image_format(image_file.read())
except ValueError:
input_format = None
finally:
image_file.seek(0) # to allow PIL.Image to read the file
# Open the image with Pillow
image = Image.open(image_file)
if input_format == "jpeg":
image = open_jpeg(image_file)
else:
image = Image.open(image_file)
# Resize image if requested
if options["resize"] != "orig":
@ -204,8 +216,9 @@ def optimize(input_file, output_file, options={}, verbose=False, quiet=False):
# Output format
if options["output_format"] == "orig":
image_file.seek(0) # PIL.Image already read the file
output_format = helpers.guess_image_format(image_file.read())
if input_format is None:
raise ValueError("Unsupported image format")
output_format = input_format
elif options["output_format"] == "auto":
if helpers.image_have_alpha(image, options["opacity_threshold"]):
output_format = "png"

View File

@ -1,4 +1,5 @@
import pyguetzli
from PIL import Image
def is_jpeg(file_bytes):
@ -32,3 +33,34 @@ def optimize_jpeg(image, quality):
if not 0.00 <= quality <= 1.00:
raise ValueError("JPEG quality value must be between 0.00 and 1.00")
return pyguetzli.process_pil_image(image, int(quality * 100))
def open_jpeg(image_file):
"""Open JPEG file.
This function also handles JPEG Orientation EXIF.
:param file-like image_file: the image file.
:rtype: PIL.Image
"""
EXIF_TAG_ORIENTATION = 274
ORIENTATION_OPERATIONS = {
1: [],
2: [Image.FLIP_LEFT_RIGHT],
3: [Image.ROTATE_180],
4: [Image.FLIP_TOP_BOTTOM],
5: [Image.FLIP_LEFT_RIGHT, Image.ROTATE_90],
6: [Image.ROTATE_270],
7: [Image.FLIP_LEFT_RIGHT, Image.ROTATE_270],
8: [Image.ROTATE_90],
}
image = Image.open(image_file)
exif = image.getexif()
if EXIF_TAG_ORIENTATION in exif:
orientation = exif[EXIF_TAG_ORIENTATION]
for operation in ORIENTATION_OPERATIONS[orientation]:
image = image.transpose(operation)
return image