diff --git a/src/docx/image/__init__.py b/src/docx/image/__init__.py index d28033ef1..24b55fbfa 100644 --- a/src/docx/image/__init__.py +++ b/src/docx/image/__init__.py @@ -9,6 +9,7 @@ from docx.image.jpeg import Exif, Jfif from docx.image.png import Png from docx.image.tiff import Tiff +from docx.image.webp import Webp SIGNATURES = ( # class, offset, signature_bytes @@ -20,4 +21,5 @@ (Tiff, 0, b"MM\x00*"), # big-endian (Motorola) TIFF (Tiff, 0, b"II*\x00"), # little-endian (Intel) TIFF (Bmp, 0, b"BM"), + (Webp, 0, b"RIFF"), ) diff --git a/src/docx/image/constants.py b/src/docx/image/constants.py index 729a828b2..c5c5bf38d 100644 --- a/src/docx/image/constants.py +++ b/src/docx/image/constants.py @@ -105,6 +105,7 @@ class MIME_TYPE: JPEG = "image/jpeg" PNG = "image/png" TIFF = "image/tiff" + WEBP = "image/webp" class PNG_CHUNK_TYPE: diff --git a/src/docx/image/webp.py b/src/docx/image/webp.py new file mode 100644 index 000000000..c455d3f66 --- /dev/null +++ b/src/docx/image/webp.py @@ -0,0 +1,257 @@ +"""Objects related to parsing headers of WEBP image streams. + +Docs: https://developers.google.com/speed/webp/docs/riff_container + +VP8: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| 'R' | 'I' | 'F' | 'F' | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| File Size | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| 'W' | 'E' | 'B' | 'P' | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| ChunkHeader('VP8 ') | +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +: VP8 data : ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +1. Data begins with string RIFF +2. Little-endian 32-bit file size +3. String WEBP +4. String VP8 (with space) +5. Little-endian 32-bit chunk size +6. 3-byte frame tag for interframes, or 10-byte frame tag for keyframes +7. Compressed data partitions containing: + - Frame header + - Macroblock prediction data + - DCT/WHT coefficient data +The frame dimensions are encoded in the frame header within the first compressed data partition, +not in a fixed position like VP8L. + +VP8L: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| 'R' | 'I' | 'F' | 'F' | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| File Size | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| 'W' | 'E' | 'B' | 'P' | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| ChunkHeader('VP8L') | +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +: VP8L data : ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +1. Data begins with the string RIFF +2. A little endian 32 bit value +3. String WEBP +4. String VP8L +5. A little endian 32 bit value +6. 1 byte signature 0x2f And then the first 28 bits contains the width and the height + +VP8X: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| 'R' | 'I' | 'F' | 'F' | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| File Size | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| 'W' | 'E' | 'B' | 'P' | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| ChunkHeader('VP8X') | +| | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +|Rsv|I|L|E|X|A|R| Reserved | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Canvas Width Minus One | ... ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +... Canvas Height Minus One | ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +1. Data begins with string RIFF +2. Little-endian 32-bit file size +3. String WEBP +4. String VP8X +5. Little-endian 32-bit chunk size +6. Reeserved (Rsv): 2 bits (MUST be 0. Readers MUST ignore this field.) +7. ICC profile (I): 1 bit (Set if the file contains an 'ICCP' Chunk.) +8. Alpha (L): 1 bit (Set if any of the frames of the image contain transparency information ("alpha").) +9. Exif metadata (E): 1 bit (Set if the file contains Exif metadata.) +10. XMP metadata (X): 1 bit (Set if the file contains XMP metadata.) +11. Animation (A): 1 bit (Set if this is an animated image. Data in 'ANIM' and 'ANMF' Chunks should be used to control the animation.) +12. Reserved (R): 1 bit (MUST be 0. Readers MUST ignore this field.) +13. Reserved: 24 bits (MUST be 0. Readers MUST ignore this field.) +14. Canvas Width Minus One: 24 bits (1-based width of the canvas in pixels. The actual canvas width is 1 + Canvas Width Minus One.) +15. Canvas Height Minus One: 24 bits (1-based height of the canvas in pixels. The actual canvas height is 1 + Canvas Height Minus One.) +16. The product of Canvas Width and Canvas Height MUST be at most 2^32 - 1. +""" + +from struct import unpack + +from docx.image.constants import MIME_TYPE +from docx.image.helpers import BIG_ENDIAN, StreamReader +from docx.image.image import BaseImageHeader + + +class Webp(BaseImageHeader): + """Image header parser for WEBP image format.""" + + @classmethod + def from_stream(cls, stream): + """Return |Webp| instance having header properties parsed from WEBP image in + `stream`.""" + stream.seek(0) + if stream.read(4) != b'RIFF': + raise ValueError("Not a valid WebP file") + + _ = stream.read(4) # File size, we can skip this + + if stream.read(4) != b'WEBP': + raise ValueError("Not a valid WebP file") + + chunk_header = stream.read(4) + + if chunk_header == b'VP8L': + width, height = cls._parse_lossless(stream) + elif chunk_header == b'VP8X': + width, height = cls._parse_extended(stream) + elif chunk_header == b'VP8 ': + width, height = cls._parse_simple(stream) + else: + raise ValueError("Unsupported WebP format") + + return cls(width, height, 72, 72) + + @staticmethod + def _parse_lossless(stream): + _ = unpack('> 14) & 0x3FFF) + 1 + + return w, h + + @staticmethod + def _parse_extended(stream): + _ = unpack('> 6) + + def _parse_extended_webp(self): + self._stream_rdr.skip(8) # Skip chunk size and flags + width_minus_one = int.from_bytes(self._stream_rdr.read(3), 'little') + height_minus_one = int.from_bytes(self._stream_rdr.read(3), 'little') + self._width = width_minus_one + 1 + self._height = height_minus_one + 1 + + @classmethod + def parse(cls, stream): + parser = cls(stream) + return parser