+#!/bin/python3
+# coding: utf-8
+'''
+A Sphinx extension that enables watermarks for HTML output.
+
+Project Source: https://github.com/JoKneeMo/sphinx-watermark
+
+Copyright 2024 - JoKneeMo
+
+Licensed under GNU General Public License version 3 (GPLv3).
+
+Commit 0762fdef2eabead5edf99e393becc2cd5a926f11 and older
+are licensed under Apache License, Version 2.0 and
+copyrighten 2021 by Brian Moss
+
+Original project: https://github.com/kallimachos/sphinxmark
+'''
+
+__author__ = 'JoKneeMo <https://github.com/JoKneeMo>'
+__email__ = '421625+JoKneeMo@users.noreply.github.com'
+__copyright__ = '2024 - JoKneeMo'
+__license__ = 'GPLv3'
+__version__ = '2.0.0'
+__keywords__ = ['Python3', 'Sphinx', 'Extension', 'watermark']
+
+from pathlib import Path
+from shutil import copy
+from string import Template
+from PIL import Image, ImageDraw, ImageFont
+from sphinx.application import Sphinx
+from sphinx.environment import BuildEnvironment
+from sphinx.util import logging
+import collections
+
+logger = logging.getLogger(__name__)
+
+watermark_config = {
+ 'enabled': False,
+ 'selector': {
+ 'type': 'div',
+ 'class': 'body'
+ },
+ 'position': {
+ 'margin': None,
+ 'repeat': True,
+ 'fixed': False
+ },
+ 'image': None,
+ 'text': {
+ 'content': None,
+ 'align': 'center',
+ 'font': 'RubikDistressed',
+ 'color': (255, 0, 0),
+ 'opacity': 40,
+ 'size': 100,
+ 'rotation': 0,
+ 'width': 816,
+ 'spacing': 400,
+ 'border': {
+ 'outline': (255, 0, 0),
+ 'fill': None,
+ 'width': 10,
+ 'padding': 30,
+ 'radius': 20,
+ }
+ }
+}
+
+[docs]def generate_css(app: Sphinx, buildpath: str, imagefile: str) -> str:
+
'''Create CSS file.'''
+
# set default values
+
repeat = 'repeat-y' if watermark_config['position']['repeat'] else 'no-repeat'
+
attachment = 'fixed' if watermark_config['position']['fixed'] else 'scroll'
+
position = 'center'
+
+
if watermark_config['position']['margin'] is not None:
+
css_template = Template('''${selector}.${selector_class} {
+
border-${side}: 100px solid transparent;
+
padding: 15px;
+
-webkit-border-image: url(${image}) 20% round;
+
-o-border-image: url(${image}) 20% round;
+
border-image: url(${image}) 20% 100% repeat;
+
}''')
+
else:
+
css_template = Template('''${selector}.${selector_class} {
+
background-image: url("${image}") !important;
+
background-repeat: ${repeat} !important;
+
background-position: ${position} top !important;
+
background-attachment: ${attachment} !important;
+
}''')
+
+
css = css_template.substitute(
+
selector=watermark_config['selector']['type'],
+
selector_class=watermark_config['selector']['class'],
+
image=imagefile,
+
repeat=repeat,
+
attachment=attachment,
+
position=position,
+
side=watermark_config['position']['margin']
+
)
+
+
logger.debug(f'[watermark] Template: {css}')
+
cssname = 'watermark.css'
+
cssfile = Path(buildpath, cssname)
+
+
with open(cssfile, 'w') as f:
+
f.write(css)
+
+
return cssname
+
+
+[docs]def createimage(app: Sphinx, srcdir: Path, buildpath: Path) -> str:
+
'''Create PNG image from string.'''
+
text_content = watermark_config['text']['content']
+
+
# draw transparent background
+
width = watermark_config['text']['width']
+
height = watermark_config['text']['spacing']
+
img = Image.new('RGBA', (width, height), (255, 255, 255, 0))
+
d = ImageDraw.Draw(img)
+
+
# set font
+
fontfile = str(Path(srcdir,'fonts', watermark_config['text']['font'] + '.ttf'))
+
font = ImageFont.truetype(fontfile, watermark_config['text']['size'])
+
+
# set x y location for text
+
left, top, right, bottom = d.textbbox((-5, -20), text_content, font=font, align=watermark_config['text']['align'])
+
xsize, ysize = (right - left, bottom - top)
+
x = (width / 2) - (xsize / 2)
+
y = (height / 2) - (ysize / 2)
+
logger.debug(f'[watermark] Left: {left}, Right: {right}, Top: {top}, Bottom: {bottom}, xsize: {xsize}, ysize: {ysize}, Width: {width}, Height: {height}, x: {x}, y: {y}')
+
+
# add text to image
+
d.text((x-5, y-25), text_content, font=font, align=watermark_config['text']['align'], fill=watermark_config['text']['color'])
+
+
# Add border around text
+
padding = watermark_config['text']['border']['padding']
+
border_x0 = x - padding
+
border_y0 = y - padding
+
border_x1 = x + xsize + padding - 5
+
border_y1 = y + ysize + padding - 10
+
d.rounded_rectangle([border_x0, border_y0, border_x1, border_y1], radius=watermark_config['text']['border']['radius'], fill=watermark_config['text']['border']['fill'], width=watermark_config['text']['border']['width'], outline=watermark_config['text']['border']['outline'])
+
+
# set opacity
+
img2 = img.copy()
+
img2.putalpha(watermark_config['text']['opacity'])
+
img.paste(img2, img)
+
+
# rotate image
+
img = img.rotate(watermark_config['text']['rotation'])
+
+
# save image
+
imagefile = f'watermark_text.png'
+
imagepath = Path(buildpath, imagefile)
+
img.save(imagepath, 'PNG')
+
logger.debug(f"[watermark] Image saved to: {imagepath}")
+
+
return imagefile
+
+
+[docs]def get_image(app: Sphinx) -> tuple:
+
'''Get image file.'''
+
srcdir = Path(__file__).parent.resolve()
+
confdir = str(app.confdir)
+
+
if app.config.html_static_path:
+
staticpath = app.config.html_static_path[0]
+
else:
+
staticpath = '_static'
+
+
buildpath = Path(app.outdir, staticpath)
+
+
logger.debug(f"[watermark] static path: {staticpath}")
+
try:
+
buildpath.mkdir()
+
except OSError:
+
if not buildpath.is_dir():
+
raise
+
+
if (watermark_config['image'] is not None) and (watermark_config['text']['content'] is not None):
+
'''Validate selection of image or text'''
+
raise TypeError('image and text.content should not *both* contain a value, remove one and try again')
+
elif (watermark_config['image'] is not None) and (len(watermark_config['image']) >= 1):
+
'''Copy configured image to build output'''
+
imagefile = watermark_config['image']
+
imagepath = Path(confdir, staticpath, imagefile)
+
logger.debug(f"[watermark] Using configured image: {imagepath}")
+
try:
+
copy(imagepath, buildpath)
+
except FileNotFoundError:
+
logger.error('Unable to copy image')
+
raise
+
elif (watermark_config['text']['content'] is not None) and (len(watermark_config['text']['content']) >= 1):
+
'''Creating an image from text content'''
+
imagefile = createimage(app, srcdir, buildpath)
+
logger.debug(f"[watermark] Image: {imagefile}")
+
else:
+
'''Final fail'''
+
raise TypeError("Something went awry, it's likely that either image or text.content has a length <= 1.")
+
+
return (buildpath, imagefile)
+
+
+[docs]def generate_watermark(app: Sphinx, env: BuildEnvironment) -> None:
+
'''Generate watermark'''
+
+
if isinstance(app.config.watermark['selector'], str):
+
selector_class = str(app.config.watermark['selector'])
+
app.config.watermark['selector'] = {'class': selector_class}
+
logger.debug(f"[watermark] Selector was a string: {app.config.watermark['selector']}")
+
+
if isinstance(app.config.watermark['text'], str):
+
text_content = str(app.config.watermark['text'])
+
app.config.watermark['text'] = {'content': text_content}
+
logger.debug(f"[watermark] Text was a string: {app.config.watermark['text']}")
+
+
deep_update(watermark_config, app.config.watermark)
+
+
logger.debug(f"[watermark] App Config: {app.config.watermark}")
+
logger.debug(f"[watermark] Watermark Config: {watermark_config}")
+
+
if watermark_config['enabled'] is True:
+
logger.info('Adding watermark...', nonl=True)
+
try:
+
buildpath, imagefile = get_image(app)
+
cssname = generate_css(app, buildpath, imagefile)
+
app.add_css_file(cssname)
+
logger.info(' done')
+
except Exception as e:
+
logger.warning(f"Failed to add watermark: {e}")
+
else:
+
logger.info('Not loading watermark')
+
+
return
+
+[docs]def deep_update(source, overrides):
+
'''
+
Update a nested dictionary or similar mapping.
+
Modify ``source`` in place.
+
'''
+
for key, value in overrides.items():
+
if isinstance(value, collections.abc.Mapping) and value:
+
returned = deep_update(source.get(key, {}), value)
+
source[key] = returned
+
else:
+
source[key] = overrides[key]
+
return source
+
+
+[docs]def setup(app: Sphinx) -> dict:
+
'''Configure setup for Sphinx extension.
+
+
:param app: Sphinx application context.
+
'''
+
app.add_config_value('watermark', watermark_config, 'html')
+
app.connect('env-updated', generate_watermark)
+
+
return {
+
'version': __version__,
+
'parallel_read_safe': True,
+
'parallel_write_safe': True,
+
}
+
+
+
+
+
+
+
+