diffpdf 0.3.1__tar.gz → 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {diffpdf-0.3.1 → diffpdf-1.0.0}/.github/dependabot.yml +1 -1
- diffpdf-1.0.0/.github/workflows/build.yml +70 -0
- diffpdf-1.0.0/.vscode/extensions.json +16 -0
- diffpdf-1.0.0/.vscode/settings.json +12 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/PKG-INFO +37 -17
- {diffpdf-0.3.1 → diffpdf-1.0.0}/README.md +31 -7
- {diffpdf-0.3.1 → diffpdf-1.0.0}/hooks/pre-commit +29 -15
- diffpdf-1.0.0/pyproject.toml +75 -0
- diffpdf-1.0.0/src/diffpdf/__init__.py +49 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/src/diffpdf/cli.py +13 -10
- diffpdf-1.0.0/src/diffpdf/logger.py +24 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/src/diffpdf/page_check.py +3 -1
- diffpdf-1.0.0/src/diffpdf/py.typed +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/src/diffpdf/text_check.py +3 -1
- {diffpdf-0.3.1 → diffpdf-1.0.0}/src/diffpdf/visual_check.py +8 -2
- diffpdf-1.0.0/tests/test_api.py +30 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/test_cli.py +13 -15
- diffpdf-1.0.0/uv.lock +694 -0
- diffpdf-0.3.1/.github/workflows/build.yml +0 -35
- diffpdf-0.3.1/.vscode/settings.json +0 -7
- diffpdf-0.3.1/pyproject.toml +0 -55
- diffpdf-0.3.1/src/diffpdf/__init__.py +0 -27
- diffpdf-0.3.1/src/diffpdf/comparators.py +0 -31
- diffpdf-0.3.1/src/diffpdf/logger.py +0 -45
- diffpdf-0.3.1/tests/test_api.py +0 -12
- diffpdf-0.3.1/tests/test_comparators.py +0 -48
- {diffpdf-0.3.1 → diffpdf-1.0.0}/.github/workflows/pypi-publish.yml +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/.gitignore +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/LICENSE +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/MANIFEST.in +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/src/diffpdf/hash_check.py +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/fail/1-letter-diff-A.pdf +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/fail/1-letter-diff-B.pdf +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/fail/major-color-diff-A.pdf +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/fail/major-color-diff-B.pdf +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/fail/page-count-diff-A.pdf +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/fail/page-count-diff-B.pdf +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/pass/hash-diff-A.pdf +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/pass/hash-diff-B.pdf +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/pass/identical-A.pdf +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/pass/identical-B.pdf +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/pass/minor-color-diff-A.pdf +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/pass/minor-color-diff-B.pdf +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/pass/multiplatform-diff-A.pdf +0 -0
- {diffpdf-0.3.1 → diffpdf-1.0.0}/tests/assets/pass/multiplatform-diff-B.pdf +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: Build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
# Tests compatibility across Ubuntu/Windows and Python/package version extremes
|
|
14
|
+
os: [ubuntu-latest, windows-latest]
|
|
15
|
+
dependencies: [locked, oldest]
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v6
|
|
19
|
+
|
|
20
|
+
# When searching for a system Python version, uv will use the first compatible version - not the newest version.
|
|
21
|
+
# Therefore, make sure the newest version is available on the runner.
|
|
22
|
+
- name: Install Python
|
|
23
|
+
uses: actions/setup-python@v6
|
|
24
|
+
with:
|
|
25
|
+
python-version: 3.x
|
|
26
|
+
|
|
27
|
+
# At the time of writing there is no way to force uv select the lowest version of Python.
|
|
28
|
+
# Therefore, extract this from pyproject.toml
|
|
29
|
+
# More info: https://github.com/astral-sh/uv/issues/16333
|
|
30
|
+
- name: Extract minimum Python version from pyproject.toml
|
|
31
|
+
if: matrix.dependencies == 'oldest'
|
|
32
|
+
id: python-version
|
|
33
|
+
shell: bash
|
|
34
|
+
run: |
|
|
35
|
+
MIN_PY=$(grep 'requires-python' pyproject.toml | sed -E 's/[^0-9.]//g')
|
|
36
|
+
echo "min=$MIN_PY" >> $GITHUB_OUTPUT
|
|
37
|
+
|
|
38
|
+
- name: Install uv
|
|
39
|
+
uses: astral-sh/setup-uv@v7
|
|
40
|
+
with:
|
|
41
|
+
version: "latest"
|
|
42
|
+
|
|
43
|
+
- name: Install locked dependencies
|
|
44
|
+
if: matrix.dependencies == 'locked'
|
|
45
|
+
run: uv sync --locked
|
|
46
|
+
|
|
47
|
+
- name: Install oldest dependencies
|
|
48
|
+
if: matrix.dependencies == 'oldest'
|
|
49
|
+
run: uv sync --resolution lowest --python ${{ steps.python-version.outputs.min }}
|
|
50
|
+
|
|
51
|
+
- name: Run ruff (check)
|
|
52
|
+
if: matrix.dependencies == 'locked'
|
|
53
|
+
run: uv run ruff check
|
|
54
|
+
|
|
55
|
+
- name: Run ruff (format)
|
|
56
|
+
if: matrix.dependencies == 'locked'
|
|
57
|
+
run: uv run ruff format --check
|
|
58
|
+
|
|
59
|
+
- name: Run ty
|
|
60
|
+
if: matrix.dependencies == 'locked'
|
|
61
|
+
run: uv run ty check --output-format github
|
|
62
|
+
|
|
63
|
+
- name: Run pytest
|
|
64
|
+
run: uv run pytest
|
|
65
|
+
|
|
66
|
+
- name: Upload coverage reports to Codecov
|
|
67
|
+
if: matrix.os == 'ubuntu-latest' && matrix.dependencies == 'locked'
|
|
68
|
+
uses: codecov/codecov-action@v5
|
|
69
|
+
with:
|
|
70
|
+
token: ${{ secrets.CODECOV_TOKEN }}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
|
3
|
+
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
|
4
|
+
|
|
5
|
+
// List of extensions which should be recommended for users of this workspace.
|
|
6
|
+
"recommendations": [
|
|
7
|
+
"ms-python.python",
|
|
8
|
+
"charliermarsh.ruff",
|
|
9
|
+
"ryanluker.vscode-coverage-gutters",
|
|
10
|
+
"astral-sh.ty"
|
|
11
|
+
],
|
|
12
|
+
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
|
13
|
+
"unwantedRecommendations": [
|
|
14
|
+
"ms-python.vscode-pylance"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Format with Ruff on save & sort imports
|
|
3
|
+
"editor.formatOnSave": true,
|
|
4
|
+
"editor.defaultFormatter": "charliermarsh.ruff",
|
|
5
|
+
"editor.codeActionsOnSave": {
|
|
6
|
+
"source.organizeImports.ruff": "explicit"
|
|
7
|
+
},
|
|
8
|
+
|
|
9
|
+
// Configure Pytest
|
|
10
|
+
"python.testing.unittestEnabled": false,
|
|
11
|
+
"python.testing.pytestEnabled": true,
|
|
12
|
+
}
|
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diffpdf
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: A tool for comparing PDF files
|
|
5
5
|
Project-URL: Homepage, https://github.com/JustusRijke/DiffPDF
|
|
6
6
|
Project-URL: Issues, https://github.com/JustusRijke/DiffPDF/issues
|
|
7
7
|
Author-email: Justus Rijke <justusrijke@gmail.com>
|
|
8
8
|
License-Expression: MIT
|
|
9
9
|
License-File: LICENSE
|
|
10
|
-
Classifier: Development Status ::
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
11
|
Classifier: Operating System :: Microsoft :: Windows
|
|
12
12
|
Classifier: Operating System :: POSIX :: Linux
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
|
|
15
|
-
Requires-
|
|
16
|
-
Requires-Dist:
|
|
14
|
+
Classifier: Typing :: Typed
|
|
15
|
+
Requires-Python: >=3.10.0
|
|
16
|
+
Requires-Dist: click>=8
|
|
17
17
|
Requires-Dist: pillow>=10.0.0
|
|
18
|
-
Requires-Dist: pixelmatch-fast>=1.1
|
|
18
|
+
Requires-Dist: pixelmatch-fast>=1.3.1
|
|
19
19
|
Requires-Dist: pymupdf>=1.23.0
|
|
20
|
-
Provides-Extra: dev
|
|
21
|
-
Requires-Dist: pytest; extra == 'dev'
|
|
22
|
-
Requires-Dist: pytest-cov; extra == 'dev'
|
|
23
|
-
Requires-Dist: ruff; extra == 'dev'
|
|
24
20
|
Description-Content-Type: text/markdown
|
|
25
21
|
|
|
26
22
|
# DiffPDF
|
|
@@ -29,6 +25,8 @@ Description-Content-Type: text/markdown
|
|
|
29
25
|
[](https://codecov.io/gh/JustusRijke/DiffPDF)
|
|
30
26
|
[](https://www.python.org/downloads/)
|
|
31
27
|
[](LICENSE)
|
|
28
|
+
[](https://pypi.org/project/DiffPDF/)
|
|
29
|
+
[](https://pypi.org/project/DiffPDF/)
|
|
32
30
|
|
|
33
31
|
CLI tool for detecting structural, textual, and visual differences between PDF files, for use in automatic regression tests.
|
|
34
32
|
|
|
@@ -45,6 +43,8 @@ Each stage only runs if all previous stages pass.
|
|
|
45
43
|
|
|
46
44
|
## Installation
|
|
47
45
|
|
|
46
|
+
Install Python (v3.10 or higher) and install the package:
|
|
47
|
+
|
|
48
48
|
```bash
|
|
49
49
|
pip install diffpdf
|
|
50
50
|
```
|
|
@@ -59,8 +59,7 @@ Options:
|
|
|
59
59
|
--threshold FLOAT Pixelmatch threshold (0.0-1.0)
|
|
60
60
|
--dpi INTEGER Render resolution
|
|
61
61
|
--output-dir DIRECTORY Diff image output directory (optional, if not specified no diff images are saved)
|
|
62
|
-
-v, --verbose Increase verbosity
|
|
63
|
-
--save-log Write log output to log.txt
|
|
62
|
+
-v, --verbose Increase verbosity
|
|
64
63
|
--version Show the version and exit.
|
|
65
64
|
--help Show this message and exit.
|
|
66
65
|
```
|
|
@@ -79,16 +78,37 @@ from diffpdf import diffpdf
|
|
|
79
78
|
# Basic usage (no diff images saved)
|
|
80
79
|
diffpdf("reference.pdf", "actual.pdf")
|
|
81
80
|
|
|
82
|
-
# With options (save diff images to ./output directory)
|
|
83
|
-
diffpdf("reference.pdf", "actual.pdf", output_dir="./output",
|
|
81
|
+
# With options (save diff images to ./output directory, extract higher quality images)
|
|
82
|
+
diffpdf("reference.pdf", "actual.pdf", output_dir="./output", dpi=300)
|
|
84
83
|
```
|
|
85
84
|
|
|
86
85
|
## Development
|
|
87
86
|
|
|
87
|
+
Install [uv](https://github.com/astral-sh/uv?tab=readme-ov-file#installation). Then, install dependencies & activate the automatically generated virtual environment:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
uv sync --locked
|
|
91
|
+
source .venv/bin/activate
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Skip `--locked` to use the newest dependencies (this might modify `uv.lock`)
|
|
95
|
+
|
|
96
|
+
Run tests:
|
|
97
|
+
```bash
|
|
98
|
+
pytest
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Check code quality:
|
|
102
|
+
```bash
|
|
103
|
+
ruff check
|
|
104
|
+
ruff format --check
|
|
105
|
+
ty check
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Better yet, install the [pre-commit](.git/hooks/pre-commit) hook, which runs code quality checks before every commit:
|
|
88
109
|
```bash
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
ruff check .
|
|
110
|
+
cp hooks/pre-commit .git/hooks/pre-commit
|
|
111
|
+
chmod +x .git/hooks/pre-commit
|
|
92
112
|
```
|
|
93
113
|
|
|
94
114
|
## Acknowledgements
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
[](https://codecov.io/gh/JustusRijke/DiffPDF)
|
|
5
5
|
[](https://www.python.org/downloads/)
|
|
6
6
|
[](LICENSE)
|
|
7
|
+
[](https://pypi.org/project/DiffPDF/)
|
|
8
|
+
[](https://pypi.org/project/DiffPDF/)
|
|
7
9
|
|
|
8
10
|
CLI tool for detecting structural, textual, and visual differences between PDF files, for use in automatic regression tests.
|
|
9
11
|
|
|
@@ -20,6 +22,8 @@ Each stage only runs if all previous stages pass.
|
|
|
20
22
|
|
|
21
23
|
## Installation
|
|
22
24
|
|
|
25
|
+
Install Python (v3.10 or higher) and install the package:
|
|
26
|
+
|
|
23
27
|
```bash
|
|
24
28
|
pip install diffpdf
|
|
25
29
|
```
|
|
@@ -34,8 +38,7 @@ Options:
|
|
|
34
38
|
--threshold FLOAT Pixelmatch threshold (0.0-1.0)
|
|
35
39
|
--dpi INTEGER Render resolution
|
|
36
40
|
--output-dir DIRECTORY Diff image output directory (optional, if not specified no diff images are saved)
|
|
37
|
-
-v, --verbose Increase verbosity
|
|
38
|
-
--save-log Write log output to log.txt
|
|
41
|
+
-v, --verbose Increase verbosity
|
|
39
42
|
--version Show the version and exit.
|
|
40
43
|
--help Show this message and exit.
|
|
41
44
|
```
|
|
@@ -54,16 +57,37 @@ from diffpdf import diffpdf
|
|
|
54
57
|
# Basic usage (no diff images saved)
|
|
55
58
|
diffpdf("reference.pdf", "actual.pdf")
|
|
56
59
|
|
|
57
|
-
# With options (save diff images to ./output directory)
|
|
58
|
-
diffpdf("reference.pdf", "actual.pdf", output_dir="./output",
|
|
60
|
+
# With options (save diff images to ./output directory, extract higher quality images)
|
|
61
|
+
diffpdf("reference.pdf", "actual.pdf", output_dir="./output", dpi=300)
|
|
59
62
|
```
|
|
60
63
|
|
|
61
64
|
## Development
|
|
62
65
|
|
|
66
|
+
Install [uv](https://github.com/astral-sh/uv?tab=readme-ov-file#installation). Then, install dependencies & activate the automatically generated virtual environment:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv sync --locked
|
|
70
|
+
source .venv/bin/activate
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Skip `--locked` to use the newest dependencies (this might modify `uv.lock`)
|
|
74
|
+
|
|
75
|
+
Run tests:
|
|
76
|
+
```bash
|
|
77
|
+
pytest
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Check code quality:
|
|
81
|
+
```bash
|
|
82
|
+
ruff check
|
|
83
|
+
ruff format --check
|
|
84
|
+
ty check
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Better yet, install the [pre-commit](.git/hooks/pre-commit) hook, which runs code quality checks before every commit:
|
|
63
88
|
```bash
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
ruff check .
|
|
89
|
+
cp hooks/pre-commit .git/hooks/pre-commit
|
|
90
|
+
chmod +x .git/hooks/pre-commit
|
|
67
91
|
```
|
|
68
92
|
|
|
69
93
|
## Acknowledgements
|
|
@@ -38,22 +38,36 @@ EOF
|
|
|
38
38
|
exit 1
|
|
39
39
|
fi
|
|
40
40
|
|
|
41
|
+
# Check if uv is installed
|
|
42
|
+
uv help -q
|
|
43
|
+
if [ $? -ne 0 ]; then
|
|
44
|
+
echo "'uv' is not installed or not in your PATH"
|
|
45
|
+
exit 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# Check if the project is synced
|
|
49
|
+
uv lock --check -q
|
|
50
|
+
if [ $? -ne 0 ]; then
|
|
51
|
+
echo "Error: Environment is out of sync. Please run 'uv sync'."
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
41
55
|
# Ruff checks
|
|
42
|
-
ruff check
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
fi
|
|
52
|
-
if [ $FORMAT_EXIT -ne 0 ]; then
|
|
53
|
-
echo "Ruff found formatting issues. Run: ruff format"
|
|
54
|
-
fi
|
|
56
|
+
uv run ruff check -q
|
|
57
|
+
if [ $? -ne 0 ]; then
|
|
58
|
+
echo "Ruff found linting errors. Please run 'ruff check --fix'"
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
uv run ruff format --check -q
|
|
63
|
+
if [ $? -ne 0 ]; then
|
|
64
|
+
echo "Ruff found formatting issues. Please run 'ruff format'"
|
|
55
65
|
exit 1
|
|
56
66
|
fi
|
|
57
67
|
|
|
58
|
-
#
|
|
59
|
-
|
|
68
|
+
# Type annotation checks
|
|
69
|
+
uv run ty check -q
|
|
70
|
+
if [ $? -ne 0 ]; then
|
|
71
|
+
echo "Found type annotation errors. Please run 'ty check'"
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"hatchling",
|
|
4
|
+
"hatch-vcs",
|
|
5
|
+
]
|
|
6
|
+
build-backend = "hatchling.build"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "diffpdf"
|
|
10
|
+
dynamic = ["version"]
|
|
11
|
+
description = "A tool for comparing PDF files"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
license-files = ["LICENSE"]
|
|
15
|
+
authors = [{name = "Justus Rijke", email="justusrijke@gmail.com"}]
|
|
16
|
+
requires-python = ">=3.10.0"
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Development Status :: 5 - Production/Stable",
|
|
20
|
+
"Operating System :: Microsoft :: Windows",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"click>=8",
|
|
26
|
+
"pymupdf>=1.23.0",
|
|
27
|
+
"pixelmatch-fast>=1.3.1",
|
|
28
|
+
"Pillow>=10.0.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/JustusRijke/DiffPDF"
|
|
33
|
+
Issues = "https://github.com/JustusRijke/DiffPDF/issues"
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=9",
|
|
38
|
+
"pytest-cov>=6",
|
|
39
|
+
"ruff>=0.10",
|
|
40
|
+
"ty>=0.0.8",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
diffpdf = "diffpdf.cli:cli"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.version]
|
|
47
|
+
source = "vcs"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.version.raw-options]
|
|
50
|
+
local_scheme = "no-local-version"
|
|
51
|
+
|
|
52
|
+
[tool.pytest]
|
|
53
|
+
strict = true
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
filterwarnings = ["error"] # Treat all warnings as errors (e.g., deprecation warnings)
|
|
56
|
+
addopts = [
|
|
57
|
+
"-v", # Verbose output
|
|
58
|
+
"--cov", # Enable coverage
|
|
59
|
+
"--cov-branch", # Make sure to cover every decision branch
|
|
60
|
+
"--cov-report=term-missing", # Report which lines aren't covered
|
|
61
|
+
"--cov-report=xml", # Dump to XML for Codecov
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
extend-select = [
|
|
66
|
+
"I", # Sort imports
|
|
67
|
+
"ANN", # Enforce type annotations
|
|
68
|
+
"PT", # Common style issues or inconsistencies with pytest-based tests
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint.isort]
|
|
72
|
+
combine-as-imports = true # Combines "as" imports on the same line
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint.per-file-ignores]
|
|
75
|
+
"tests/**" = ["ANN"] # Do not enforce type annotations for tests
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from importlib.metadata import version
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from .hash_check import check_hash
|
|
5
|
+
from .logger import setup_logging
|
|
6
|
+
from .page_check import check_page_counts
|
|
7
|
+
from .text_check import check_text_content
|
|
8
|
+
from .visual_check import check_visual_content
|
|
9
|
+
|
|
10
|
+
__version__ = version("diffpdf")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def diffpdf(
|
|
14
|
+
reference: str | Path,
|
|
15
|
+
actual: str | Path,
|
|
16
|
+
threshold: float = 0.1,
|
|
17
|
+
dpi: int = 96,
|
|
18
|
+
output_dir: str | Path | None = None,
|
|
19
|
+
verbose: bool = False,
|
|
20
|
+
) -> bool:
|
|
21
|
+
ref_path = Path(reference) if isinstance(reference, str) else reference
|
|
22
|
+
actual_path = Path(actual) if isinstance(actual, str) else actual
|
|
23
|
+
out_path = Path(output_dir) if isinstance(output_dir, str) else output_dir
|
|
24
|
+
|
|
25
|
+
logger = setup_logging(verbose)
|
|
26
|
+
|
|
27
|
+
logger.info("[1/4] Checking file hashes...")
|
|
28
|
+
if check_hash(ref_path, actual_path):
|
|
29
|
+
logger.info("Files are identical (hash match)")
|
|
30
|
+
return True
|
|
31
|
+
logger.info("Hashes differ, continuing checks")
|
|
32
|
+
|
|
33
|
+
logger.info("[2/4] Checking page counts...")
|
|
34
|
+
if not check_page_counts(ref_path, actual_path):
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
logger.info("[3/4] Checking text content...")
|
|
38
|
+
if not check_text_content(ref_path, actual_path):
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
logger.info("[4/4] Checking visual content...")
|
|
42
|
+
if not check_visual_content(ref_path, actual_path, threshold, dpi, out_path):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
logger.info("PDFs are equivalent")
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = ["diffpdf", "__version__"]
|
|
@@ -3,7 +3,7 @@ from pathlib import Path
|
|
|
3
3
|
|
|
4
4
|
import click
|
|
5
5
|
|
|
6
|
-
from .
|
|
6
|
+
from . import diffpdf
|
|
7
7
|
from .logger import setup_logging
|
|
8
8
|
|
|
9
9
|
|
|
@@ -25,22 +25,25 @@ from .logger import setup_logging
|
|
|
25
25
|
@click.option(
|
|
26
26
|
"-v",
|
|
27
27
|
"--verbose",
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
help="Increase verbosity (-v for INFO, -vv for DEBUG)",
|
|
28
|
+
is_flag=True,
|
|
29
|
+
help="Increase verbosity",
|
|
31
30
|
)
|
|
32
|
-
@click.option("--save-log", is_flag=True, help="Write log output to log.txt")
|
|
33
31
|
@click.version_option(package_name="diffpdf")
|
|
34
|
-
def cli(
|
|
32
|
+
def cli(
|
|
33
|
+
reference: Path,
|
|
34
|
+
actual: Path,
|
|
35
|
+
threshold: float,
|
|
36
|
+
dpi: int,
|
|
37
|
+
output_dir: Path | None,
|
|
38
|
+
verbose: bool,
|
|
39
|
+
) -> None:
|
|
35
40
|
"""Compare two PDF files for structural, textual, and visual differences."""
|
|
36
|
-
logger = setup_logging(verbosity, save_log)
|
|
37
|
-
logger.debug("Debug logging enabled")
|
|
38
|
-
|
|
39
41
|
try:
|
|
40
|
-
if
|
|
42
|
+
if diffpdf(reference, actual, threshold, dpi, output_dir, verbose):
|
|
41
43
|
sys.exit(0)
|
|
42
44
|
else:
|
|
43
45
|
sys.exit(1)
|
|
44
46
|
except Exception as e: # pragma: no cover
|
|
47
|
+
logger = setup_logging(verbose)
|
|
45
48
|
logger.critical(f"Error: {e}", exc_info=True)
|
|
46
49
|
sys.exit(2)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
LOG_FORMAT = (
|
|
4
|
+
"%(asctime)s %(levelname)-8s %(filename)s:%(lineno)d (%(funcName)s): %(message)s"
|
|
5
|
+
)
|
|
6
|
+
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def setup_logging(verbose: bool) -> logging.Logger:
|
|
10
|
+
if verbose:
|
|
11
|
+
level = logging.INFO
|
|
12
|
+
else:
|
|
13
|
+
level = logging.WARNING
|
|
14
|
+
|
|
15
|
+
formatter = logging.Formatter(LOG_FORMAT, datefmt=DATE_FORMAT)
|
|
16
|
+
|
|
17
|
+
console_handler = logging.StreamHandler()
|
|
18
|
+
console_handler.setFormatter(formatter)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger()
|
|
21
|
+
logger.setLevel(level)
|
|
22
|
+
logger.addHandler(console_handler)
|
|
23
|
+
|
|
24
|
+
return logger
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
import fitz
|
|
@@ -10,7 +11,8 @@ def get_page_count(pdf_path: Path) -> int:
|
|
|
10
11
|
return count
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
def check_page_counts(ref: Path, actual: Path
|
|
14
|
+
def check_page_counts(ref: Path, actual: Path) -> bool:
|
|
15
|
+
logger = logging.getLogger()
|
|
14
16
|
ref_count = get_page_count(ref)
|
|
15
17
|
actual_count = get_page_count(actual)
|
|
16
18
|
|
|
File without changes
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import difflib
|
|
2
|
+
import logging
|
|
2
3
|
import re
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Iterable
|
|
@@ -32,7 +33,8 @@ def generate_diff(
|
|
|
32
33
|
return diff
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
def check_text_content(ref: Path, actual: Path
|
|
36
|
+
def check_text_content(ref: Path, actual: Path) -> bool:
|
|
37
|
+
logger = logging.getLogger()
|
|
36
38
|
# Extract text and remove whitespace
|
|
37
39
|
ref_text = re.sub(r"\s+", " ", extract_text(ref)).strip()
|
|
38
40
|
actual_text = re.sub(r"\s+", " ", extract_text(actual)).strip()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
import fitz
|
|
@@ -21,7 +22,7 @@ def compare_images(
|
|
|
21
22
|
output_path: Path | None,
|
|
22
23
|
) -> bool:
|
|
23
24
|
mismatch_count = pixelmatch(
|
|
24
|
-
ref_img, actual_img,
|
|
25
|
+
ref_img, actual_img, output=output_path, threshold=threshold
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
if mismatch_count > 0:
|
|
@@ -31,8 +32,13 @@ def compare_images(
|
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def check_visual_content(
|
|
34
|
-
ref: Path,
|
|
35
|
+
ref: Path,
|
|
36
|
+
actual: Path,
|
|
37
|
+
threshold: float,
|
|
38
|
+
dpi: int,
|
|
39
|
+
output_dir: Path | None,
|
|
35
40
|
) -> bool:
|
|
41
|
+
logger = logging.getLogger()
|
|
36
42
|
if output_dir is not None:
|
|
37
43
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
38
44
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from diffpdf import diffpdf
|
|
6
|
+
|
|
7
|
+
TEST_ASSETS_DIR = Path(__file__).parent / "assets"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.mark.parametrize(
|
|
11
|
+
("ref_pdf_rel", "actual_pdf_rel", "should_pass"),
|
|
12
|
+
[
|
|
13
|
+
# Pass cases
|
|
14
|
+
("pass/identical-A.pdf", "pass/identical-B.pdf", True),
|
|
15
|
+
("pass/hash-diff-A.pdf", "pass/hash-diff-B.pdf", True),
|
|
16
|
+
("pass/minor-color-diff-A.pdf", "pass/minor-color-diff-B.pdf", True),
|
|
17
|
+
("pass/multiplatform-diff-A.pdf", "pass/multiplatform-diff-B.pdf", True),
|
|
18
|
+
# Fail cases
|
|
19
|
+
("fail/1-letter-diff-A.pdf", "fail/1-letter-diff-B.pdf", False),
|
|
20
|
+
("fail/major-color-diff-A.pdf", "fail/major-color-diff-B.pdf", False),
|
|
21
|
+
("fail/page-count-diff-A.pdf", "fail/page-count-diff-B.pdf", False),
|
|
22
|
+
],
|
|
23
|
+
)
|
|
24
|
+
def test_api(ref_pdf_rel, actual_pdf_rel, should_pass):
|
|
25
|
+
ref_pdf = TEST_ASSETS_DIR / ref_pdf_rel
|
|
26
|
+
actual_pdf = TEST_ASSETS_DIR / actual_pdf_rel
|
|
27
|
+
|
|
28
|
+
result = diffpdf(ref_pdf, actual_pdf)
|
|
29
|
+
|
|
30
|
+
assert result == should_pass
|
|
@@ -7,30 +7,28 @@ from diffpdf.cli import cli
|
|
|
7
7
|
TEST_ASSETS_DIR = Path(__file__).parent / "assets"
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
def
|
|
10
|
+
def test_cli_with_output_dir():
|
|
11
11
|
runner = CliRunner()
|
|
12
|
-
result = runner.invoke(
|
|
13
|
-
cli,
|
|
14
|
-
[
|
|
15
|
-
str(TEST_ASSETS_DIR / "pass/identical-A.pdf"),
|
|
16
|
-
str(TEST_ASSETS_DIR / "pass/identical-B.pdf"),
|
|
17
|
-
"-v",
|
|
18
|
-
],
|
|
19
|
-
)
|
|
20
|
-
assert result.exit_code == 0
|
|
21
|
-
assert "INFO" in result.output
|
|
22
|
-
assert "DEBUG" not in result.output
|
|
23
12
|
|
|
13
|
+
with runner.isolated_filesystem():
|
|
14
|
+
ref_pdf = str(TEST_ASSETS_DIR / "fail/major-color-diff-A.pdf")
|
|
15
|
+
actual_pdf = str(TEST_ASSETS_DIR / "fail/major-color-diff-B.pdf")
|
|
16
|
+
|
|
17
|
+
result = runner.invoke(cli, [ref_pdf, actual_pdf, "--output-dir", "./diff"])
|
|
18
|
+
|
|
19
|
+
assert result.exit_code == 1
|
|
20
|
+
assert Path("./diff").exists()
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
|
|
23
|
+
def test_verbose_flag():
|
|
26
24
|
runner = CliRunner()
|
|
27
25
|
result = runner.invoke(
|
|
28
26
|
cli,
|
|
29
27
|
[
|
|
30
28
|
str(TEST_ASSETS_DIR / "pass/identical-A.pdf"),
|
|
31
29
|
str(TEST_ASSETS_DIR / "pass/identical-B.pdf"),
|
|
32
|
-
"-
|
|
30
|
+
"-v",
|
|
33
31
|
],
|
|
34
32
|
)
|
|
35
33
|
assert result.exit_code == 0
|
|
36
|
-
assert "
|
|
34
|
+
assert "INFO" in result.output
|