Merge branch 'jpeg-orientation'
|
@ -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:**
|
||||
|
||||
|
|
|
@ -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)
|
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 79 KiB |
After Width: | Height: | Size: 67 KiB |
After Width: | Height: | Size: 61 KiB |
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|