diff --git a/sketch_map_tool/helpers.py b/sketch_map_tool/helpers.py index 74bb8185..d2925824 100644 --- a/sketch_map_tool/helpers.py +++ b/sketch_map_tool/helpers.py @@ -1,8 +1,10 @@ +from io import BytesIO from pathlib import Path import cv2 import numpy as np from numpy.typing import NDArray +from PIL import Image as PILImage from reportlab.graphics.shapes import Drawing @@ -27,3 +29,18 @@ def resize_rlg_by_height(d: Drawing, size: float) -> Drawing: def to_array(buffer: bytes) -> NDArray: return cv2.imdecode(np.fromstring(buffer, dtype="uint8"), cv2.IMREAD_UNCHANGED) + + +def resize_png(input_buffer: BytesIO, max_length: float) -> BytesIO: + input_img = PILImage.open(input_buffer) + ratio = input_img.width / input_img.height + if ratio > 1: + width = min(max_length, input_img.width) + height = width / ratio + else: + height = min(max_length, input_img.height) + width = height * ratio + output_image = BytesIO() + input_img.resize((int(width), int(height))).save(output_image, format="png") + output_image.seek(0) + return output_image diff --git a/sketch_map_tool/map_generation/generate_pdf.py b/sketch_map_tool/map_generation/generate_pdf.py index a9b1ff64..6330d9e0 100644 --- a/sketch_map_tool/map_generation/generate_pdf.py +++ b/sketch_map_tool/map_generation/generate_pdf.py @@ -18,7 +18,7 @@ from svglib.svglib import svg2rlg from sketch_map_tool.definitions import PDF_RESOURCES_PATH -from sketch_map_tool.helpers import resize_rlg_by_width +from sketch_map_tool.helpers import resize_png, resize_rlg_by_width from sketch_map_tool.models import PaperFormat # PIL should be able to open high resolution PNGs of large Maps: @@ -26,7 +26,7 @@ def generate_pdf( # noqa: C901 - map_image_input: PILImage, + map_frame_input: PILImage, qr_code: Drawing, format_: PaperFormat, scale: float, @@ -38,13 +38,13 @@ def generate_pdf( # noqa: C901 Also generate template image (PNG) as Pillow object for later upload processing - :param map_image_input: Image of the map to be used as sketch map. + :param map_frame_input: Image of the map to be used as sketch map. :param qr_code: QR code to be included on the sketch map for georeferencing. :param format_: Paper format of the PDF document. :param scale: Ratio for the scale in the sketch map legend :return: Sketch Map PDF, Template image """ - map_width_px, map_height_px = map_image_input.size + map_width_px, map_height_px = map_frame_input.size map_margin = format_.map_margin # TODO: Use orientation parameter to determine rotation @@ -68,14 +68,14 @@ def generate_pdf( # noqa: C901 column_origin_y = 0 column_margin = map_margin * cm - map_image_reportlab = PIL_image_to_image_reader(map_image_input) + map_frame_reportlab = PIL_image_to_image_reader(map_frame_input) # calculate m per px in map frame cm_per_px = frame_width * scale / map_width_px m_per_px = cm_per_px / 100 # create map_image by adding globes - map_img = create_map_frame( - map_image_reportlab, format_, map_height_px, map_width_px, portrait, m_per_px + map_frame = create_map_frame( + map_frame_reportlab, format_, map_height_px, map_width_px, portrait, m_per_px ) map_pdf = BytesIO() @@ -85,7 +85,7 @@ def generate_pdf( # noqa: C901 # Add map to canvas: canv_map_margin = map_margin canv_map.drawImage( - ImageReader(map_img), + ImageReader(map_frame), canv_map_margin * cm, canv_map_margin * cm, mask="auto", @@ -123,16 +123,13 @@ def generate_pdf( # noqa: C901 canv_map.save() map_pdf.seek(0) - map_img.seek(0) + map_frame.seek(0) - if portrait: # Rotate the map frame for correct georeferencing - map_img_rotated_bytes = BytesIO() - map_img_rotated = PILImage.open(map_img).rotate(270, expand=1) - map_img_rotated.save(map_img_rotated_bytes, format="png") - map_img_rotated_bytes.seek(0) - map_img = map_img_rotated_bytes + # Resize map_frame if a length of 2000 px is exceeded in one dimension to avoid very large objects in upload + # processing of e.g. A0 maps + map_frame = resize_png(map_frame, max_length=2000) - return map_pdf, map_img + return map_pdf, map_frame def draw_right_column( diff --git a/sketch_map_tool/tasks.py b/sketch_map_tool/tasks.py index 180f9d16..101b76a2 100644 --- a/sketch_map_tool/tasks.py +++ b/sketch_map_tool/tasks.py @@ -54,19 +54,19 @@ def generate_sketch_map( ) -> BytesIO | AsyncResult: """Generate and returns a sketch map as PDF and stores the map frame in DB.""" raw = wms_client.get_map_image(bbox, size) - map_image = wms_client.as_image(raw) + map_frame = wms_client.as_image(raw) qr_code_ = map_generation.qr_code( str(uuid), bbox, format_, ) - map_pdf, map_img = map_generation.generate_pdf( - map_image, + map_pdf, map_frame = map_generation.generate_pdf( + map_frame, qr_code_, format_, scale, ) - db_client_celery.insert_map_frame(map_img, uuid) + db_client_celery.insert_map_frame(map_frame, uuid) return map_pdf diff --git a/tests/fixtures/map-img-big-landscape.jpg b/tests/fixtures/map-img-big-landscape.jpg new file mode 100644 index 00000000..2caefe2c Binary files /dev/null and b/tests/fixtures/map-img-big-landscape.jpg differ diff --git a/tests/fixtures/map-img-big-portrait.jpg b/tests/fixtures/map-img-big-portrait.jpg new file mode 100644 index 00000000..cdad0df2 Binary files /dev/null and b/tests/fixtures/map-img-big-portrait.jpg differ diff --git a/tests/unit/test_helpers.py b/tests/unit/test_helpers.py index f0beb73d..bea4e40c 100644 --- a/tests/unit/test_helpers.py +++ b/tests/unit/test_helpers.py @@ -1,5 +1,9 @@ +from io import BytesIO from pathlib import Path +import pytest +from PIL import Image + from sketch_map_tool import helpers @@ -7,3 +11,41 @@ def test_get_project_root(): expected = Path(__file__).resolve().parent.parent.parent.resolve() result = helpers.get_project_root() assert expected == result + + +@pytest.mark.parametrize( + ("max_length", "width_exp", "height_exp"), + [ + (1450, 1450, 1232), # One dimension too big + (1000, 1000, 850), # Both dimensions too big + (1456, 1456, 1238), # Not too big, exactly limit + (1600, 1456, 1238), # Not too big, smaller than limit + ], +) +def test_resize_png(map_frame_buffer, max_length, width_exp, height_exp): + img = Image.open(map_frame_buffer) + img_rotated = img.rotate(90, expand=1) + width, height = img.width, img.height + width_rotated, height_rotated = img_rotated.width, img_rotated.height + assert width == 1456 and height == 1238 + assert width_rotated == 1238 and height_rotated == 1456 + + img_rotated_buffer = BytesIO() + img_rotated.save(img_rotated_buffer, format="png") + img_rotated_buffer.seek(0) + + img_resized_buffer = helpers.resize_png(map_frame_buffer, max_length=max_length) + img_rotated_resized_buffer = helpers.resize_png( + img_rotated_buffer, max_length=max_length + ) + img_resized = Image.open(img_resized_buffer) + img_rotated_resized = Image.open(img_rotated_resized_buffer) + + width_resized, height_resized = img_resized.width, img_resized.height + width_rotated_resized, height_rotated_resized = ( + img_rotated_resized.width, + img_rotated_resized.height, + ) + + assert width_resized == width_exp and height_resized == height_exp + assert width_rotated_resized == height_exp and height_rotated_resized == width_exp diff --git a/tests/unit/test_map_generation.py b/tests/unit/test_map_generation.py index cb664c2b..0592904e 100644 --- a/tests/unit/test_map_generation.py +++ b/tests/unit/test_map_generation.py @@ -55,6 +55,14 @@ def map_image(request): return Image.open(p) +@pytest.fixture +def map_image_too_big(request): + """Map image from WMS exceeding the limit on map frame size""" + orientation = request.getfixturevalue("orientation") + p = FIXTURE_DIR / "map-img-big-{}.jpg".format(orientation) + return Image.open(p) + + @pytest.fixture def qr_code(bbox, format_, size): return generate_qr_code( @@ -109,6 +117,25 @@ def test_generate_pdf( # ) +@pytest.mark.parametrize("orientation", ["landscape", "portrait"]) +def test_generate_pdf_map_frame_too_big( + map_image_too_big, + qr_code, + orientation, +) -> None: + sketch_map, sketch_map_template = generate_pdf( + map_image_too_big, + qr_code, + A0, + 1283.129, + ) + + map_frame_img = Image.open(sketch_map_template) + + # Check that the upper limit on map frame size is enforced + assert map_frame_img.width <= 2000 and map_frame_img.height <= 2000 + + def test_get_globes(format_): globes = get_globes(format_.globe_scale) for globe in globes: