Skip to content

Commit

Permalink
resizing the watermark relative to image (fixes #874)
Browse files Browse the repository at this point in the history
two new (optional) parameters added to the watermark filter to resize
the watermark relatively to the image it should be placed on

see the provided doc file to understand how it works
  • Loading branch information
savar committed Aug 30, 2017
1 parent 1fc40b5 commit a9bfd75
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 10 deletions.
Binary file added docs/images/tom_watermark_resized_none_height.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 docs/images/tom_watermark_resized_width.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 67 additions & 2 deletions docs/watermark.rst
@@ -1,13 +1,14 @@
Watermark
=========

Usage: watermark(imageUrl, x, y, alpha)
Usage: watermark(imageUrl, x, y, alpha [, w_ratio [, h_ratio]])

Description
-----------

This filter adds a watermark to the image. It can be positioned inside the image
with the alpha channel specified.
with the alpha channel specified and optionally resized based on the image size by
specifying the ratio (see Resizing_).

Arguments
---------
Expand All @@ -32,6 +33,12 @@ Arguments
from the image height as percentage
- alpha - Watermark image transparency. Should be a number between 0
(fully opaque) and 100 (fully transparent).
- w_ratio - percentage of the width of the image the watermark should fit-in, defaults to 'none'
(without the single quotes) which means it won't be limited in the width on resizing but also won't
be resized based on this value
- h_ratio - percentage of the height of the image the watermark should fit-in, defaults to 'none'
(without the single quotes) which means it won't be limited in the height on resizing but also won't
be resized based on this value

Example
-------
Expand All @@ -46,6 +53,55 @@ Example

|watermark_relative|

Resizing
--------

Resizing is being done by defining borders the watermark needs to fit in or being upscaled to.
The ratio of the watermark will not be changed and will be expanded or shrinked to the size which
fits best into the borders.

Some examples are shown below with an original image having width=300 and height=200 and an imaginary
watermark having width=30 and height=40. Borders are shown in red and the watermark drafted in green.

- **original image (300x200)**

|original|

- **watermark(imageUrl, 30, 10, 50, 20)**

20% of the *width*: 300px*0.2 = 60px so the original watermark *width* is 30px which means it
can be resized by 2.

Because the *height* isn't limited it can grow to 2x40px which is 80px.

|watermark_resized_width|

- **watermark(imageUrl, 30, 10, 50, none, 15)**

15% of the *height*: 200px*0.15 = 30px so the original watermark *height* is 40px which means
it has to shrink by 25%.

Because the *width* isn't limited it can shrink to 0.75*30px which is 22.5px (rounded to 23px).

|watermark_resized_none_height|

- **watermark(imageUrl, 30, 10, 50, 30, 30)**

30% of the *width*: 300px*0.3 = 90px

and

30% of the *height*: 200px*0.3 = 60px

so the original watermark *width* is 30px but cannot use 90px because then (to keep
the ratio) the *height* would need to become (40/30)*90px=120px but only 60px is allowed.

Therefor the *height* is limiting the resizing here and *height* would become 60px and *width*
would be (30/40)*60px=45px which fits into the 90px border.

|watermark_resized_width_height|


.. |original| image:: images/tom_before_brightness.jpg
:alt: Picture before the watermark filter

Expand All @@ -54,3 +110,12 @@ Example

.. |watermark_relative| image:: images/tom_watermark_relative.jpg
:alt: Picture explaining watermark relative placement feature

.. |watermark_resized_width| image:: images/tom_watermark_resized_width.jpg
:alt: Picture explaining watermark resizing feature

.. |watermark_resized_none_height| image:: images/tom_watermark_resized_none_height.jpg
:alt: Picture explaining watermark resizing feature

.. |watermark_resized_width_height| image:: images/tom_watermark_resized_width_height.jpg
:alt: Picture explaining watermark resizing feature
66 changes: 65 additions & 1 deletion tests/filters/test_watermark.py
Expand Up @@ -12,7 +12,7 @@

from tests.base import FilterTestCase
from thumbor.filters import watermark
from tests.fixtures.watermark_fixtures import POSITIONS
from tests.fixtures.watermark_fixtures import SOURCE_IMAGE_SIZES, WATERMARK_IMAGE_SIZES, RATIOS, POSITIONS


class WatermarkFilterTestCase(FilterTestCase):
Expand Down Expand Up @@ -99,3 +99,67 @@ def test_watermark_filter_simple_big(self):
expected = self.get_fixture('watermarkSimpleBig.jpg')
ssim = self.get_ssim(image, expected)
expect(ssim).to_be_greater_than(0.98)

def test_watermark_filter_simple_50p_width(self):
image = self.get_filtered('source.jpg', 'thumbor.filters.watermark', 'watermark(watermark.png,30,-50,20,50)')
expected = self.get_fixture('watermarkResize50pWidth.jpg')
ssim = self.get_ssim(image, expected)
expect(ssim).to_be_greater_than(0.98)

def test_watermark_filter_simple_70p_height(self):
image = self.get_filtered('source.jpg', 'thumbor.filters.watermark', 'watermark(watermark.png,30,-50,20,none,70)')
expected = self.get_fixture('watermarkResize70pHeight.jpg')
ssim = self.get_ssim(image, expected)
expect(ssim).to_be_greater_than(0.98)

def test_watermark_filter_simple_60p_80p(self):
image = self.get_filtered('source.jpg', 'thumbor.filters.watermark', 'watermark(watermark.png,-30,-200,20,60,80)')
expected = self.get_fixture('watermarkResize60p80p.jpg')
ssim = self.get_ssim(image, expected)
expect(ssim).to_be_greater_than(0.98)

def test_watermark_filter_calculated_resizing(self):
watermark.Filter.pre_compile()
filter = watermark.Filter("http://dummy,0,0,0", self.context)

for source_image_width, source_image_height in SOURCE_IMAGE_SIZES:
for watermark_source_image_width, watermark_source_image_height in WATERMARK_IMAGE_SIZES:
for w_ratio, h_ratio in RATIOS:
max_width = source_image_width * (float(w_ratio)/100) if w_ratio else float('inf')
max_height = source_image_height * (float(h_ratio)/100) if h_ratio else float('inf')
w_ratio = float(w_ratio) / 100.0 if w_ratio else False
h_ratio = float(h_ratio) / 100.0 if h_ratio else False

ratio = float(watermark_source_image_width)/watermark_source_image_height

watermark_image_width, watermark_image_height = filter.calc_watermark_size(
(source_image_width, source_image_height),
(watermark_source_image_width, watermark_source_image_height),
w_ratio,
h_ratio
)
watermark_image = float(watermark_image_width)/watermark_image_height

test = {
'source_image_width': source_image_width,
'source_image_height': source_image_height,
'watermark_source_image_width': watermark_source_image_width,
'watermark_source_image_height': watermark_source_image_height,
'watermark_image_width': watermark_image_width,
'watermark_image_height': watermark_image_height,
'w_ratio': w_ratio,
'h_ratio': h_ratio
}

test['topic_name'] = 'watermark_image_width'
expect(watermark_image_width).to_fit_into(max_width, **test)
test['topic_name'] = 'watermark_image_height'
expect(watermark_image_height).to_fit_into(max_height, **test)

test['topic_name'] = 'fill out'
expect(
(watermark_image_width == max_width or watermark_image_height == max_height)
).to_be_true_with_additional_info(**test)

test['topic_name'] = 'image ratio'
expect(watermark_image).to_almost_equal(ratio, 2, **test)
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/filters/watermarkResize60p80p.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 82 additions & 0 deletions tests/fixtures/watermark_fixtures.py
Expand Up @@ -25,9 +25,91 @@
]


SOURCE_IMAGE_SIZES = [
(800, 600),
(600, 600),
(600, 800),
]

WATERMARK_IMAGE_SIZES = [
# bigger ones
(1200, 900),
(900, 900),
(900, 1200),

# one size bigger
(1200, 500),
(700, 400),
(400, 700),

# equal
(800, 600),
(600, 600),

# smaller
(500, 300),
(300, 300),
(300, 500),
]

RATIOS = [
# only X
(300, None),
(200, None),
(100, None),
(50, None),
(25, None),

# only Y
(None, 300),
(None, 200),
(None, 100),
(None, 50),
(None, 25),

# X and Y
(300, 300),
(200, 200),
(100, 100),
(50, 50),
(25, 25),
(300, 25),
(300, 50),
(300, 100),
(300, 200),
(25, 300),
(50, 300),
(100, 300),
(200, 300),
(25, 50),
(50, 25),
]


@preggy.assertion
def to_fit_into(topic, boundary, **kwargs):
assert topic <= boundary, \
"Expected topic({topic}) to fit into boundary {boundary} with test: {test}".format(
topic=topic, boundary=boundary, test=kwargs
)


@preggy.assertion
def to_be_true_with_additional_info(topic, **kwargs):
assert topic, \
"Expected topic to be true with test: {test}".format(test=kwargs)

@preggy.assertion
def to_be_equal_with_additional_info(topic, expected, **kwargs):
assert topic == expected, \
"Expected topic({topic}) to be ({expected}) with test: {test}".format(
topic=topic, expected=expected, test=kwargs
)

@preggy.assertion
def to_almost_equal(topic, expected, differ, **kwargs):
assert abs(1 - topic / expected) <= (differ/100.0), \
"Expected topic({topic}) to be almost equal expected({expected}) differing only in {percent}% with test: {test}".format(
topic=topic, expected=expected, test=kwargs,
percent=differ
)
46 changes: 39 additions & 7 deletions thumbor/filters/watermark.py
Expand Up @@ -30,6 +30,25 @@ def detect_and_get_ratio_position(self, pos, length):

return pos

def calc_watermark_size(self, sz, watermark_sz, w_ratio, h_ratio):
wm_max_width = sz[0] * w_ratio if w_ratio else None
wm_max_height = sz[1] * h_ratio if h_ratio else None

if not wm_max_width:
wm_max_width = watermark_sz[0] * wm_max_height / watermark_sz[1]

if not wm_max_height:
wm_max_height = watermark_sz[1] * wm_max_width / watermark_sz[0]

if float(watermark_sz[0])/wm_max_width >= float(watermark_sz[1])/wm_max_height:
wm_height = int(round(watermark_sz[1] * wm_max_width / watermark_sz[0]))
wm_width = int(round(wm_max_width))
else:
wm_height = int(round(wm_max_height))
wm_width = int(round(watermark_sz[0] * wm_max_height / watermark_sz[1]))

return (wm_width, wm_height)

def on_image_ready(self, buffer):
self.watermark_engine.load(buffer, None)
self.watermark_engine.enable_alpha()
Expand All @@ -44,6 +63,10 @@ def on_image_ready(self, buffer):
sz = self.engine.size
watermark_sz = self.watermark_engine.size

if self.w_ratio or self.h_ratio:
watermark_sz = self.calc_watermark_size(sz, watermark_sz, self.w_ratio, self.h_ratio)
self.watermark_engine.resize(watermark_sz[0], watermark_sz[1])

self.x = self.detect_and_get_ratio_position(self.x, sz[0])
self.y = self.detect_and_get_ratio_position(self.y, sz[1])

Expand Down Expand Up @@ -126,25 +149,34 @@ def on_fetch_done(self, result):
self.storage.put_crypto(self.url)
self.on_image_ready(buffer)

@tornado.gen.coroutine
@filter_method(
BaseFilter.String,
r'(?:-?\d+p?)|center|repeat',
r'(?:-?\d+p?)|center|repeat',
BaseFilter.PositiveNumber,
r'(?:-?\d+)|none',
r'(?:-?\d+)|none',
async=True
)
@tornado.gen.coroutine
def watermark(self, callback, url, x, y, alpha):
def watermark(self, callback, url, x, y, alpha, w_ratio=False, h_ratio=False):
self.url = url
self.x = x
self.y = y
self.alpha = alpha
self.w_ratio = float(w_ratio) / 100.0 if w_ratio and w_ratio != 'none' else False
self.h_ratio = float(h_ratio) / 100.0 if h_ratio and h_ratio != 'none' else False
self.callback = callback
self.watermark_engine = self.context.modules.engine.__class__(self.context)
self.storage = self.context.modules.storage

buffer = yield tornado.gen.maybe_future(self.storage.get(self.url))
if buffer is not None:
self.on_image_ready(buffer)
else:
self.context.modules.loader.load(self.context, self.url, self.on_fetch_done)
try:
buffer = yield tornado.gen.maybe_future(self.storage.get(self.url))
if buffer is not None:
self.on_image_ready(buffer)
else:
self.context.modules.loader.load(self.context, self.url, self.on_fetch_done)
except Exception as e:
logger.exception(e)
logger.warn("bad watermark")
raise tornado.web.HTTPError(500)

0 comments on commit a9bfd75

Please sign in to comment.