from __future__ import annotations
import os.path
from io import BytesIO
from PIL import Image
from typing import Union
[docs]
def imgSize(path: str) -> tuple[int, int]:
""" returns (width, height) """
im = Image.open(path)
return im.size
[docs]
def asImage(obj: Union[str, Image.Image]) -> Image.Image:
"""
Returns `obj` if already a pillow Image or reads it from disk if given a path
Args:
obj: a pilllow Image or a path to an image file
Returns:
a pillow Image
"""
if isinstance(obj, Image.Image):
return obj
elif isinstance(obj, str):
return Image.open(obj)
else:
raise TypeError(f"obj type {type(obj)} not supported")
[docs]
def hasTransparency(img: Union[str, Image.Image]) -> bool:
"""
Returns True if this image has an alpha channel
Args:
img: the image or a path to it
Returns:
True if the image has transparency
"""
img = asImage(img)
if img.mode == "P":
transparent = img.info.get("transparency", -1)
for _, index in img.getcolors():
if index == transparent:
return True
elif img.mode == "RGBA":
extrema = img.getextrema()
if extrema[3][0] < 255:
return True
return False
[docs]
def removeTransparency(im: Image.Image, background=(255, 255, 255)) -> Image.Image:
"""
Remove transparency (alpha channel) from a PIL image
Args:
im: a PIL image (read via PIL.Image.open)
background: the color to use as background
Returns:
"""
# Only process if image has transparency (http://stackoverflow.com/a/1963146)
if im.mode in ('RGBA', 'LA') or (im.mode == 'P' and 'transparency' in im.info):
# Need to convert to RGBA if LA format due to a bug in
# PIL (http://stackoverflow.com/a/1963146)
alpha = im.convert('RGBA').getchannel('A') # .split()[-1]
# Create a new background image of our matt color.
# Must be RGBA because paste requires both images have the same format
# (http://stackoverflow.com/a/8720632 and http://stackoverflow.com/a/9459208)
bg = Image.new("RGBA", im.size, background + (255,))
bg.paste(im, mask=alpha)
return bg
else:
return im
[docs]
def pngRemoveTransparency(pngfile: str, outfile='', background=(255, 255, 255)
) -> None:
'''
Remove transparency from a png file
If outfile is not given, the operation is performed in place
Args:
pngfile: the source file
outfile: if given, the result is saved to this file
background: the color to use as replacement for the background
'''
assert os.path.splitext(pngfile)[1] == '.png'
img = Image.open(pngfile)
img = removeTransparency(img, background=background)
if outfile:
img.save(outfile)
else:
os.remove(pngfile)
img.save(pngfile)
[docs]
def readImageAsBase64(imgpath: str,
outformat='',
removeAlpha=False
) -> tuple[bytes, int, int]:
"""
Read an image and output its base64 representation
Args:
imgpath: the path to the image
outformat: the format to save the image to
removeAlpha: if True, remove alpha channel
Returns:
a tuple (data, width, height), where data is the base64
representation of the image, as bytes
"""
import base64
if not outformat:
ext = os.path.splitext(imgpath)[1][1:].lower()
outformat = {'png': 'png', 'jpg': 'jpeg', 'jpeg': 'jpeg'}.get(ext)
if outformat is None:
raise ValueError(f"Format unknown for file {imgpath}")
assert outformat in {'jpeg', 'png'}, f"Unsupported format: {outformat}. Formats supported: png, jpeg"
buffer = BytesIO()
im = Image.open(imgpath)
if removeAlpha:
im = removeTransparency(im)
im.save(buffer, format=outformat)
imgbytes = base64.b64encode(buffer.getvalue())
width, height = im.size
return imgbytes, width, height
[docs]
def cropToBoundingBox(inputpath: str, outpath: str = '', margin: int | tuple[int, int, int, int] = 0) -> None:
"""
Crop an image to its content, trimming any empty space
Args:
inputpath: the path of the input image
outpath: the path of the output. If not given the original image is modified
margin: a margin in pixels. Can also be a tuple (x0, y0, x1, y1)
"""
from PIL import ImageChops
img = Image.open(inputpath).convert('RGB')
border = img.getpixel((0, 0))
bg = Image.new(img.mode, img.size, border)
diff = ImageChops.difference(img, bg)
diff = ImageChops.add(diff, diff, 2.0, -100)
box = diff.getbbox()
if not box:
raise RuntimeError('Could not find bounding box')
if margin:
if isinstance(margin, int):
x0 = y0 = x1 = y1 = margin
else:
x0, y0, x1, y1 = margin
box = (max(0, box[0] - x0),
max(0, box[1] - y0),
min(box[2] + x1, img.width),
min(box[3] + y1, img.height)
)
croppedimg = img.crop(box)
croppedimg.save(outpath or inputpath)
[docs]
def htmlImgBase64(imgpath: str,
width: int | str = None,
maxwidth: int | str = None,
margintop='14px',
padding='10px',
removeAlpha=False,
scale=1.
) -> str:
"""
Read an image and return the data as base64 within an img html tag
Args:
imgpath: the path to the image
width: the width of the displayed image. Either a width
in pixels or a str as passed to css ('800px', '100%').
maxwidth: similar to width
scale: if width is not given, a scale value can be used to display
the image at a relative width
Returns:
the generated html
"""
imgstr, imwidth, imheight = readImageAsBase64(imgpath, outformat='png',
removeAlpha=removeAlpha)
imgstr = imgstr.decode('utf-8')
if scale and not width:
width = int(imwidth * scale)
attrs = [f'padding:{padding}',
f'margin-top:{margintop}']
if maxwidth:
if isinstance(maxwidth, int):
maxwidth = f'{maxwidth}px'
attrs.append(f'max-width: {maxwidth}')
if width is not None:
if isinstance(width, int):
width = f'{width}px'
attrs.append(f'width:{width}')
style = ";\n".join(attrs)
return fr'''
<img style="display:inline; {style}"
src="data:image/png;base64,{imgstr}"/>'''