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

Map frame resize #215

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
17 changes: 17 additions & 0 deletions sketch_map_tool/helpers.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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
29 changes: 13 additions & 16 deletions sketch_map_tool/map_generation/generate_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,33 @@
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:
Image.MAX_IMAGE_PIXELS = None


def generate_pdf( # noqa: C901
map_image_input: PILImage,
map_frame_input: PILImage,
qr_code: Drawing,
format_: PaperFormat,
scale: float,
) -> Tuple[BytesIO, BytesIO]:
"""
Generate a sketch map pdf, i.e. a PDF containing the given map image as well as a code for

Check failure on line 35 in sketch_map_tool/map_generation/generate_pdf.py

View workflow job for this annotation

GitHub Actions / Python Linting

Ruff (E501)

sketch_map_tool/map_generation/generate_pdf.py:35:89: E501 Line too long (94 > 88 characters)

Check failure on line 35 in sketch_map_tool/map_generation/generate_pdf.py

View workflow job for this annotation

GitHub Actions / Python Linting

Ruff (E501)

sketch_map_tool/map_generation/generate_pdf.py:35:89: E501 Line too long (94 > 88 characters)
georeferencing, a scale, copyright information, and objects to help the feature detection

Check failure on line 36 in sketch_map_tool/map_generation/generate_pdf.py

View workflow job for this annotation

GitHub Actions / Python Linting

Ruff (E501)

sketch_map_tool/map_generation/generate_pdf.py:36:89: E501 Line too long (93 > 88 characters)

Check failure on line 36 in sketch_map_tool/map_generation/generate_pdf.py

View workflow job for this annotation

GitHub Actions / Python Linting

Ruff (E501)

sketch_map_tool/map_generation/generate_pdf.py:36:89: E501 Line too long (93 > 88 characters)
during the upload processing.

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
Expand All @@ -68,14 +68,14 @@
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()
Expand All @@ -85,7 +85,7 @@
# 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",
Expand Down Expand Up @@ -123,16 +123,13 @@
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

Check failure on line 128 in sketch_map_tool/map_generation/generate_pdf.py

View workflow job for this annotation

GitHub Actions / Python Linting

Ruff (E501)

sketch_map_tool/map_generation/generate_pdf.py:128:89: E501 Line too long (112 > 88 characters)

Check failure on line 128 in sketch_map_tool/map_generation/generate_pdf.py

View workflow job for this annotation

GitHub Actions / Python Linting

Ruff (E501)

sketch_map_tool/map_generation/generate_pdf.py:128:89: E501 Line too long (112 > 88 characters)
# 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(
Expand Down Expand Up @@ -167,7 +164,7 @@
qr_size = min(width, height) - margin
qr_code = resize_rlg_by_width(qr_code, qr_size)

# fills up the remaining space, placed between the TOP and the BOTTOM aligned elements

Check failure on line 167 in sketch_map_tool/map_generation/generate_pdf.py

View workflow job for this annotation

GitHub Actions / Python Linting

Ruff (E501)

sketch_map_tool/map_generation/generate_pdf.py:167:89: E501 Line too long (90 > 88 characters)

Check failure on line 167 in sketch_map_tool/map_generation/generate_pdf.py

View workflow job for this annotation

GitHub Actions / Python Linting

Ruff (E501)

sketch_map_tool/map_generation/generate_pdf.py:167:89: E501 Line too long (90 > 88 characters)
space_filler = Spacer(width, 0) # height will be filled after list creation
# order all elements in column
flowables = [
Expand Down Expand Up @@ -216,7 +213,7 @@
return normal_style


def PIL_image_to_image_reader(map_image_input):

Check failure on line 216 in sketch_map_tool/map_generation/generate_pdf.py

View workflow job for this annotation

GitHub Actions / Python Linting

Ruff (N802)

sketch_map_tool/map_generation/generate_pdf.py:216:5: N802 Function name `PIL_image_to_image_reader` should be lowercase

Check failure on line 216 in sketch_map_tool/map_generation/generate_pdf.py

View workflow job for this annotation

GitHub Actions / Python Linting

Ruff (N802)

sketch_map_tool/map_generation/generate_pdf.py:216:5: N802 Function name `PIL_image_to_image_reader` should be lowercase
map_image_raw = io.BytesIO()
map_image_input.save(map_image_raw, format="png")
map_image_reportlab = ImageReader(map_image_raw)
Expand Down
8 changes: 4 additions & 4 deletions sketch_map_tool/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
Binary file added tests/fixtures/map-img-big-landscape.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/fixtures/map-img-big-portrait.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions tests/unit/test_helpers.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,51 @@
from io import BytesIO
from pathlib import Path

import pytest
from PIL import Image

from sketch_map_tool import helpers


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
27 changes: 27 additions & 0 deletions tests/unit/test_map_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand Down