async def guess_mime(file: File) -> str: """Return the file's mimetype, or `application/octet-stream` if unknown.""" if isinstance(file, io.IOBase): file.seek(0, 0) elif isinstance(file, AsyncBufferedReader): await file.seek(0, 0) try: first_chunk: bytes async for first_chunk in async_generator_from_data(file): break else: return "inode/x-empty" # empty file # TODO: plaintext mime = filetype.guess_mime(first_chunk) return mime or ( "image/svg+xml" if await is_svg(file) else "application/octet-stream" ) finally: if isinstance(file, io.IOBase): file.seek(0, 0) elif isinstance(file, AsyncBufferedReader): await file.seek(0, 0)
async def is_svg(file: File) -> bool: """Return whether the file is a SVG (`lxml` is used for detection).""" chunks = [c async for c in async_generator_from_data(file)] with io.BytesIO(b"".join(chunks)) as file: try: _, element = next(xml_etree.iterparse(file, ("start",))) return element.tag == "{http://www.w3.org/2000/svg}svg" except (StopIteration, xml_etree.ParseError): return False
async def generate_thumbnail( self, data: UploadData, is_svg: bool = False, ) -> Tuple[bytes, MatrixImageInfo]: """Create a thumbnail from an image, return the bytes and info.""" png_modes = ("1", "L", "P", "RGBA") data = b"".join([c async for c in async_generator_from_data(data)]) is_svg = await utils.guess_mime(data) == "image/svg+xml" if is_svg: svg_width, svg_height = await utils.svg_dimensions(data) data = cairosvg.svg2png( bytestring=data, parent_width=svg_width, parent_height=svg_height, ) thumb = PILImage.open(io.BytesIO(data)) small = thumb.width <= 800 and thumb.height <= 600 is_jpg_png = thumb.format in ("JPEG", "PNG") jpgable_png = thumb.format == "PNG" and thumb.mode not in png_modes if small and is_jpg_png and not jpgable_png and not is_svg: raise UneededThumbnail() if not small: thumb.thumbnail((800, 600)) with io.BytesIO() as out: if thumb.mode in png_modes: thumb.save(out, "PNG", optimize=True) mime = "image/png" else: thumb.convert("RGB").save(out, "JPEG", optimize=True) mime = "image/jpeg" thumb_data = out.getvalue() thumb_size = len(thumb_data) if thumb_size >= len(data) and is_jpg_png and not is_svg: raise UneededThumbnail() info = MatrixImageInfo(thumb.width, thumb.height, mime, thumb_size) return (thumb_data, info)
async def svg_dimensions(file: File) -> Size: """Return the width and height, or viewBox width and height for a SVG. If these properties are missing (broken file), ``(256, 256)`` is returned. """ chunks = [c async for c in async_generator_from_data(file)] with io.BytesIO(b"".join(chunks)) as file: attrs = xml_etree.parse(file).getroot().attrib try: width = round(float(attrs.get("width", attrs["viewBox"].split()[3]))) except (KeyError, IndexError, ValueError, TypeError): width = 256 try: height = round(float(attrs.get("height", attrs["viewBox"].split()[4]))) except (KeyError, IndexError, ValueError, TypeError): height = 256 return (width, height)