Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify Wand builder and replace Imagemagick and Pillow with it #274

Merged
merged 25 commits into from
Nov 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9a87fb5
modify image__wand builder so that it is like common thumbnail genera…
a523 Oct 19, 2021
2c8c8e6
enable wand builder and disable others
a523 Oct 19, 2021
24463a1
reintroduce white background for file with transparency
a523 Oct 22, 2021
1af97cc
test case for transparent img
a523 Oct 22, 2021
7572911
deprecate Pillow and ImageMagick (command line) builder
a523 Oct 22, 2021
6dbd8d3
support progressive JPEG
a523 Oct 22, 2021
15aeab2
rename
a523 Oct 22, 2021
4720aa4
add doc about how to enable eps format support
a523 Oct 30, 2021
8c08a72
refactor wand build
a523 Oct 31, 2021
bfeb419
format
a523 Oct 31, 2021
a4ccee1
update supported mimetypes
a523 Oct 31, 2021
3aa7c7a
fix supported mimetype table and weight
inkhey Nov 3, 2021
4e4ba63
Merge branch 'develop' of github.com:algoo/preview-generator into wand
inkhey Nov 3, 2021
2a73bd8
reintroduce raw/xcf support
inkhey Nov 3, 2021
5409818
add test for raw file without extension
inkhey Nov 3, 2021
6198027
support file that without extension
a523 Nov 13, 2021
946e66c
fix: merge layers
a523 Nov 13, 2021
e6091f0
make sure transparency turns white
a523 Nov 15, 2021
32ec567
fix test case of heic
a523 Nov 15, 2021
33126d5
misc: add concourse ci
inkhey Nov 15, 2021
445bf66
fix(backend): enforce office mimetype and add better exception
inkhey Nov 16, 2021
40a1450
v0.26
inkhey Nov 16, 2021
a6b1139
fix readme
inkhey Nov 16, 2021
33962c6
fix: Repair loading builders without pytest and make code more explic…
inkhey Nov 16, 2021
edcf565
Merge remote-tracking branch 'algoo/develop' into wand
a523 Nov 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,16 @@ Building ImageMagick with heic support: `Building ImageMagick with heic support`

.. _`Building ImageMagick with heic support`: doc/build_im_with_heic_support.rst

EPS support
~~~~~~~~~~~~

You need to edit the policies of ImageMagick in /etc/ImageMagick-*/policy.xml.
.. code:: xhtml
<policy domain="coder" rights="none" pattern="ESP" />
Just wrap it between <!-- and --> to comment it.

-----
Usage
Expand Down
82 changes: 6 additions & 76 deletions doc/supported_mimetypes.rst

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions preview_generator/preview/builder/cad__vtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from preview_generator.exception import BuilderDependencyNotFound
from preview_generator.exception import UnsupportedMimeType
from preview_generator.extension import mimetypes_storage
from preview_generator.preview.builder.image__pillow import ImagePreviewBuilderPillow # nopep8
from preview_generator.preview.builder.image__wand import ImagePreviewBuilderWand # nopep8
from preview_generator.preview.generic_preview import PreviewBuilder
from preview_generator.utils import ImgDims
from preview_generator.utils import MimetypeMapping
Expand Down Expand Up @@ -188,7 +188,7 @@ def build_jpeg_preview(
writer.SetInputConnection(windowto_image_filter.GetOutputPort())
writer.Write()

return ImagePreviewBuilderPillow().build_jpeg_preview(
return ImagePreviewBuilderWand().build_jpeg_preview(
tmp_png.name, preview_name, cache_path, page_id, extension, size, mimetype
)

Expand Down
4 changes: 2 additions & 2 deletions preview_generator/preview/builder/document__drawio.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from preview_generator.exception import BuilderDependencyNotFound
from preview_generator.exception import IntermediateFileBuildingFailed
from preview_generator.preview.builder.image__pillow import ImagePreviewBuilderPillow
from preview_generator.preview.builder.image__wand import ImagePreviewBuilderWand
from preview_generator.preview.generic_preview import PreviewBuilder
from preview_generator.utils import ImgDims
from preview_generator.utils import MimetypeMapping
Expand Down Expand Up @@ -90,7 +90,7 @@ def build_jpeg_preview(
"failed with status {}".format(build_jpg_result_code)
)

ImagePreviewBuilderPillow().build_jpeg_preview(
ImagePreviewBuilderWand().build_jpeg_preview(
tmp_jpg.name, preview_name, cache_path, page_id, extension, size, mimetype
)

Expand Down
4 changes: 2 additions & 2 deletions preview_generator/preview/builder/document__sketch.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import typing
import zipfile

from preview_generator.preview.builder.image__pillow import ImagePreviewBuilderPillow # nopep8
from preview_generator.preview.builder.image__wand import ImagePreviewBuilderWand # nopep8
from preview_generator.preview.generic_preview import PreviewBuilder
from preview_generator.utils import ImgDims
from preview_generator.utils import MimetypeMapping
Expand Down Expand Up @@ -46,7 +46,7 @@ def build_jpeg_preview(
zip.extract("previews/preview.png", tmp_dir)
zip.close()

ImagePreviewBuilderPillow().build_jpeg_preview(
ImagePreviewBuilderWand().build_jpeg_preview(
tmp_dir + "/previews/preview.png",
preview_name,
cache_path,
Expand Down
4 changes: 2 additions & 2 deletions preview_generator/preview/builder/image__cairosvg.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

# HACK - G.M - 2020-12-26 - Hack to allow loading modules without cairosvg installed
from preview_generator.exception import BuilderDependencyNotFound
from preview_generator.preview.builder.image__pillow import ImagePreviewBuilderPillow # nopep8
from preview_generator.preview.builder.image__wand import ImagePreviewBuilderWand # nopep8
from preview_generator.preview.generic_preview import ImagePreviewBuilder
from preview_generator.utils import ImgDims

Expand Down Expand Up @@ -55,7 +55,7 @@ def build_jpeg_preview(
) as tmp_png:
cairosvg.svg2png(url=file_path, write_to=tmp_png.name, dpi=96)

return ImagePreviewBuilderPillow().build_jpeg_preview(
return ImagePreviewBuilderWand().build_jpeg_preview(
tmp_png.name, preview_name, cache_path, page_id, extension, size, mimetype
)

Expand Down
5 changes: 3 additions & 2 deletions preview_generator/preview/builder/image__imconvert.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@


class ImagePreviewBuilderIMConvert(ImagePreviewBuilder):
"""IM means Image Magick"""
"""WARNING : This builder is deprecated, prefer ImagePreviewBuilderWand instead which
support the same list of format."""

MIMETYPES = [] # type: typing.List[str]
# TODO - G.M - 2019-11-21 - find better storage solution for mimetype mapping
Expand Down Expand Up @@ -53,7 +54,7 @@ class ImagePreviewBuilderIMConvert(ImagePreviewBuilder):
MimetypeMapping("image/heic", ".heif"),
]

weight = 30
weight = 0
inkhey marked this conversation as resolved.
Show resolved Hide resolved

@classmethod
def get_label(cls) -> str:
Expand Down
4 changes: 2 additions & 2 deletions preview_generator/preview/builder/image__inkscape.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from preview_generator.exception import BuilderDependencyNotFound
from preview_generator.exception import IntermediateFileBuildingFailed
from preview_generator.preview.builder.image__pillow import ImagePreviewBuilderPillow # nopep8
from preview_generator.preview.builder.image__wand import ImagePreviewBuilderWand # nopep8
from preview_generator.preview.generic_preview import ImagePreviewBuilder
from preview_generator.utils import ImgDims
from preview_generator.utils import executable_is_available
Expand Down Expand Up @@ -83,6 +83,6 @@ def build_jpeg_preview(
"failed with status {}".format(build_png_result_code)
)

return ImagePreviewBuilderPillow().build_jpeg_preview(
return ImagePreviewBuilderWand().build_jpeg_preview(
tmp_png.name, preview_name, cache_path, page_id, extension, size, mimetype
)
3 changes: 3 additions & 0 deletions preview_generator/preview/builder/image__pillow.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ def get_strategy(self, image: PIL.Image) -> ImageConvertStrategy:


class ImagePreviewBuilderPillow(ImagePreviewBuilder):
"""WARNING : This builder is deprecated, prefer ImagePreviewBuilderWand instead which
support the same list of format."""

weight = 20

def __init__(
Expand Down
177 changes: 115 additions & 62 deletions preview_generator/preview/builder/image__wand.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,64 @@
# -*- coding: utf-8 -*-

from io import BytesIO
import os
import typing

from wand.image import Color
from wand.image import Image as WImage
from wand.color import Color
from wand.image import Image
from wand.exceptions import CoderError, CoderFatalError, CoderWarning
import wand.version

from preview_generator.preview.builder.image__imconvert import ImagePreviewBuilderIMConvert
from preview_generator.extension import mimetypes_storage
from preview_generator.exception import BuilderDependencyNotFound
from preview_generator.preview.generic_preview import ImagePreviewBuilder
from preview_generator.utils import ImgDims
from preview_generator.utils import MimetypeMapping
from preview_generator.utils import compute_resize_dims
from preview_generator.utils import executable_is_available
from preview_generator.utils import imagemagick_supported_mimes

DEFAULT_JPEG_QUALITY = 85
DEFAULT_JPEG_PROGRESSIVE = True


class ImagePreviewBuilderWand(ImagePreviewBuilder):
"""
WARNING : This builder is deprecated, prefer ImagePreviewBuilderIMConvert instead which
support the same list of format.
"""

weight = 10
weight = 30
MIMETYPES = [] # type: typing.List[str]
# TODO - G.M - 2019-11-21 - find better storage solution for mimetype mapping
# dict and/or list.
# see https://github.com/algoo/preview-generator/pull/148#discussion_r346381508
SUPPORTED_RAW_CAMERA_MIMETYPE_MAPPING = [
MimetypeMapping("image/x-sony-arw", ".arw"),
MimetypeMapping("image/x-adobe-dng", ".dng"),
MimetypeMapping("image/x-sony-sr2", ".sr2"),
MimetypeMapping("image/x-sony-srf", ".srf"),
MimetypeMapping("image/x-sigma-x3f", ".x3f"),
MimetypeMapping("image/x-canon-crw", ".crw"),
MimetypeMapping("image/x-canon-cr2", ".cr2"),
MimetypeMapping("image/x-epson-erf", ".erf"),
MimetypeMapping("image/x-fuji-raf", ".raf"),
MimetypeMapping("image/x-nikon-nef", ".nef"),
MimetypeMapping("image/x-olympus-orf", ".orf"),
MimetypeMapping("image/x-panasonic-raw", ".raw"),
MimetypeMapping("image/x-panasonic-rw2", ".rw2"),
MimetypeMapping("image/x-pentax-pef", ".pef"),
MimetypeMapping("image/x-kodak-dcr", ".dcr"),
MimetypeMapping("image/x-kodak-k25", ".k25"),
MimetypeMapping("image/x-kodak-kdc", ".kdc"),
MimetypeMapping("image/x-minolta-mrw", ".mrw"),
]

SUPPORTED_HEIC_MIMETYPE_MAPPING = [
MimetypeMapping("image/heic", ".heic"),
MimetypeMapping("image/heic", ".heif"),
]

def __init__(
self, quality: int = DEFAULT_JPEG_QUALITY, progressive: bool = DEFAULT_JPEG_PROGRESSIVE,
):
super().__init__()
self.quality = quality
self.progressive = progressive

@classmethod
def get_label(cls) -> str:
Expand All @@ -37,7 +74,29 @@ def __load_mimetypes(cls) -> typing.List[str]:
Load supported mimetypes from WAND library
:return: list of supported mime types
"""
return imagemagick_supported_mimes()

mimes = imagemagick_supported_mimes() # type: typing.List[str]
# HACK - G.M - 2019-10-31 - Handle raw format only if ufraw-batch is installed as most common
# default imagemagick configuration delegate raw format to ufraw-batch.
if executable_is_available("ufraw-batch"):
for mimetype_mapping in cls.SUPPORTED_RAW_CAMERA_MIMETYPE_MAPPING:
mimes.append(mimetype_mapping.mimetype)
return mimes

@classmethod
def get_mimetypes_mapping(cls) -> typing.List[MimetypeMapping]:
mimetypes_mapping = [] # type: typing.List[MimetypeMapping]
mimetypes_mapping = (
mimetypes_mapping
+ cls.SUPPORTED_RAW_CAMERA_MIMETYPE_MAPPING
+ cls.SUPPORTED_HEIC_MIMETYPE_MAPPING
)
return mimetypes_mapping

@classmethod
def check_dependencies(cls) -> None:
if not executable_is_available("convert"):
raise BuilderDependencyNotFound("this builder requires convert to be available")

@classmethod
def get_supported_mimetypes(cls) -> typing.List[str]:
Expand All @@ -48,21 +107,9 @@ def get_supported_mimetypes(cls) -> typing.List[str]:
ImagePreviewBuilderWand.MIMETYPES = cls.__load_mimetypes()
mimetypes = ImagePreviewBuilderWand.MIMETYPES

# INFO - G.M - 2021-04-30
# Disable support for postscript,xcf and raw image format in wand, to ensure
# proper builder is used (either imagemagick convert or pillow)

invalid_mimetypes = ["application/postscript", "application/x-xcf", "image/x-xcf"]
for (
mimetype_mapping
) in ImagePreviewBuilderIMConvert().SUPPORTED_RAW_CAMERA_MIMETYPE_MAPPING:
invalid_mimetypes.append(mimetype_mapping.mimetype)

for invalid_mimetype in invalid_mimetypes:
try:
mimetypes.remove(invalid_mimetype)
except ValueError:
pass
extra_mimetypes = ["application/x-xcf", "image/x-xcf"]
mimetypes.extend(extra_mimetypes)

return mimetypes

def build_jpeg_preview(
Expand All @@ -77,41 +124,47 @@ def build_jpeg_preview(
) -> None:
if not size:
size = self.default_size
with open(file_path, "rb") as img:
result = self.image_to_jpeg_wand(img, ImgDims(width=size.width, height=size.height))

with open(
"{path}{extension}".format(path=cache_path + preview_name, extension=extension),
"wb",
) as jpeg:
buffer = result.read(1024)
while buffer:
jpeg.write(buffer)
buffer = result.read(1024)

def image_to_jpeg_wand(
self, jpeg: typing.Union[str, typing.IO[bytes]], preview_dims: ImgDims
) -> BytesIO:
preview_name = preview_name + extension
dest_path = os.path.join(cache_path, preview_name)
self.image_to_jpeg_wand(file_path, size, dest_path, mimetype=mimetype)

def image_to_jpeg_wand(self, file_path: str, preview_dims: ImgDims, dest_path: str,
mimetype: typing.Optional[str]) -> None:
try:
with self._convert_image(file_path, preview_dims) as img:
img.save(filename=dest_path)
except (CoderError, CoderFatalError, CoderWarning) as e:
assert mimetype
file_ext = mimetypes_storage.guess_extension(mimetype, strict=False) or ""
if file_ext:
file_path = file_ext.lstrip(".") + ":" + file_path
with self._convert_image(file_path, preview_dims) as img:
img.save(filename=dest_path)
else:
raise e

def _convert_image(self, file_path: str, preview_dims: ImgDims) -> Image:
"""
for jpeg, gif and bmp
:param jpeg:
:param size:
:return:
refer: https://legacy.imagemagick.org/Usage/thumbnails/
like cmd: convert -layers merge -background white -thumbnail widthxheight \
-auto-orient -quality 85 -interlace plane input.jpeg output.jpeg
"""
self.logger.info("Converting image to jpeg using wand")

with WImage(file=jpeg, background=Color("white")) as image:

preview_dims = ImgDims(width=preview_dims.width, height=preview_dims.height)

resize_dim = compute_resize_dims(
dims_in=ImgDims(width=image.size[0], height=image.size[1]), dims_out=preview_dims
)
image.resize(resize_dim.width, resize_dim.height)
# INFO - jumenzel - 2019-03-12 - remove metadata, color-profiles from this image.
image.strip()
content_as_bytes = image.make_blob("jpeg")
output = BytesIO()
output.write(content_as_bytes)
output.seek(0, 0)
return output

img = Image(filename=file_path)
resize_dim = compute_resize_dims(
dims_in=ImgDims(width=img.width, height=img.height), dims_out=preview_dims
)

img.auto_orient()
img.iterator_reset()
img.background_color = Color("white")
img.merge_layers("merge")

if self.progressive:
img.interlace_scheme = "plane"

img.compression_quality = self.quality

img.thumbnail(resize_dim.width, resize_dim.height)

return img
5 changes: 3 additions & 2 deletions preview_generator/preview/builder_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,10 @@ def load_builders(self, force: bool = False) -> None:
if is_abstract(cls):
# INFO - G.M - 2021-06-22 - Skip abstract classes from loaded builders
pass
elif cls.__name__ == "ImagePreviewBuilderWand":
elif cls.__name__ in ("ImagePreviewBuilderPillow", "ImagePreviewBuilderIMConvert"):
self.logger.info(
"ImagePreviewBuilderWand builder is deprecated and is not registered by default. Consider using ImagePreviewBuilderIMConvert instead"
"%s builder is deprecated and is not registered by default. "
"Consider using ImagePreviewBuilderIMConvert instead".format(cls.__name__)
)
else:
self.register_builder(cls, overwrite=False)
Expand Down
33 changes: 33 additions & 0 deletions tests/builders/test_image_wand_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os
from PIL import Image
from preview_generator.preview.builder.image__wand import ImagePreviewBuilderWand
from preview_generator.preview.builder.image__imconvert import ImagePreviewBuilderIMConvert
from preview_generator.utils import ImgDims

CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
CACHE_DIR = "/tmp/preview-generator-tests/cache"


def test_build_jpeg_preview() -> None:
wand_builder = ImagePreviewBuilderWand()
test_orient_path = os.path.join(CURRENT_DIR, "the_img.png")
extension = ".jpg"
preview_name = "preview_the_img"
width = 512
height = 256
size = ImgDims(width=width, height=height)
wand_builder.build_jpeg_preview(
file_path=test_orient_path,
preview_name=preview_name,
cache_path=CACHE_DIR,
page_id=-1,
size=size,
extension=extension
)
preview_name = preview_name + extension
dest_path = os.path.join(CACHE_DIR, preview_name)
assert os.path.exists(dest_path)
assert os.path.getsize(dest_path) > 0
with Image.open(dest_path) as jpg:
assert jpg.height == height
assert jpg.width in range(288, 290)
Binary file added tests/builders/the_img.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/input/arw_raw/DSC08523
Binary file not shown.
Loading