├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── .travis.yml ├── CHANGES ├── LICENSE ├── MANIFEST.in ├── PYPIREADME.rst ├── README.rst ├── example ├── README.rst ├── __init__.py ├── initial_data.json ├── manage.py ├── media │ ├── .gitignore │ ├── scream.jpg │ └── watermarks │ │ └── sample.png ├── requirements.txt ├── settings.py ├── templates │ └── index.html └── urls.py ├── setup.cfg ├── setup.py ├── showcase.png ├── tox.ini └── watermarker ├── __init__.py ├── admin.py ├── conf.py ├── locale └── ru │ └── LC_MESSAGES │ ├── django.mo │ └── django.po ├── migrations ├── 0001_initial.py ├── 0002_auto_20210320_2145.py └── __init__.py ├── models.py ├── templatetags ├── __init__.py └── watermark.py ├── tests ├── __init__.py ├── conftest.py ├── models.py ├── overlay.png ├── settings.py ├── test.png └── test_utils.py └── utils.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | env: 15 | TWINE_USERNAME: "__token__" 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: '3.x' 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install setuptools wheel twine tox 26 | - name: Build package 27 | run: python setup.py sdist bdist_wheel 28 | - name: Publish package 29 | env: 30 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 31 | run: twine upload dist/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | dist: xenial 3 | language: python 4 | cache: pip 5 | sudo: false 6 | 7 | python: 8 | - "3.6" 9 | - "3.7" 10 | - "3.8" 11 | - "3.9" 12 | 13 | install: 14 | - pip install tox-travis 15 | 16 | script: 17 | - tox 18 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | ----- 3 | 4 | - added the ability to remove alpha introduced with the watermark effect 5 | - dropped support for Python 2.x, removed deprecated code and various bugfixes 6 | - removed 10px margin introduced in 0.1.7 7 | 8 | 0.1.9 9 | ----- 10 | 11 | - python_2_unicode_compatible import error for latest django version 3.0.13 bug fix 12 | - fixed bug cannot write mode RGBA as JPEG 13 | 14 | 0.1.8 15 | ----- 16 | 17 | - python 2.x and 3.x bugfixes 18 | 19 | 0.1.7 20 | ----- 21 | 22 | - makes it easy for browsers like Mozilla and Opera get the correct url of images 23 | - added a 10px margin from corners to apply watermark 24 | - resize watermark before calculating its position 25 | - apply anti-aliasing on watermark 26 | - django >= 1.4 timezone support 27 | - python 3 compatibility 28 | - pillow compatibility 29 | 30 | 0.1.5-pre1 31 | ---------- 32 | 33 | - refactored a good deal of the code inside the watermark filter and placed it in utils.py. 34 | Now the filter is mostly just an interface to the utils.watermark function. 35 | - removed the "parameter precedence" with positioning, tiling, and scaling. 36 | Now they can all be used at the same time. 37 | - added several enhancements to the positioning and tiling features in particular 38 | 39 | 0.1.2-pre1 40 | ---------- 41 | 42 | - added the ability to randomly position watermarks 43 | - removed the "top-left" part of positioning--meaning that position=50%x50% 44 | actually centers the watermark image instead of centering the top-left corner 45 | of the watermark image 46 | - added watermark rotation 47 | 48 | 0.1.1-pre1 49 | ---------- 50 | 51 | - added the ability to conver the watermark image to a transparent 52 | greyscale image before applying it to the target image 53 | 54 | 0.1.0-pre2 55 | ---------- 56 | 57 | - fixed a typo that broke the filter 58 | 59 | 0.1.0-pre1 60 | ---------- 61 | 62 | - initial release 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008-2015, Josh VanderLinden 2 | Copyright (c) 2015, Basil Shubin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | * Neither the name of the author nor the names of other 16 | contributors may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES 2 | include LICENSE 3 | include README.rst 4 | recursive-include watermarker/locale * 5 | -------------------------------------------------------------------------------- /PYPIREADME.rst: -------------------------------------------------------------------------------- 1 | django-watermark 2 | ================ 3 | 4 | .. image:: https://img.shields.io/pypi/v/django-watermark.svg 5 | :target: https://pypi.python.org/pypi/django-watermark/ 6 | 7 | .. image:: https://img.shields.io/pypi/dm/django-watermark.svg 8 | :target: https://pypi.python.org/pypi/django-watermark/ 9 | 10 | .. image:: https://img.shields.io/github/license/bashu/django-watermark.svg 11 | :target: https://pypi.python.org/pypi/django-watermark/ 12 | 13 | .. image:: https://img.shields.io/travis/bashu/django-watermark.svg 14 | :target: https://travis-ci.com/github/bashu/django-watermark/ 15 | 16 | This project provides a simple way for you to apply custom watermarks 17 | to images on your django-powered website. 18 | 19 | Maintained by `Basil Shubin `_, and some great 20 | `contributors `_. 21 | 22 | .. image:: https://raw.githubusercontent.com/bashu/django-watermark/develop/showcase.png 23 | :target: https://raw.githubusercontent.com/bashu/django-watermark/develop/showcase.png 24 | :align: center 25 | :width: 600px 26 | 27 | Features 28 | -------- 29 | 30 | * Opacity: the filter allows you to specify the transparency level for your 31 | watermark image 32 | * Watermark positioning: you have several options for positioning watermarks on 33 | your images 34 | 35 | * Absolute: you can specify exact pixel locations for your watermark 36 | * Relative: you can use percentages to place your watermark 37 | * Corners: you can position your watermark in the corners of your images 38 | * Random: you can tell the filter to randomly generate a position for your 39 | watermark 40 | * Center: you can place watermarks in the center of the target image 41 | 42 | * Scaling: the watermark can be scaled to cover your images or specify a 43 | scaling factor to use 44 | * Tiling: the watermark can be tiled across your images 45 | * Greyscale: you can convert the watermark to be greyscale before having it 46 | applied to the target image. 47 | * Rotation: you can rotate your watermark a certain number of degrees or have 48 | the rotation be random. 49 | 50 | Installation 51 | ------------ 52 | 53 | First install the module, preferably in a virtual environment. It can be installed from PyPI: 54 | 55 | .. code-block:: bash 56 | 57 | pip install django-watermark 58 | 59 | Setup 60 | ----- 61 | 62 | First of all, you must add this project to your list of ``INSTALLED_APPS`` in 63 | ``settings.py`` : 64 | 65 | .. code-block:: python 66 | 67 | INSTALLED_APPS += [ 68 | "watermarker", 69 | ] 70 | 71 | Run ``./manage.py migrate``. This creates the tables in your database 72 | that are necessary for operation. 73 | 74 | Please see ``example`` application. This application is used to manually 75 | test the functionalities of this package. This also serves as a good 76 | example. 77 | 78 | You need Django 1.4 or above to run that. It might run on older 79 | versions but that is not tested. 80 | 81 | Upgrading from 0.1.6 82 | ~~~~~~~~~~~~~~~~~~~~ 83 | 84 | Upgrading from 0.1.6 is likely to cause problems trying to apply a 85 | migration when the tables already exist. In this case a fake migration 86 | needs to be applied: 87 | 88 | .. code-block:: shell 89 | 90 | ./manage.py migrate watermarker 0001 --fake 91 | 92 | Configuration (optional) 93 | ------------------------ 94 | 95 | While we're in this section, I might as well mention a settings 96 | variable that you can override: ``WATERMARK_QUALITY``. This should 97 | be an integer between 0 and 100. The default is 85. 98 | 99 | By default, ``django-watermark`` obscures the original image's file 100 | name, as the original requirements were to make it impossible to 101 | download the watermark-less image. As of version 0.1.6, you can set 102 | ``WATERMARK_OBSCURE_ORIGINAL`` to ``False`` in your ``setings.py`` to 103 | make the original image file name accessible to the user. 104 | 105 | ``django-watermark`` also lets you configure how random watermark 106 | positioning should work. By default, a when a watermark is to be 107 | positioned randomly, only one watermarked image will be generated. If 108 | you wish to generate a random position for an image's watermark on 109 | each request, set ``WATERMARK_RANDOM_POSITION_ONCE`` to ``False`` in 110 | your ``settings.py``. 111 | 112 | Usage 113 | ----- 114 | 115 | As mentioned above, you have several options when using ``django-watermark``. 116 | The first thing you must do is load the filter for the template in which you 117 | wish to apply watermarks to your images. 118 | 119 | .. code-block:: html+django 120 | 121 | {% load watermark %} 122 | 123 | From the Django admin, go ahead and populate your database with some watermarks 124 | that you want to apply to your regular images. Simply specify a name for the 125 | watermark and upload the watermark image itself. *It's probably not a good 126 | idea to put commas in your watermark names.* Watermarks should be transparent 127 | PNG files for best results. I can't make any guarantees that other formats 128 | will work nicely. 129 | 130 | The first parameter to the ``watermark`` filter _must_ be the name you 131 | specified for the watermark in the Django admin. You can then choose from a 132 | few other parameters to customize the application of the watermark. Here they 133 | are: 134 | 135 | * ``position`` - This one is quite customizable. First, you can plug your 136 | watermark into one corner of your images by using one of ``BR``, ``BL``, 137 | ``TR``, and ``TL``. These represent 'bottom-right', 'bottom-left', 138 | 'top-right', and 'top-left' respectively. 139 | 140 | Alternatively, you can use relative or absolute positioning for the 141 | watermark. Relative positioning uses percentages; absolute positioning uses 142 | exact pixels. You can mix and match these two modes of positioning, but you 143 | cannot mix and match relative/absolute with the corner positioning. When 144 | using relative/absolute positioning, the value for the ``position`` parameter 145 | is ``XxY``, where ``X`` is the left value and ``Y`` is the top value. The 146 | left and top values must be separated with a lowercase ``x``. 147 | 148 | If you wanted your watermark image to show up in the center of any image you 149 | want to watermark, you would use a position parameter such as 150 | ``position=50%x50%`` or even ``position=C``. If you wanted the watermark to 151 | show up half-way between the left and right edges of the image and 100 pixels 152 | from the top, you would use a position parameter such as 153 | ``position=50%x100``. 154 | 155 | Finally, you may tell the filter to generate a position for your watermark 156 | dynamically. To do this, use ``position=R``. 157 | * ``opacity`` - This parameter allows you to specify the transparency of the 158 | applied watermark. The value must be an integer between 0 and 100, where 0 159 | is fully transparent and 100 is fully opaque. By default, the opacity is set 160 | at 50%. 161 | * ``tile`` - If you want your watermark to tile across the entire image, you 162 | simply specify a parameter such as ``tile=1``. 163 | * ``scale`` - If you'd like to have the watermark as big as possible on the 164 | target image and fully visible, you might want to use ``scale=F``. If you 165 | want to specify a particular scaling factor, just use something like 166 | ``scale=1.43``. Scale could also be a percentage of the smallest image, the 167 | one to be watermarked, dimension, for example ``scale=R20%`` would scale the 168 | watermark to be 20% of the smallest between width and height of the target image. 169 | * ``greyscale`` - If you want your watermark to be greyscale, you can specify 170 | the parameter ``greyscale=1`` and all color saturation will go away. 171 | * ``rotation`` - Set this parameter to any integer between 0 and 359 (really 172 | any integer should work, but for your own sanity I recommend keeping the 173 | value between 0 and 359). If you want the rotation to be random, use 174 | ``rotation=R`` instead of an integer. 175 | * ``obscure`` - Set this parameter to 0 to make the original image's filename 176 | visible to the user. Default is 1 (or True) to obscure the original 177 | filename. 178 | * ``noalpha`` - Set this to 1 to remove any alpha introduced with the watermark 179 | effect, useful to force a JPEG image to remain the same, saving a lot of space, 180 | setting to 1 effectively converts any RGBA color space to RGB. Defalt is 1 (or True). 181 | * ``quality`` - Set this to an integer between 0 and 100 to specify the quality 182 | of the resulting image. Default is 85. 183 | * ``random_position_once`` - Set this to 0 or 1 to specify the random 184 | positioning behavior for the image's watermark. When set to 0, the watermark 185 | will be randomly placed on each request. When set to 1, the watermark will 186 | be positioned randomly on the first request, and subsequent requests will use 187 | the produced image. Default is ``True`` (random positioning only happens on 188 | first request). 189 | 190 | Examples 191 | ~~~~~~~~ 192 | 193 | .. code-block:: html+django 194 | 195 | {{ image_url|watermark:"My Watermark,position=br,opacity=35" }} 196 | 197 | Looks for a watermark named "My Watermark", place it in the bottom-right corner of the target image, using a 35% transparency level. 198 | 199 | .. code-block:: html+django 200 | 201 | {{ image_url|watermark:"Your Watermark,position=tl,opacity=75" }} 202 | 203 | Looks for a watermark named "Your Watermark", place it in the top-left corner of the target image, using a 75% transparency level. 204 | 205 | .. code-block:: html+django 206 | 207 | {{ image_url|watermark:"The Watermark,position=43%x80%,opacity=40" }} 208 | 209 | Looks for a watermark named "The Watermark", places it at 43% on the x-axis and 80% of the y-axis of the target image, at a transparency level of 40%. 210 | 211 | .. code-block:: html+django 212 | 213 | {{ image_url|watermark:"The Watermark,position=R,opacity=10,rotation=45" }} 214 | 215 | Looks for a watermark named "The Watermark", randomly generates a position for it, at a transparency level of 10%, rotated 45 degrees. 216 | 217 | .. code-block:: html+django 218 | 219 | {{ image_url|watermark:"w00t,opacity=40,tile=1" }} 220 | 221 | Looks for a watermark called "w00t", tiles it across the entire target image, at a transparency level of 40%. 222 | 223 | Credits 224 | ------- 225 | 226 | `django-watermark `_ was originally started by `Josh VanderLinden `_ who has now unfortunately abandoned the project. 227 | 228 | Based on recipe from http://code.activestate.com/recipes/362879/ created by Shane Hathaway. 229 | 230 | License 231 | ------- 232 | 233 | ``django-watermark`` is released under the BSD license. 234 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-watermark 2 | ================ 3 | 4 | .. image:: https://img.shields.io/pypi/v/django-watermark.svg 5 | :target: https://pypi.python.org/pypi/django-watermark/ 6 | 7 | .. image:: https://img.shields.io/pypi/dm/django-watermark.svg 8 | :target: https://pypi.python.org/pypi/django-watermark/ 9 | 10 | .. image:: https://img.shields.io/github/license/bashu/django-watermark.svg 11 | :target: https://pypi.python.org/pypi/django-watermark/ 12 | 13 | .. image:: https://img.shields.io/travis/bashu/django-watermark.svg 14 | :target: https://travis-ci.com/github/bashu/django-watermark/ 15 | 16 | This project provides a simple way for you to apply custom watermarks 17 | to images on your django-powered website. 18 | 19 | Maintained by `Basil Shubin `_, and some great 20 | `contributors `_. 21 | 22 | .. raw:: html 23 | 24 |

25 | 26 |

27 | 28 | Features 29 | -------- 30 | 31 | * Opacity: the filter allows you to specify the transparency level for your 32 | watermark image 33 | * Watermark positioning: you have several options for positioning watermarks on 34 | your images 35 | 36 | * Absolute: you can specify exact pixel locations for your watermark 37 | * Relative: you can use percentages to place your watermark 38 | * Corners: you can position your watermark in the corners of your images 39 | * Random: you can tell the filter to randomly generate a position for your 40 | watermark 41 | * Center: you can place watermarks in the center of the target image 42 | 43 | * Scaling: the watermark can be scaled to cover your images or specify a 44 | scaling factor to use 45 | * Tiling: the watermark can be tiled across your images 46 | * Greyscale: you can convert the watermark to be greyscale before having it 47 | applied to the target image. 48 | * Rotation: you can rotate your watermark a certain number of degrees or have 49 | the rotation be random. 50 | 51 | Installation 52 | ------------ 53 | 54 | First install the module, preferably in a virtual environment. It can be installed from PyPI: 55 | 56 | .. code-block:: bash 57 | 58 | pip install django-watermark 59 | 60 | Setup 61 | ----- 62 | 63 | First of all, you must add this project to your list of ``INSTALLED_APPS`` in 64 | ``settings.py`` : 65 | 66 | .. code-block:: python 67 | 68 | INSTALLED_APPS += [ 69 | "watermarker", 70 | ] 71 | 72 | Run ``./manage.py migrate``. This creates the tables in your database 73 | that are necessary for operation. 74 | 75 | Please see ``example`` application. This application is used to manually 76 | test the functionalities of this package. This also serves as a good 77 | example. 78 | 79 | You need Django 1.4 or above to run that. It might run on older 80 | versions but that is not tested. 81 | 82 | Upgrading from 0.1.6 83 | ~~~~~~~~~~~~~~~~~~~~ 84 | 85 | Upgrading from 0.1.6 is likely to cause problems trying to apply a 86 | migration when the tables already exist. In this case a fake migration 87 | needs to be applied: 88 | 89 | .. code-block:: shell 90 | 91 | ./manage.py migrate watermarker 0001 --fake 92 | 93 | Configuration (optional) 94 | ------------------------ 95 | 96 | While we're in this section, I might as well mention a settings 97 | variable that you can override: ``WATERMARK_QUALITY``. This should 98 | be an integer between 0 and 100. The default is 85. 99 | 100 | By default, ``django-watermark`` obscures the original image's file 101 | name, as the original requirements were to make it impossible to 102 | download the watermark-less image. As of version 0.1.6, you can set 103 | ``WATERMARK_OBSCURE_ORIGINAL`` to ``False`` in your ``setings.py`` to 104 | make the original image file name accessible to the user. 105 | 106 | ``django-watermark`` also lets you configure how random watermark 107 | positioning should work. By default, a when a watermark is to be 108 | positioned randomly, only one watermarked image will be generated. If 109 | you wish to generate a random position for an image's watermark on 110 | each request, set ``WATERMARK_RANDOM_POSITION_ONCE`` to ``False`` in 111 | your ``settings.py``. 112 | 113 | Usage 114 | ----- 115 | 116 | As mentioned above, you have several options when using ``django-watermark``. 117 | The first thing you must do is load the filter for the template in which you 118 | wish to apply watermarks to your images. 119 | 120 | .. code-block:: html+django 121 | 122 | {% load watermark %} 123 | 124 | From the Django admin, go ahead and populate your database with some watermarks 125 | that you want to apply to your regular images. Simply specify a name for the 126 | watermark and upload the watermark image itself. *It's probably not a good 127 | idea to put commas in your watermark names.* Watermarks should be transparent 128 | PNG files for best results. I can't make any guarantees that other formats 129 | will work nicely. 130 | 131 | The first parameter to the ``watermark`` filter _must_ be the name you 132 | specified for the watermark in the Django admin. You can then choose from a 133 | few other parameters to customize the application of the watermark. Here they 134 | are: 135 | 136 | * ``position`` - This one is quite customizable. First, you can plug your 137 | watermark into one corner of your images by using one of ``BR``, ``BL``, 138 | ``TR``, and ``TL``. These represent 'bottom-right', 'bottom-left', 139 | 'top-right', and 'top-left' respectively. 140 | 141 | Alternatively, you can use relative or absolute positioning for the 142 | watermark. Relative positioning uses percentages; absolute positioning uses 143 | exact pixels. You can mix and match these two modes of positioning, but you 144 | cannot mix and match relative/absolute with the corner positioning. When 145 | using relative/absolute positioning, the value for the ``position`` parameter 146 | is ``XxY``, where ``X`` is the left value and ``Y`` is the top value. The 147 | left and top values must be separated with a lowercase ``x``. 148 | 149 | If you wanted your watermark image to show up in the center of any image you 150 | want to watermark, you would use a position parameter such as 151 | ``position=50%x50%`` or even ``position=C``. If you wanted the watermark to 152 | show up half-way between the left and right edges of the image and 100 pixels 153 | from the top, you would use a position parameter such as 154 | ``position=50%x100``. 155 | 156 | Finally, you may tell the filter to generate a position for your watermark 157 | dynamically. To do this, use ``position=R``. 158 | * ``opacity`` - This parameter allows you to specify the transparency of the 159 | applied watermark. The value must be an integer between 0 and 100, where 0 160 | is fully transparent and 100 is fully opaque. By default, the opacity is set 161 | at 50%. 162 | * ``tile`` - If you want your watermark to tile across the entire image, you 163 | simply specify a parameter such as ``tile=1``. 164 | * ``scale`` - If you'd like to have the watermark as big as possible on the 165 | target image and fully visible, you might want to use ``scale=F``. If you 166 | want to specify a particular scaling factor, just use something like 167 | ``scale=1.43``. Scale could also be a percentage of the smallest image, the 168 | one to be watermarked, dimension, for example ``scale=R20%`` would scale the 169 | watermark to be 20% of the smallest between width and height of the target image. 170 | * ``greyscale`` - If you want your watermark to be greyscale, you can specify 171 | the parameter ``greyscale=1`` and all color saturation will go away. 172 | * ``rotation`` - Set this parameter to any integer between 0 and 359 (really 173 | any integer should work, but for your own sanity I recommend keeping the 174 | value between 0 and 359). If you want the rotation to be random, use 175 | ``rotation=R`` instead of an integer. 176 | * ``obscure`` - Set this parameter to 0 to make the original image's filename 177 | visible to the user. Default is 1 (or True) to obscure the original 178 | filename. 179 | * ``noalpha`` - Set this to 1 to remove any alpha introduced with the watermark 180 | effect, useful to force a JPEG image to remain the same, saving a lot of space, 181 | setting to 1 effectively converts any RGBA color space to RGB. Defalt is 1 (or True). 182 | * ``quality`` - Set this to an integer between 0 and 100 to specify the quality 183 | of the resulting image. Default is 85. 184 | * ``random_position_once`` - Set this to 0 or 1 to specify the random 185 | positioning behavior for the image's watermark. When set to 0, the watermark 186 | will be randomly placed on each request. When set to 1, the watermark will 187 | be positioned randomly on the first request, and subsequent requests will use 188 | the produced image. Default is ``True`` (random positioning only happens on 189 | first request). 190 | 191 | Examples 192 | ~~~~~~~~ 193 | 194 | .. code-block:: html+django 195 | 196 | {{ image_url|watermark:"My Watermark,position=br,opacity=35" }} 197 | 198 | Looks for a watermark named "My Watermark", place it in the bottom-right corner of the target image, using a 35% transparency level. 199 | 200 | .. code-block:: html+django 201 | 202 | {{ image_url|watermark:"Your Watermark,position=tl,opacity=75" }} 203 | 204 | Looks for a watermark named "Your Watermark", place it in the top-left corner of the target image, using a 75% transparency level. 205 | 206 | .. code-block:: html+django 207 | 208 | {{ image_url|watermark:"The Watermark,position=43%x80%,opacity=40" }} 209 | 210 | Looks for a watermark named "The Watermark", places it at 43% on the x-axis and 80% of the y-axis of the target image, at a transparency level of 40%. 211 | 212 | .. code-block:: html+django 213 | 214 | {{ image_url|watermark:"The Watermark,position=R,opacity=10,rotation=45" }} 215 | 216 | Looks for a watermark named "The Watermark", randomly generates a position for it, at a transparency level of 10%, rotated 45 degrees. 217 | 218 | .. code-block:: html+django 219 | 220 | {{ image_url|watermark:"w00t,opacity=40,tile=1" }} 221 | 222 | Looks for a watermark called "w00t", tiles it across the entire target image, at a transparency level of 40%. 223 | 224 | Credits 225 | ------- 226 | 227 | `django-watermark `_ was originally started by `Josh VanderLinden `_ who has now unfortunately abandoned the project. 228 | 229 | Based on recipe from http://code.activestate.com/recipes/362879/ created by Shane Hathaway. 230 | 231 | License 232 | ------- 233 | 234 | ``django-watermark`` is released under the BSD license. 235 | -------------------------------------------------------------------------------- /example/README.rst: -------------------------------------------------------------------------------- 1 | Example 2 | ======= 3 | 4 | To run the example application, make sure you have the required 5 | packages installed. You can do this using following commands : 6 | 7 | .. code-block:: bash 8 | 9 | mkvirtualenv example 10 | pip install -r example/requirements.txt 11 | 12 | This assumes you already have ``virtualenv`` and ``virtualenvwrapper`` 13 | installed and configured. 14 | 15 | Next, you can setup the django instance using : 16 | 17 | .. code-block:: bash 18 | 19 | python example/manage.py migrate 20 | python example/manage.py loaddata example/initial_data.json 21 | 22 | And run it : 23 | 24 | .. code-block:: bash 25 | 26 | python example/manage.py runserver 27 | 28 | Good luck! 29 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-watermark/72c67c9a668b76db5e2f1618a93aef35bf7f9ece/example/__init__.py -------------------------------------------------------------------------------- /example/initial_data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "watermarker.watermark", 5 | "fields": { 6 | "date_updated": "2015-07-19T10:20:18.108Z", 7 | "date_created": "2015-07-19T10:06:45.668Z", 8 | "image": "watermarks/sample.png", 9 | "is_active": true, 10 | "name": "sample" 11 | } 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | # Allow starting the app without installing the module. 11 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 12 | 13 | execute_from_command_line(sys.argv) 14 | -------------------------------------------------------------------------------- /example/media/.gitignore: -------------------------------------------------------------------------------- 1 | watermarked 2 | -------------------------------------------------------------------------------- /example/media/scream.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-watermark/72c67c9a668b76db5e2f1618a93aef35bf7f9ece/example/media/scream.jpg -------------------------------------------------------------------------------- /example/media/watermarks/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-watermark/72c67c9a668b76db5e2f1618a93aef35bf7f9ece/example/media/watermarks/sample.png -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | django-appconf 3 | pillow 4 | -------------------------------------------------------------------------------- /example/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for example project. 3 | 4 | For more information on this file, see 5 | https://docs.djangoproject.com/en/1.8/topics/settings/ 6 | 7 | For the full list of settings and their values, see 8 | https://docs.djangoproject.com/en/1.8/ref/settings/ 9 | """ 10 | 11 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 12 | import os 13 | 14 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 15 | 16 | # Quick-start development settings - unsuitable for production 17 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 18 | 19 | # SECURITY WARNING: keep the secret key used in production secret! 20 | SECRET_KEY = "YOUR_SECRET_KEY" 21 | 22 | # SECURITY WARNING: don't run with debug turned on in production! 23 | DEBUG = True 24 | 25 | ALLOWED_HOSTS = [] 26 | 27 | SERVE_MEDIA = True 28 | 29 | # Application definition 30 | 31 | PROJECT_APPS = [ 32 | "watermarker", 33 | ] 34 | 35 | INSTALLED_APPS = [ 36 | "django.contrib.admin", 37 | "django.contrib.auth", 38 | "django.contrib.contenttypes", 39 | "django.contrib.sessions", 40 | "django.contrib.messages", 41 | "django.contrib.staticfiles", 42 | ] + PROJECT_APPS 43 | 44 | MIDDLEWARE_CLASSES = [ 45 | "django.contrib.sessions.middleware.SessionMiddleware", 46 | "django.middleware.common.CommonMiddleware", 47 | "django.middleware.csrf.CsrfViewMiddleware", 48 | "django.contrib.auth.middleware.AuthenticationMiddleware", 49 | # "django.contrib.auth.middleware.SessionAuthenticationMiddleware", 50 | "django.contrib.messages.middleware.MessageMiddleware", 51 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 52 | # "django.middleware.security.SecurityMiddleware", 53 | ] 54 | 55 | MIDDLEWARE = MIDDLEWARE_CLASSES 56 | 57 | ROOT_URLCONF = "example.urls" 58 | 59 | SITE_ID = 1 60 | 61 | 62 | TEMPLATES = [ 63 | { 64 | "BACKEND": "django.template.backends.django.DjangoTemplates", 65 | "DIRS": [ 66 | os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates"), 67 | ], 68 | "APP_DIRS": True, 69 | "OPTIONS": { 70 | "context_processors": [ 71 | "django.template.context_processors.debug", 72 | "django.template.context_processors.request", 73 | "django.contrib.auth.context_processors.auth", 74 | "django.contrib.messages.context_processors.messages", 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 82 | 83 | DATABASES = { 84 | "default": { 85 | "ENGINE": "django.db.backends.sqlite3", 86 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 87 | } 88 | } 89 | 90 | # Internationalization 91 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 92 | 93 | LANGUAGE_CODE = "en-us" 94 | 95 | TIME_ZONE = "UTC" 96 | 97 | USE_I18N = True 98 | 99 | USE_L10N = True 100 | 101 | USE_TZ = True 102 | 103 | # Static files (CSS, JavaScript, Images) 104 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 105 | 106 | # Absolute filesystem path to the directory that will hold user-uploaded files. 107 | MEDIA_ROOT = os.path.join(os.path.dirname(__file__), "media") 108 | 109 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 110 | # trailing slash. 111 | MEDIA_URL = "/media/" 112 | 113 | # Absolute path to the directory static files should be collected to. 114 | STATIC_ROOT = os.path.join(os.path.dirname(__file__), "static") 115 | 116 | STATIC_URL = "/static/" 117 | 118 | # Default primary key field type 119 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 120 | 121 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 122 | 123 | # Watermark settings 124 | 125 | WATERMARK_QUALITY = 95 126 | WATERMARK_OBSCURE_ORIGINAL = True 127 | WATERMARK_RANDOM_POSITION_ONCE = False 128 | -------------------------------------------------------------------------------- /example/templates/index.html: -------------------------------------------------------------------------------- 1 | {% load static watermark %} 2 | 3 | 4 | 5 | {% with "/media/scream.jpg" as url_path %} 6 | 7 | {% endwith %} 8 | 9 | 10 | -------------------------------------------------------------------------------- /example/urls.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.conf import settings 4 | from django.contrib import admin 5 | from django.urls import path, re_path 6 | from django.views.generic import TemplateView 7 | 8 | urlpatterns = [ 9 | path("admin/", admin.site.urls), 10 | ] 11 | 12 | if settings.SERVE_MEDIA: 13 | from django.views.static import serve 14 | 15 | urlpatterns += [ 16 | re_path( 17 | r"^%s(?P.*)$" % re.escape(settings.STATIC_URL.lstrip("/")), 18 | serve, 19 | kwargs={"document_root": settings.STATIC_ROOT}, 20 | ) 21 | ] 22 | 23 | urlpatterns += [ 24 | re_path( 25 | r"^%s(?P.*)$" % re.escape(settings.MEDIA_URL.lstrip("/")), 26 | serve, 27 | kwargs={"document_root": settings.MEDIA_ROOT}, 28 | ) 29 | ] 30 | 31 | urlpatterns += [ 32 | path("", TemplateView.as_view(template_name="index.html")), 33 | ] 34 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = django-watermark 3 | version = 0.2.0 4 | description = Quick and efficient way to apply watermarks to images in Django 5 | long_description = file: PYPIREADME.rst 6 | long_description_content_type = text/x-rst 7 | keywords = django, watermark, image, photo, logo 8 | author = Josh VanderLinden 9 | author_email = codekoala@gmail.com 10 | maintainer = Basil Shubin 11 | maintainer_email = basil.shubin@gmail.com 12 | url = https://github.com/bashu/django-watermark 13 | download_url = https://github.com/bashu/django-watermark/zipball/master 14 | license = BSD License 15 | classifiers = 16 | Development Status :: 5 - Production/Stable 17 | Environment :: Web Environment 18 | Intended Audience :: Developers 19 | License :: OSI Approved :: BSD License 20 | Operating System :: OS Independent 21 | Programming Language :: Python 22 | Programming Language :: Python :: 3 :: Only 23 | Programming Language :: Python :: 3.6 24 | Programming Language :: Python :: 3.7 25 | Programming Language :: Python :: 3.8 26 | Programming Language :: Python :: 3.9 27 | Framework :: Django 28 | Framework :: Django :: 2.2 29 | Framework :: Django :: 3.0 30 | Framework :: Django :: 3.1 31 | Framework :: Django :: 3.2 32 | 33 | [options] 34 | zip_safe = False 35 | include_package_data = True 36 | packages = find: 37 | install_requires = 38 | django-appconf 39 | pillow 40 | 41 | [options.packages.find] 42 | exclude = example* 43 | 44 | [options.extras_require] 45 | develop = 46 | tox 47 | django 48 | pytest-django 49 | pytest 50 | test = 51 | pytest-django 52 | pytest-cov 53 | pytest 54 | 55 | [bdist_wheel] 56 | # No longer universal (Python 3 only) but leaving this section in here will 57 | # trigger zest to build a wheel. 58 | universal = 0 59 | 60 | [flake8] 61 | # Some sane defaults for the code style checker flake8 62 | # black compatibility 63 | max-line-length = 88 64 | # E203 and W503 have edge cases handled by black 65 | extend-ignore = E203, W503 66 | exclude = 67 | .tox 68 | build 69 | dist 70 | .eggs 71 | 72 | [tool:pytest] 73 | DJANGO_SETTINGS_MODULE = watermarker.tests.settings 74 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-watermark/72c67c9a668b76db5e2f1618a93aef35bf7f9ece/showcase.png -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | distribute = False 3 | envlist = 4 | py{36,37,38,39}-dj{22,30,31,32} 5 | skip_missing_interpreters = True 6 | 7 | [travis] 8 | python = 9 | 3.6: py36 10 | 3.7: py37 11 | 3.8: py38 12 | 3.9: py39 13 | 14 | [testenv] 15 | usedevelop = True 16 | extras = test 17 | setenv = 18 | DJANGO_SETTINGS_MODULE = watermarker.tests.settings 19 | deps = 20 | dj22: Django>=2.2,<3.0 21 | dj30: Django>=3.0,<3.1 22 | dj31: Django>=3.1,<3.2 23 | dj32: Django>=3.2,<3.3 24 | commands = pytest --cov --cov-append --cov-report= 25 | -------------------------------------------------------------------------------- /watermarker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-watermark/72c67c9a668b76db5e2f1618a93aef35bf7f9ece/watermarker/__init__.py -------------------------------------------------------------------------------- /watermarker/admin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.contrib import admin 4 | 5 | from .models import Watermark 6 | 7 | 8 | class WatermarkAdmin(admin.ModelAdmin): 9 | list_display = ["name", "is_active"] 10 | list_filter = ["is_active"] 11 | search_fields = ["name"] 12 | 13 | 14 | admin.site.register(Watermark, WatermarkAdmin) 15 | -------------------------------------------------------------------------------- /watermarker/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from appconf import AppConf 4 | from django.conf import settings # pylint: disable=W0611 5 | 6 | 7 | class WatermarkSettings(AppConf): 8 | QUALITY = 85 9 | OBSCURE_ORIGINAL = True 10 | RANDOM_POSITION_ONCE = True 11 | 12 | class Meta: 13 | prefix = "watermark" 14 | holder = "watermarker.conf.settings" 15 | -------------------------------------------------------------------------------- /watermarker/locale/ru/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-watermark/72c67c9a668b76db5e2f1618a93aef35bf7f9ece/watermarker/locale/ru/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /watermarker/locale/ru/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the PACKAGE package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: django-watermark\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2013-08-05 09:02+0700\n" 11 | "PO-Revision-Date: 2013-08-05 09:33+0700\n" 12 | "Last-Translator: Basil Shubin \n" 13 | "Language-Team: ru \n" 14 | "MIME-Version: 1.0\n" 15 | "Content-Type: text/plain; charset=UTF-8\n" 16 | "Content-Transfer-Encoding: 8bit\n" 17 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" 18 | "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" 19 | "X-Generator: Poedit 1.5.4\n" 20 | 21 | #: models.py:11 22 | msgid "name" 23 | msgstr "название" 24 | 25 | #: models.py:12 26 | msgid "image" 27 | msgstr "изображение" 28 | 29 | #: models.py:13 30 | msgid "is active" 31 | msgstr "активный" 32 | 33 | #: models.py:22 34 | msgid "watermark" 35 | msgstr "водяной знак" 36 | 37 | #: models.py:23 38 | msgid "watermarks" 39 | msgstr "водяные знаки" 40 | -------------------------------------------------------------------------------- /watermarker/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="Watermark", 14 | fields=[ 15 | ("id", models.AutoField(verbose_name="ID", serialize=False, auto_created=True, primary_key=True)), 16 | ("name", models.CharField(max_length=50, verbose_name="name")), 17 | ("image", models.ImageField(upload_to=b"watermarks", verbose_name="image")), 18 | ("is_active", models.BooleanField(default=True, verbose_name="is active")), 19 | ("date_created", models.DateTimeField(auto_now_add=True)), 20 | ("date_updated", models.DateTimeField(auto_now=True)), 21 | ], 22 | options={ 23 | "ordering": ["name"], 24 | "verbose_name": "watermark", 25 | "verbose_name_plural": "watermarks", 26 | }, 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /watermarker/migrations/0002_auto_20210320_2145.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.0.13 on 2021-03-20 18:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("watermarker", "0001_initial"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name="watermark", 15 | name="image", 16 | field=models.ImageField(upload_to="watermarks", verbose_name="image"), 17 | ), 18 | migrations.AlterField( 19 | model_name="watermark", 20 | name="is_active", 21 | field=models.BooleanField(blank=True, default=True, verbose_name="is active"), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /watermarker/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-watermark/72c67c9a668b76db5e2f1618a93aef35bf7f9ece/watermarker/migrations/__init__.py -------------------------------------------------------------------------------- /watermarker/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from django.db import models 4 | from django.utils.translation import gettext_lazy as _ 5 | 6 | 7 | class Watermark(models.Model): 8 | 9 | name = models.CharField(max_length=50, verbose_name=_("name")) 10 | image = models.ImageField(upload_to="watermarks", verbose_name=_("image")) 11 | is_active = models.BooleanField(default=True, blank=True, verbose_name=_("is active")) 12 | 13 | # for internal use... 14 | 15 | date_created = models.DateTimeField(auto_now_add=True) 16 | date_updated = models.DateTimeField(auto_now=True) 17 | 18 | class Meta: 19 | ordering = ["name"] 20 | verbose_name = _("watermark") 21 | verbose_name_plural = _("watermarks") 22 | 23 | def __str__(self): 24 | return self.name 25 | 26 | def save(self, *args, **kwargs): 27 | if self.is_active: 28 | # select all other active items 29 | qs = self.__class__.objects.filter(name__exact=self.name, is_active=True) 30 | # except self (if self already exists) 31 | if self.pk: 32 | qs = qs.exclude(pk=self.pk) 33 | # and deactive them 34 | qs.update(is_active=False) 35 | 36 | super(Watermark, self).save(*args, **kwargs) 37 | -------------------------------------------------------------------------------- /watermarker/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-watermark/72c67c9a668b76db5e2f1618a93aef35bf7f9ece/watermarker/templatetags/__init__.py -------------------------------------------------------------------------------- /watermarker/templatetags/watermark.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import errno 4 | import hashlib 5 | import logging 6 | import os 7 | import traceback 8 | from datetime import datetime 9 | 10 | from PIL import Image 11 | 12 | from urllib.parse import unquote 13 | from urllib.request import url2pathname 14 | 15 | from django import template 16 | from django.utils.encoding import smart_str 17 | from django.utils.timezone import get_default_timezone, is_aware, make_aware 18 | 19 | from watermarker import utils 20 | from watermarker.conf import settings 21 | from watermarker.models import Watermark 22 | 23 | QUALITY = settings.WATERMARK_QUALITY 24 | OBSCURE_ORIGINAL = settings.WATERMARK_OBSCURE_ORIGINAL 25 | RANDOM_POSITION_ONCE = settings.WATERMARK_RANDOM_POSITION_ONCE 26 | 27 | register = template.Library() 28 | 29 | logger = logging.getLogger("watermarker") 30 | 31 | 32 | class Watermarker(object): 33 | def __call__( 34 | self, 35 | url, 36 | name, 37 | position=None, 38 | opacity=0.5, 39 | tile=False, 40 | scale=1.0, 41 | greyscale=False, 42 | rotation=0, 43 | noalpha=True, 44 | quality=QUALITY, 45 | obscure=OBSCURE_ORIGINAL, 46 | random_position_once=RANDOM_POSITION_ONCE, 47 | ): 48 | """ 49 | Creates a watermarked copy of an image. 50 | """ 51 | # look for the specified watermark by name. If it's not there, go no 52 | # further 53 | try: 54 | watermark = Watermark.objects.get(name__exact=name, is_active=True) 55 | except Watermark.DoesNotExist: 56 | logger.error('Watermark "%s" does not exist... Bailing out.' % name) 57 | return url 58 | 59 | # make sure URL is a string 60 | url = smart_str(url) 61 | 62 | basedir = "%s/watermarked/" % os.path.dirname(url) 63 | original_basename, ext = os.path.splitext(os.path.basename(url)) 64 | 65 | # open the target image file along with the watermark image 66 | target = Image.open(self._get_filesystem_path(url)) 67 | mark = Image.open(watermark.image.path) 68 | 69 | # determine the actual value that the parameters provided will render 70 | random_position = bool(position is None or str(position).lower() == "r") 71 | scale = utils.determine_scale(scale, target, mark) 72 | mark = mark.resize(scale, resample=Image.LANCZOS) 73 | rotation = utils.determine_rotation(rotation, mark) 74 | pos = utils.determine_position(position, target, mark) 75 | 76 | # see if we need to create only one randomly positioned watermarked 77 | # image 78 | if not random_position or (not random_position_once and random_position): 79 | logger.debug("Generating random position for watermark each time") 80 | position = pos 81 | else: 82 | logger.debug("Random positioning watermark once") 83 | 84 | params = { 85 | "position": position, 86 | "opacity": opacity, 87 | "scale": scale, 88 | "tile": tile, 89 | "greyscale": greyscale, 90 | "rotation": rotation, 91 | "original_basename": original_basename, 92 | "ext": ext, 93 | "noalpha": noalpha, 94 | "quality": quality, 95 | "watermark": watermark.id, 96 | "left": pos[0], 97 | "top": pos[1], 98 | "fstat": os.stat(self._get_filesystem_path(url)), 99 | } 100 | logger.debug("Params: %s" % params) 101 | 102 | fname = self.generate_filename(mark, **params) 103 | url_path = self.get_url_path(basedir, original_basename, ext, fname, obscure) 104 | fpath = self._get_filesystem_path(url_path) 105 | 106 | logger.debug( 107 | "Watermark name: %s; URL: %s; Path: %s" 108 | % ( 109 | fname, 110 | url_path, 111 | fpath, 112 | ) 113 | ) 114 | 115 | # see if the image already exists on the filesystem. If it does, use it. 116 | if os.access(fpath, os.R_OK): 117 | # see if the ``Watermark`` object was modified since the 118 | # file was created 119 | modified = make_aware(datetime.fromtimestamp(os.path.getmtime(fpath)), get_default_timezone()) 120 | date_updated = watermark.date_updated 121 | if not is_aware(date_updated): 122 | date_updated = make_aware(date_updated, get_default_timezone()) 123 | # only return the old file if things appear to be the same 124 | if modified >= date_updated: 125 | logger.info("Watermark exists and has not changed. Bailing out.") 126 | return url_path 127 | 128 | # make sure the position is in our params for the watermark 129 | params["position"] = pos 130 | 131 | self.create_watermark(target, mark, fpath, **params) 132 | 133 | # send back the URL to the new, watermarked image 134 | return url_path 135 | 136 | def _get_filesystem_path(self, url_path, basedir=settings.MEDIA_ROOT): 137 | """Makes a filesystem path from the specified URL path""" 138 | 139 | if url_path.startswith(settings.MEDIA_URL): 140 | url_path = url_path[len(settings.MEDIA_URL):] # strip media root url 141 | 142 | return os.path.normpath(os.path.join(basedir, url2pathname(url_path))) 143 | 144 | def generate_filename(self, mark, **kwargs): 145 | """Comes up with a good filename for the watermarked image""" 146 | 147 | kwargs = kwargs.copy() 148 | 149 | kwargs["opacity"] = int(kwargs["opacity"] * 100) 150 | kwargs["st_mtime"] = kwargs["fstat"].st_mtime 151 | kwargs["st_size"] = kwargs["fstat"].st_size 152 | 153 | params = [ 154 | "%(original_basename)s", 155 | "wm", 156 | "w%(watermark)i", 157 | "o%(opacity)i", 158 | "gs%(greyscale)i", 159 | "r%(rotation)i", 160 | "fm%(st_mtime)i", 161 | "fz%(st_size)i", 162 | "p%(position)s", 163 | ] 164 | 165 | scale = kwargs.get("scale", None) 166 | if scale and scale != mark.size: 167 | params.append("_s%i" % (float(kwargs["scale"][0]) / mark.size[0] * 100)) 168 | 169 | if kwargs.get("tile", None): 170 | params.append("_tiled") 171 | 172 | # make thumbnail filename 173 | filename = "%s%s" % ("_".join(params), kwargs["ext"]) 174 | 175 | return filename % kwargs 176 | 177 | def get_url_path(self, basedir, original_basename, ext, name, obscure=True): 178 | """Determines an appropriate watermark path""" 179 | 180 | try: 181 | hash = hashlib.sha1(smart_str(name)).hexdigest() 182 | except TypeError: 183 | hash = hashlib.sha1(smart_str(name).encode("utf-8")).hexdigest() 184 | 185 | # figure out where the watermark would be saved on the filesystem 186 | if obscure is True: 187 | logger.debug("Obscuring original image name: %s => %s" % (name, hash)) 188 | url_path = os.path.join(basedir, hash + ext) 189 | else: 190 | logger.debug("Not obscuring original image name.") 191 | url_path = os.path.join(basedir, hash, original_basename + ext) 192 | 193 | # make sure the destination directory exists 194 | try: 195 | fpath = self._get_filesystem_path(url_path) 196 | os.makedirs(os.path.dirname(fpath)) 197 | except OSError as e: 198 | if e.errno == errno.EEXIST: 199 | pass # not to worry, directory exists 200 | else: 201 | logger.error("Error creating path: %s" % traceback.format_exc()) 202 | raise 203 | else: 204 | logger.debug("Created directory: %s" % os.path.dirname(fpath)) 205 | 206 | return url_path 207 | 208 | def create_watermark(self, target, mark, fpath, quality=QUALITY, **kwargs): 209 | """Create the watermarked image on the filesystem""" 210 | 211 | im = utils.watermark(target, mark, **kwargs) 212 | if not kwargs.get("noalpha", True) is False: 213 | im = im.convert("RGB") 214 | im.save(fpath, quality=quality) 215 | return im 216 | 217 | 218 | @register.filter 219 | def watermark(url, args=""): 220 | """ 221 | Returns the URL to a watermarked copy of the image specified. 222 | 223 | """ 224 | # initialize some variables 225 | args = args.split(",") 226 | 227 | params = dict( 228 | name=args.pop(0), 229 | opacity=0.5, 230 | tile=False, 231 | scale=1.0, 232 | greyscale=False, 233 | rotation=0, 234 | position=None, 235 | noalpha=True, 236 | quality=QUALITY, 237 | obscure=OBSCURE_ORIGINAL, 238 | random_position_once=RANDOM_POSITION_ONCE, 239 | ) 240 | 241 | params["url"] = unquote(url) 242 | 243 | # iterate over all parameters to see what we need to do 244 | for arg in args: 245 | key, value = arg.split("=") 246 | key, value = key.strip(), value.strip() 247 | if key == "position": 248 | params["position"] = value 249 | elif key == "opacity": 250 | params["opacity"] = utils._percent(value) 251 | elif key == "tile": 252 | params["tile"] = bool(int(value)) 253 | elif key == "scale": 254 | params["scale"] = value 255 | elif key == "greyscale": 256 | params["greyscale"] = bool(int(value)) 257 | elif key == "rotation": 258 | params["rotation"] = value 259 | elif key == "noalpha": 260 | params["noalpha"] = bool(int(value)) 261 | elif key == "quality": 262 | params["quality"] = int(value) 263 | elif key == "obscure": 264 | params["obscure"] = bool(int(value)) 265 | elif key == "random_position_once": 266 | params["random_position_once"] = bool(int(value)) 267 | 268 | return Watermarker()(**params) 269 | -------------------------------------------------------------------------------- /watermarker/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-watermark/72c67c9a668b76db5e2f1618a93aef35bf7f9ece/watermarker/tests/__init__.py -------------------------------------------------------------------------------- /watermarker/tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Dummy conftest.py for watermarker. 4 | If you don't know what this is for, just leave it empty. 5 | Read more about conftest.py under: 6 | https://pytest.org/latest/plugins.html 7 | """ 8 | from __future__ import print_function, absolute_import, division 9 | 10 | import pytest 11 | -------------------------------------------------------------------------------- /watermarker/tests/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-watermark/72c67c9a668b76db5e2f1618a93aef35bf7f9ece/watermarker/tests/models.py -------------------------------------------------------------------------------- /watermarker/tests/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-watermark/72c67c9a668b76db5e2f1618a93aef35bf7f9ece/watermarker/tests/overlay.png -------------------------------------------------------------------------------- /watermarker/tests/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 4 | import os 5 | 6 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | 8 | SECRET_KEY = "DUMMY_SECRET_KEY" 9 | 10 | INTERNAL_IPS = [] 11 | 12 | # Application definition 13 | 14 | PROJECT_APPS = ["watermarker.tests", "watermarker"] 15 | 16 | INSTALLED_APPS = [ 17 | "django.contrib.auth", 18 | "django.contrib.contenttypes", 19 | "django.contrib.staticfiles", 20 | ] + PROJECT_APPS 21 | 22 | TEMPLATES = [ 23 | { 24 | "BACKEND": "django.template.backends.django.DjangoTemplates", 25 | "DIRS": [], 26 | "APP_DIRS": True, 27 | "OPTIONS": { 28 | "context_processors": [ 29 | "django.contrib.auth.context_processors.auth", 30 | "django.template.context_processors.debug", 31 | "django.template.context_processors.i18n", 32 | "django.template.context_processors.media", 33 | "django.template.context_processors.request", 34 | "django.template.context_processors.static", 35 | "django.template.context_processors.tz", 36 | "django.contrib.messages.context_processors.messages", 37 | ], 38 | }, 39 | }, 40 | ] 41 | 42 | # Database 43 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 44 | 45 | DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}} 46 | -------------------------------------------------------------------------------- /watermarker/tests/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashu/django-watermark/72c67c9a668b76db5e2f1618a93aef35bf7f9ece/watermarker/tests/test.png -------------------------------------------------------------------------------- /watermarker/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | 5 | from django.test import TestCase 6 | from PIL import Image 7 | 8 | from ..utils import watermark 9 | 10 | 11 | class UtilsTestCase(TestCase): 12 | def setUp(self): 13 | self.im = Image.open(os.path.join(os.path.dirname(__file__), "test.png")) 14 | self.mark = Image.open(os.path.join(os.path.dirname(__file__), "overlay.png")) 15 | 16 | def test_tile(self): 17 | watermark(self.im, self.mark, tile=True, opacity=0.5, rotation=30).save( 18 | os.path.join(os.path.dirname(__file__), "test1.png") 19 | ) 20 | 21 | def test_scale(self): 22 | watermark(self.im, self.mark, scale="F").save(os.path.join(os.path.dirname(__file__), "test2.png")) 23 | 24 | def test_grayscale(self): 25 | watermark(self.im, self.mark, position=(100, 100), opacity=0.5, greyscale=True, rotation=-45).save( 26 | os.path.join(os.path.dirname(__file__), "test3.png") 27 | ) 28 | 29 | def test_position(self): 30 | watermark(self.im, self.mark, position="C", tile=False, opacity=0.2, scale=2, rotation=30).save( 31 | os.path.join(os.path.dirname(__file__), "test4.png") 32 | ) 33 | -------------------------------------------------------------------------------- /watermarker/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Utilities for applying a watermark to an image using PIL. 4 | 5 | Stolen from http://code.activestate.com/recipes/362879/ 6 | 7 | """ 8 | import re 9 | import random 10 | 11 | from PIL import Image, ImageEnhance 12 | 13 | from .conf import settings 14 | 15 | 16 | def _percent(var): 17 | """ 18 | Just a simple interface to the _val function with a more meaningful name. 19 | """ 20 | return _val(var, True) 21 | 22 | 23 | def _int(var): 24 | """ 25 | Just a simple interface to the _val function with a more meaningful name. 26 | """ 27 | return _val(var) 28 | 29 | 30 | def _val(var, is_percent=False): 31 | """ 32 | Tries to determine the appropriate value of a particular variable that is 33 | passed in. If the value is supposed to be a percentage, a whole integer 34 | will be sought after and then turned into a floating point number between 35 | 0 and 1. If the value is supposed to be an integer, the variable is cast 36 | into an integer. 37 | 38 | """ 39 | try: 40 | if is_percent: 41 | var = float(int(var.strip("%")) / 100.0) 42 | else: 43 | var = int(var) 44 | except ValueError: 45 | raise ValueError("invalid watermark parameter: " + var) 46 | return var 47 | 48 | 49 | def reduce_opacity(img, opacity): 50 | """ 51 | Returns an image with reduced opacity. 52 | """ 53 | assert opacity >= 0 and opacity <= 1 54 | 55 | if img.mode != "RGBA": 56 | img = img.convert("RGBA") 57 | else: 58 | img = img.copy() 59 | 60 | alpha = img.split()[3] 61 | alpha = ImageEnhance.Brightness(alpha).enhance(opacity) 62 | img.putalpha(alpha) 63 | 64 | return img 65 | 66 | 67 | def determine_scale(scale, img, mark): 68 | """ 69 | Scales an image using a specified ratio, 'F' or 'R%%'. If `scale` is 70 | 'F', the image is scaled to be as big as possible to fit in `img` 71 | without falling off the edges. If `scale` is 'R%%', the watermark 72 | resizes to a percentage of minimum size of source image. Returns 73 | the scaled `mark`. 74 | 75 | """ 76 | if scale: 77 | try: 78 | scale = float(scale) 79 | except (ValueError, TypeError): 80 | pass 81 | 82 | if isinstance(scale, str) and scale.upper() == "F": 83 | # scale watermark to full, but preserve the aspect ratio 84 | scale = min(float(img.size[0]) / mark.size[0], float(img.size[1]) / mark.size[1]) 85 | elif isinstance(scale, str) and re.match(r"R\d{1,3}\%", scale.upper()): 86 | # scale watermark to % of source image and preserve the aspect ratio 87 | percentage = float(re.match(r"R(\d{1,3})\%", scale.upper()).group(1)) 88 | scale = ( 89 | min(float(img.size[0]) / mark.size[0], float(img.size[1]) / mark.size[1]) 90 | / 100 * percentage 91 | ) 92 | elif not isinstance(scale, (float, int)): 93 | raise ValueError( 94 | 'Invalid scale value "%s"! Valid values are "F" ' 95 | 'for ratio-preserving scaling, "R%%" for percantage aspect ' 96 | "ratio of source image and floating-point numbers and " 97 | "integers greater than 0." % scale 98 | ) 99 | 100 | # determine the new width and height 101 | w = int(mark.size[0] * float(scale)) 102 | h = int(mark.size[1] * float(scale)) 103 | 104 | # apply the new width and height, and return the new `mark` 105 | return (w, h) 106 | else: 107 | return mark.size 108 | 109 | 110 | def determine_rotation(rotation, mark): 111 | """ 112 | Determines the number of degrees to rotate the watermark image. 113 | """ 114 | if isinstance(rotation, str) and rotation.lower() == "r": 115 | rotation = random.randint(0, 359) 116 | else: 117 | rotation = _int(rotation) 118 | 119 | return rotation 120 | 121 | 122 | def determine_position(position, img, mark): 123 | """ 124 | Options: 125 | TL: top-left 126 | TR: top-right 127 | BR: bottom-right 128 | BL: bottom-left 129 | C: centered 130 | R: random 131 | X%xY%: relative positioning on both the X and Y axes 132 | X%xY: relative positioning on the X axis and absolute positioning on the 133 | Y axis 134 | XxY%: absolute positioning on the X axis and relative positioning on the 135 | Y axis 136 | XxY: absolute positioning on both the X and Y axes 137 | 138 | """ 139 | left = top = 0 140 | 141 | max_left = max(img.size[0] - mark.size[0], 0) 142 | max_top = max(img.size[1] - mark.size[1], 0) 143 | 144 | if not position: 145 | position = "r" 146 | 147 | if isinstance(position, tuple): 148 | left, top = position 149 | elif isinstance(position, str): 150 | position = position.lower() 151 | 152 | # corner positioning 153 | if position in ["tl", "tr", "br", "bl"]: 154 | if "t" in position: 155 | top = 0 156 | elif "b" in position: 157 | top = max_top 158 | if "l" in position: 159 | left = 0 160 | elif "r" in position: 161 | left = max_left 162 | 163 | # center positioning 164 | elif position == "c": 165 | left = int(max_left / 2) 166 | top = int(max_top / 2) 167 | 168 | # random positioning 169 | elif position == "r": 170 | left = random.randint(0, max_left) 171 | top = random.randint(0, max_top) 172 | 173 | # relative or absolute positioning 174 | elif "x" in position: 175 | left, top = position.split("x") 176 | 177 | if "%" in left: 178 | left = max_left * _percent(left) 179 | else: 180 | left = _int(left) 181 | 182 | if "%" in top: 183 | top = max_top * _percent(top) 184 | else: 185 | top = _int(top) 186 | 187 | return int(left), int(top) 188 | 189 | 190 | def watermark( 191 | img, 192 | mark, 193 | position=(0, 0), 194 | opacity=1, 195 | scale=1.0, 196 | tile=False, 197 | greyscale=False, 198 | rotation=0, 199 | return_name=False, 200 | **kwargs 201 | ): 202 | """Adds a watermark to an image""" 203 | 204 | if opacity < 1: 205 | mark = reduce_opacity(mark, opacity) 206 | 207 | if not isinstance(scale, tuple): 208 | scale = determine_scale(scale, img, mark) 209 | 210 | if scale[0] != mark.size[0] and scale[1] != mark.size[1]: 211 | mark = mark.resize(scale, resample=Image.ANTIALIAS) 212 | 213 | if greyscale and mark.mode != "LA": 214 | mark = mark.convert("LA") 215 | 216 | rotation = determine_rotation(rotation, mark) 217 | if rotation != 0: 218 | # give some leeway for rotation overlapping 219 | new_w = int(mark.size[0] * 1.5) 220 | new_h = int(mark.size[1] * 1.5) 221 | 222 | new_mark = Image.new("RGBA", (new_w, new_h), (0, 0, 0, 0)) 223 | 224 | # center the watermark in the newly resized image 225 | new_l = int((new_w - mark.size[0]) / 2) 226 | new_t = int((new_h - mark.size[1]) / 2) 227 | new_mark.paste(mark, (new_l, new_t)) 228 | 229 | mark = new_mark.rotate(rotation) 230 | 231 | position = determine_position(position, img, mark) 232 | 233 | if img.mode != "RGBA": 234 | img = img.convert("RGBA") 235 | 236 | # make sure we have a tuple for a position now 237 | assert isinstance(position, tuple), 'Invalid position "%s"!' % position 238 | 239 | # create a transparent layer the size of the image and draw the 240 | # watermark in that layer. 241 | layer = Image.new("RGBA", img.size, (0, 0, 0, 0)) 242 | if tile: 243 | first_y = int(position[1] % mark.size[1] - mark.size[1]) 244 | first_x = int(position[0] % mark.size[0] - mark.size[0]) 245 | 246 | for y in range(first_y, img.size[1], mark.size[1]): 247 | for x in range(first_x, img.size[0], mark.size[0]): 248 | layer.paste(mark, (x, y)) 249 | else: 250 | layer.paste(mark, position) 251 | 252 | # composite the watermark with the layer 253 | return Image.composite(layer, img, layer) 254 | --------------------------------------------------------------------------------