Chore: Drop ImageMagik as a dependency (#14440)

* Mark the thumbnail constants Final

* Comment each pdftoppm option

* Feature/Chore: Replaces all usages of ImageMagik's convert binary with a simple Pillow based helper

This removes ImageMagik as a dependency entirly and all documentation is cleaned up from it.
We no longer need to install it, have a policy for it to allow PDFs and no longer shell
out to it either.

* Fix: Save WebP images losslessly when removing the alpha channel

Pillow's default WebP save is lossy, so a WebP with an alpha channel came
back from remove_alpha_channel with slightly shifted pixel values before
it reached OCR. Save WebP lossless; other formats are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Trenton HandClaude Sonnet 5.5 authored and GitHub committed 2026-10-20 11:22:43 -07:00
1 parent a05afd654c
commit c089bba32c
14 files changed
+151 -169

No files matched your search

+101
View File
@@ -1,6 +1,11 @@
from pathlib import Path
import pytest
import pytest_mock
from PIL import Image
from documents.utils import QuerySetStream
from documents.utils import remove_alpha_channel
class TestQuerySetStream:
@@ -33,3 +38,99 @@ class TestQuerySetStream:
# the thing being verified here.
mock_queryset.count.assert_called_with()
mock_queryset.iterator.assert_called_once_with(chunk_size=1000)
class TestRemoveAlphaChannel:
@pytest.mark.parametrize(
("image_format", "mode", "expected_mode", "color", "expected_pixel"),
[
pytest.param(
"PNG",
"RGBA",
"RGB",
(10, 20, 30, 0),
(10, 20, 30),
id="png-rgba",
),
pytest.param(
"TIFF",
"RGBA",
"RGB",
(10, 20, 30, 128),
(10, 20, 30),
id="tiff-rgba",
),
pytest.param("PNG", "LA", "L", (40, 0), 40, id="png-la"),
],
)
def test_drops_alpha_and_keeps_format(
self,
tmp_path: Path,
image_format: str,
mode: str,
expected_mode: str,
color: tuple[int, ...],
expected_pixel: int | tuple[int, ...],
) -> None:
"""
GIVEN:
- An image with an alpha channel
WHEN:
- The alpha channel is removed to a destination without an extension
THEN:
- The destination has the matching opaque mode, the same size and
format, and the color values are kept rather than composited
"""
source = tmp_path / "source"
Image.new(mode, (8, 6), color).save(source, format=image_format)
dest = tmp_path / "no-alpha"
result = remove_alpha_channel(source, dest)
assert result == dest
with Image.open(dest) as out:
assert out.mode == expected_mode
assert out.size == (8, 6)
assert out.format == image_format
assert out.getpixel((0, 0)) == expected_pixel
def test_keeps_every_frame_of_a_multi_page_tiff(self, tmp_path: Path) -> None:
"""
GIVEN:
- A multi-page TIFF where every page has an alpha channel
WHEN:
- The alpha channel is removed
THEN:
- All pages are kept and none has an alpha channel
"""
source = tmp_path / "multi.tiff"
first = Image.new("RGBA", (8, 6), (255, 0, 0, 50))
rest = [Image.new("RGBA", (8, 6), (0, 255, 0, 50)) for _ in range(2)]
first.save(source, save_all=True, append_images=rest)
dest = tmp_path / "multi-no-alpha.tiff"
remove_alpha_channel(source, dest)
with Image.open(dest) as out:
assert out.n_frames == 3
for index in range(out.n_frames):
out.seek(index)
assert out.mode == "RGB"
def test_keeps_dpi(self, tmp_path: Path) -> None:
"""
GIVEN:
- An alpha image with a DPI set
WHEN:
- The alpha channel is removed
THEN:
- The DPI is unchanged, since OCR scaling depends on it
"""
source = tmp_path / "source.png"
Image.new("RGBA", (8, 6)).save(source, dpi=(300, 300))
dest = tmp_path / "dest.png"
remove_alpha_channel(source, dest)
with Image.open(dest) as out:
assert round(out.info["dpi"][0]) == 300