Update workflows/publish_pypi.yml
[manga-dl.git] / manga_py / manga_image.py
blobc0a4cfbe2c3403a09e42afefe2c3e31f4622df38
1 import imghdr
2 from os import path
3 from typing import Tuple, Optional
5 from PIL import Image as PilImage, ImageChops, ImageFile
6 try:
7 from PIL import UnidentifiedImageError
8 except (ModuleNotFoundError, ImportError):
9 UnidentifiedImageError = OSError
12 __all__ = ['MangaImage']
15 def _pil_fmt(_) -> Optional[str]:
16 try:
17 with PilImage.open(_) as img:
18 return img.format.lower()
19 except UnidentifiedImageError:
20 return None
23 class MangaImage:
24 _image = None # type: PilImage.Image
25 src_path = None # type: str
27 def __init__(self, src_path):
28 """
29 :param src_path:
30 """
31 if not path.isfile(src_path):
32 raise AttributeError('Image not found')
34 self.src_path = src_path
35 try:
36 self._image = PilImage.open(src_path)
37 except UnidentifiedImageError:
38 if not ImageFile.LOAD_TRUNCATED_IMAGES:
39 ImageFile.LOAD_TRUNCATED_IMAGES = True
40 self._image = PilImage.open(src_path)
42 @staticmethod
43 def new(mode: str, size: Tuple[int, int]):
44 return PilImage.new(mode, size)
46 @property
47 def image(self) -> PilImage.Image:
48 """
49 :rtype: PilImage.Image
50 :return:
51 """
52 return self._image
54 @image.setter
55 def image(self, image: PilImage.Image):
56 self._image = image
58 def gray(self, dest_path: str):
59 """
60 :param dest_path:
61 :return:
62 """
63 try:
64 image = self.image.convert('LA')
65 except (ValueError, OSError):
66 image = self.image.convert('L')
67 if dest_path is not None:
68 image.save(dest_path)
69 return image
71 def convert(self, dest_path: str, quality: int = 95):
72 """
73 see http://pillow.readthedocs.io/en/3.4.x/handbook/image-file-formats.html
74 :param dest_path:
75 :param quality:
76 :return:
77 """
78 self.image.save(dest_path, quality=quality)
79 return dest_path
81 def crop_manual_with_offsets(self, offsets, dest_path: str):
82 """
83 :param offsets:
84 :param dest_path:
85 :return:
86 """
87 left, upper, right, lower = offsets
88 width, height = self.image.size
89 image = self.image.crop((
90 left,
91 upper,
92 width - right,
93 height - lower
95 image.save(dest_path)
97 def crop_manual(self, sizes: tuple, dest_path: str):
98 """
99 :param sizes: The crop rectangle, as a (left, upper, right, lower)-tuple.
100 :param dest_path:
101 :return:
103 self.image.crop(sizes).save(dest_path)
105 def crop_auto(self, dest_path: str):
107 :param dest_path:
108 :return:
110 bg = PilImage.new(
111 self.image.mode,
112 self.image.size,
113 self.image.getpixel((0, 0))
115 diff = ImageChops.difference(self.image, bg)
116 diff = ImageChops.add(diff, diff, 2.0, -100)
117 bbox = diff.getbbox()
118 if bbox:
119 crop = self.image.crop(bbox)
120 if dest_path:
121 crop.save(dest_path)
123 def close(self):
124 self.image is not None and self.image.close()
126 @staticmethod
127 def real_extension(_path):
128 ext = imghdr.what(_path)
129 if ext is None:
130 ext = _pil_fmt(_path)
131 if ext is None:
132 return None
133 return '.%s' % ext
135 @staticmethod
136 def is_image(_path) -> bool:
137 return (imghdr.what(_path) or _pil_fmt(_path)) is not None