├── .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 |
--------------------------------------------------------------------------------