diff --git a/.github/workflows/lint-and-test.yml b/.github/workflows/lint-and-test.yml new file mode 100644 index 0000000..dcc5d7c --- /dev/null +++ b/.github/workflows/lint-and-test.yml @@ -0,0 +1,41 @@ +name: Lint and Test 🧪 + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + push: + branches: + - dev + - master + +jobs: + lint-black: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: psf/black@stable + lint-isort: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: isort/isort-action@v1 + test: + name: Run tests 🧪 + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Install dependencies + run: | + pip install -r requirements.txt + sudo apt-get install -y poppler-utils + - name: Run pytest + run: | + pip install pytest pytest-cov + pytest --cov=src --cov-report=xml --cov-report=html \ No newline at end of file diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..bf7b9fb --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,2 @@ +[settings] +profile=black \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index d7dcee7..bb42d33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,3 +31,6 @@ local_scheme = "no-local-version" [tool.setuptools.dynamic] dependencies = { file = ["requirements.txt"] } + +[tool.pytest.ini_options] +pythonpath = "src" diff --git a/src/pdf_watermark/options.py b/src/pdf_watermark/options.py index c53f262..7a7d9e0 100644 --- a/src/pdf_watermark/options.py +++ b/src/pdf_watermark/options.py @@ -10,7 +10,7 @@ class DrawingOptions: def __init__( self, watermark: str, - opacity, + opacity: float, angle: float, text_color: str, text_font: str, diff --git a/src/pdf_watermark/watermark.py b/src/pdf_watermark/watermark.py index 4bed5db..5ce1949 100644 --- a/src/pdf_watermark/watermark.py +++ b/src/pdf_watermark/watermark.py @@ -11,6 +11,24 @@ ) +class DEFAULTS: + angle = 45 + dpi = 300 + horizontal_alignment = "center" + horizontal_boxes = 3 + image_scale = 1 + margin = False + opacity = 0.1 + save_as_image = False + text_color = "#000000" + text_font = "Helvetica" + text_size = 12 + unselectable = False + vertical_boxes = 6 + x = 0.5 + y = 0.5 + + def generic_watermark_parameters(f): @wraps(f) @click.argument("file") @@ -26,62 +44,62 @@ def generic_watermark_parameters(f): "--opacity", type=float, help="Watermark opacity between 0 (invisible) and 1 (no transparency).", - default=0.1, + default=DEFAULTS.opacity, ) @click.option( "-a", "--angle", type=float, help="Watermark inclination in degrees.", - default=45, + default=DEFAULTS.angle, ) @click.option( "-tc", "--text-color", type=str, help="Text color in hexadecimal format, e.g. #000000.", - default="#000000", + default=DEFAULTS.text_color, ) @click.option( "-tf", "--text-font", type=str, help="Text font to use. Supported fonts are those supported by reportlab.", - default="Helvetica", + default=DEFAULTS.text_font, ) @click.option( "-ts", "--text-size", type=int, help="Text font size.", - default=12, + default=DEFAULTS.text_size, ) @click.option( "--unselectable", type=bool, is_flag=True, help="Make the watermark text unselectable. This works by drawing the text as an image, and thus results in a larger file size.", - default=False, + default=DEFAULTS.unselectable, ) @click.option( "-is", "--image-scale", type=float, help="Scale factor for the image. Note that before this factor is applied, the image is already scaled down to fit in the boxes.", - default=1, + default=DEFAULTS.image_scale, ) @click.option( "--save-as-image", type=bool, is_flag=True, help="Convert each PDF page to an image. This makes removing the watermark more difficult but also increases the file size.", - default=False, + default=DEFAULTS.save_as_image, ) @click.option( "--dpi", type=int, help="DPI to use when saving the PDF as an image.", - default=300, + default=DEFAULTS.dpi, ) def wrapper(*args, **kwargs): return f(*args, **kwargs) @@ -105,21 +123,21 @@ def cli(): "--y", type=float, help="Position of the watermark with respect to the vertical direction. Must be between 0 and 1.", - default=0.5, + default=DEFAULTS.y, ) @click.option( "-x", "--x", type=float, help="Position of the watermark with respect to the horizontal direction. Must be between 0 and 1.", - default=0.5, + default=DEFAULTS.x, ) @click.option( "-ha", "--horizontal-alignment", type=str, help="Alignment of the watermark with respect to the horizontal direction. Can be one of 'left', 'right' and 'center'.", - default="center", + default=DEFAULTS.horizontal_alignment, ) @generic_watermark_parameters def insert( @@ -174,14 +192,14 @@ def insert( "--horizontal-boxes", type=int, help="Number of repetitions of the watermark along the horizontal direction.", - default=3, + default=DEFAULTS.horizontal_boxes, ) @click.option( "-v", "--vertical-boxes", type=int, help="Number of repetitions of the watermark along the vertical direction.", - default=6, + default=DEFAULTS.vertical_boxes, ) @click.option( "-m", @@ -189,7 +207,7 @@ def insert( type=bool, is_flag=True, help="Wether to leave a margin around the page or not. When False (default), the watermark will be cut on the PDF edges.", - default=False, + default=DEFAULTS.margin, ) @generic_watermark_parameters def grid( diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/fixtures/0.pdf b/tests/fixtures/0.pdf new file mode 100644 index 0000000..3c44f85 Binary files /dev/null and b/tests/fixtures/0.pdf differ diff --git a/tests/fixtures/1.pdf b/tests/fixtures/1.pdf new file mode 100644 index 0000000..4692cde Binary files /dev/null and b/tests/fixtures/1.pdf differ diff --git a/tests/fixtures/input.pdf b/tests/fixtures/input.pdf new file mode 100644 index 0000000..f5a5354 Binary files /dev/null and b/tests/fixtures/input.pdf differ diff --git a/tests/test_add_watermark_from_options.py b/tests/test_add_watermark_from_options.py new file mode 100644 index 0000000..90991cb --- /dev/null +++ b/tests/test_add_watermark_from_options.py @@ -0,0 +1,85 @@ +""" +Test the outputs of the CLI with a bunch of different options on simple features. These tests are far from perfect. +""" + +import os + +import numpy as np +import pytest +from pdf2image import convert_from_path + +from pdf_watermark.handler import add_watermark_from_options +from pdf_watermark.options import ( + DrawingOptions, + FilesOptions, + GridOptions, + InsertOptions, +) +from pdf_watermark.watermark import DEFAULTS + +INPUT = "tests/fixtures/input.pdf" +OUTPUT = "output.pdf" +FIXTURES = ["tests/fixtures/0.pdf", "tests/fixtures/1.pdf"] + + +@pytest.fixture(autouse=True) +def cleanup(): + yield + os.remove(OUTPUT) + + +DRAWING_OPTIONS_FIXTURES = [ + DrawingOptions( + watermark="watermark", + opacity=DEFAULTS.opacity, + angle=DEFAULTS.angle, + text_color=DEFAULTS.text_color, + text_font=DEFAULTS.text_font, + text_size=DEFAULTS.text_size, + unselectable=DEFAULTS.unselectable, + image_scale=DEFAULTS.image_scale, + save_as_image=DEFAULTS.save_as_image, + dpi=DEFAULTS.dpi, + ) +] + +FILES_OPTIONS_FIXTURES = [FilesOptions(INPUT, OUTPUT)] + +GRID_OPTIONS_FIXTURES = [ + GridOptions( + horizontal_boxes=DEFAULTS.horizontal_boxes, + vertical_boxes=DEFAULTS.vertical_boxes, + margin=DEFAULTS.margin, + ) +] + +INSERT_OPTIONS_FIXTURES = [ + InsertOptions( + y=DEFAULTS.y, + x=DEFAULTS.x, + horizontal_alignment=DEFAULTS.horizontal_alignment, + ) +] + + +def assert_pdfs_are_close(path_1: str, path_2: str, epsilon: float = 1e-10): + """This function checks that two PDFs are close enough. We chose to convert the PDFs to images and then compare their L1 norms, because other techniques (hashing for instance) might break easily.""" + images_1 = convert_from_path(path_1) + images_2 = convert_from_path(path_2) + + for im1, im2 in zip(images_1, images_2): + assert np.sum(np.abs(np.array(im1) - np.array(im2))) < epsilon + + +def test_add_watermark_from_options(): + index = 0 + for files_options in FILES_OPTIONS_FIXTURES: + for drawing_options in DRAWING_OPTIONS_FIXTURES: + for specific_options in GRID_OPTIONS_FIXTURES + INSERT_OPTIONS_FIXTURES: + add_watermark_from_options( + files_options=files_options, + drawing_options=drawing_options, + specific_options=specific_options, + ) + assert_pdfs_are_close(OUTPUT, FIXTURES[index]) + index += 1