Skip to content
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

Add module to check image size #151

Merged
merged 11 commits into from
Jan 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/usage/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ The package is installable with pip:
pip install qgis-deployment-toolbelt
```

It's then availabl as a CLI: see [the relevant section](/usage/cli)
It's then available as a CLI: see [the relevant section](/usage/cli)
143 changes: 143 additions & 0 deletions qgis_deployment_toolbelt/utils/check_image_size.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#! python3 # noqa: E265

"""
Check image size using pure Python <https://github.com/shibukawa/imagesize_py>.

Author: Julien Moura (https://github.com/guts)
"""

# #############################################################################
# ########## Libraries #############
# ##################################

# Standard library
import logging
import xml.etree.ElementTree as ET
from decimal import Decimal
from pathlib import Path
from typing import Tuple, Union

# 3rd party
import imagesize

# #############################################################################
# ########## Globals ###############
# ##################################

# logs
logger = logging.getLogger(__name__)


# #############################################################################
# ########## Functions #############
# ##################################


def get_image_size(image_filepath: Path) -> Tuple[int, int]:
"""Get image dimensions as a tuple (width,height). Return None in case of error.

:param Path image_filepath: path to the image

:return Tuple[int, int]: dimensions tuple (width,height)
"""
# handle SVG
if image_filepath.suffix.lower() == ".svg":
svg_size = get_svg_size(image_filepath)
if not svg_size:
return None
else:
return svg_size

# get image dimensions
try:
return imagesize.get(image_filepath)
except ValueError as exc:
logging.error(f"Invalid image: {image_filepath.resolve()}. Trace: {exc}")
except Exception as exc:
logging.error(
f"Something went wrong reading the image: {image_filepath.resolve()}. Trace: {exc}"
)

return None


def get_svg_size(image_filepath: Path) -> Tuple[int, int]:
"""Extract SVG width and height from a SVG file and convert them into integers. \
Relevant and working only if the file root has width and height attributes.

:param Path image_filepath: path to the svg file

:return Tuple[int, int]: tuple of dimensions as integers (width,height)
"""
try:
tree = ET.parse(image_filepath)
root = tree.getroot()
except Exception as err:
logger.error(f"Unable to open SVG file as XML: {image_filepath}. Trace: {err}")
return None

try:
return int(Decimal(root.attrib["width"])), int(Decimal(root.attrib["height"]))
except Exception as err:
logger.warning(
"Unable to determine image dimensions from width/height "
f"attributes: {image_filepath}. It migh be infinitely scalable. Trace: {err}"
)
return None


def check_image_dimensions(
image_filepath: Union[str, Path],
min_width: int = 500,
max_width: int = 600,
min_height: int = 250,
max_height: int = 350,
allowed_images_extensions: tuple = (".jpg", ".jpeg", ".png", ".svg"),
) -> bool:
"""Check input image dimensions against passed limits.

:param Union[str, Path] image_filepath: path to the image to check
:param int min_width: minimum width, defaults to 500
:param int max_width: maximum width, defaults to 600
:param int min_height: minimum height, defaults to 250
:param int max_height: maximum height, defaults to 350

:return bool: True if image dimensions are inferior
"""

if image_filepath.suffix.lower() not in allowed_images_extensions:
logger.error(
f"Image extension {image_filepath.suffix.lower()} is not one of "
f"supported: {allowed_images_extensions}"
)
return None

image_dimensions = get_image_size(image_filepath=image_filepath)
if not image_dimensions:
logger.info(
f"Unable to determine image dimensions ({image_filepath.resolve()}), "
"so unable to check it it complies with limits."
)
return None

return all(d <= l for d, l in zip(image_dimensions, (max_width, max_height)))


# #############################################################################
# ##### Stand alone program ########
# ##################################

if __name__ == "__main__":
"""Standalone execution."""
svg_path = Path(
"tests/fixtures/miscellaneous/sample_with_dimensions_attributes.svg"
)
assert svg_path.is_file()
print(get_svg_size(image_filepath=svg_path))
print(get_image_size(image_filepath=svg_path))

svg_path = Path(
"tests/fixtures/miscellaneous/sample_without_dimensions_attributes.svg"
)
assert svg_path.is_file()
print(get_svg_size(image_filepath=svg_path))
1 change: 1 addition & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
click>=8,<9
dulwich>=0.20,<0.20.51
giturlparse>=0.10,<0.11
imagesize>=1.4,<1.5
pyyaml>=5.4,<7
py-setenv>=1.1,<1.2
pywin32==305 ; sys_platform == 'win32'
Expand Down
1 change: 1 addition & 0 deletions requirements/testing.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Testing dependencies
# --------------------

pillow>=9.4,<10
pytest-cov>=3.0,<4.1
semver>=2.13,<2.14
validators>=0.19,<0.21
22 changes: 22 additions & 0 deletions tests/fixtures/miscellaneous/sample_with_dimensions_attributes.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
123 changes: 123 additions & 0 deletions tests/test_utils_images_size.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#! python3 # noqa E265

"""
Usage from the repo root folder:

.. code-block:: bash
# for whole tests
python -m unittest tests.test_utils_images_size
# for specific test
python -m unittest tests.test_utils_images_size.TestUtilsImagesSizeChecker.get_svg_size
"""


# standard library
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory

# 3rd party
from PIL import Image

# project
from qgis_deployment_toolbelt.__about__ import __title_clean__, __version__
from qgis_deployment_toolbelt.utils.check_image_size import (
check_image_dimensions,
get_image_size,
get_svg_size,
)

# ############################################################################
# ########## Classes #############
# ################################


class TestUtilsImagesSizeChecker(unittest.TestCase):
"""Test package utilities."""

# -- Standard methods --------------------------------------------------------
@classmethod
def setUpClass(cls) -> None:
"""Set up test."""

cls.img_tmp_folder = TemporaryDirectory(
prefix=f"{__title_clean__}_{__version__}_"
)

# create a temporary image
cls.img_black_800x600 = Path(cls.img_tmp_folder.name, "img_black_800x600.jpg")
width = 800
height = 600
image = Image.new("RGB", (width, height), "black")
image.save(cls.img_black_800x600)

@classmethod
def tearDownClass(cls):
"""Executed after each test."""
cls.img_tmp_folder.cleanup()

# -- TESTS ---------------------------------------------------------
def test_svg_size_with_dimensions(self):
"""Test svg size retriever."""
svg_with_dimensions_attributes = Path(
"tests/fixtures/miscellaneous/sample_with_dimensions_attributes.svg"
)

self.assertTrue(svg_with_dimensions_attributes.is_file())
# svg with dimensions set
svg_size = get_image_size(image_filepath=svg_with_dimensions_attributes)

self.assertIsInstance(svg_size, tuple)
self.assertIsInstance(svg_size[0], int)
self.assertIsInstance(svg_size[1], int)
self.assertEqual(svg_size[0], 853)
self.assertEqual(svg_size[1], 568)

def test_svg_size_without_dimensions(self):
"""Test svg size retriever."""
svg_without_dimensions_attributes = Path(
"tests/fixtures/miscellaneous/sample_without_dimensions_attributes.svg"
)
self.assertTrue(svg_without_dimensions_attributes.is_file())
svg_size = get_image_size(image_filepath=svg_without_dimensions_attributes)

self.assertIsNone(svg_size)

def test_get_image_dimensions(self):
img_800x600 = get_image_size(self.img_black_800x600)
self.assertIsInstance(img_800x600, tuple)
self.assertEqual(img_800x600[0], 800)
self.assertEqual(img_800x600[1], 600)

def test_check_image_dimensions(self):
"""Test image dimensions checker."""
self.assertTrue(
check_image_dimensions(
image_filepath=self.img_black_800x600, max_width=801, max_height=601
)
)

self.assertFalse(
check_image_dimensions(
image_filepath=self.img_black_800x600, max_width=300, max_height=601
)
)

self.assertFalse(
check_image_dimensions(
image_filepath=self.img_black_800x600, max_width=2000, max_height=200
)
)

self.assertFalse(
check_image_dimensions(
image_filepath=self.img_black_800x600, max_width=300, max_height=500
)
)


# ############################################################################
# ####### Stand-alone run ########
# ################################
if __name__ == "__main__":
unittest.main()