"""Decode only bounded, embedded raster assets. Never follow document filenames.""" import base64 import io import struct import warnings import zlib import olefile from PIL import Image from .record_decoder import binary_records, ParseError MAX_ASSET = 16 * 1024 * 1024 MAX_PIXELS = 16_000_000 Image.MAX_IMAGE_PIXELS = MAX_PIXELS def asset_name(name): return name.replace('\\', '/').rsplit('/', 1)[-1].casefold() def read_images(data): if not data.startswith(olefile.MAGIC): return {}, [] assets, errors = {}, [] try: with olefile.OleFileIO(io.BytesIO(data)) as ole: if not ole.exists('Storage'): return assets, errors raw = ole.openstream('Storage').read() total = 0 for record in binary_records(raw, 'Storage'): if not record.binary: continue payload = base64.b64decode(record.properties['BINARY_BASE64']) if len(payload) < 6 or payload[0] != 0xd0: errors.append('Unsupported embedded storage framing'); continue n = payload[1] if len(payload) < n+6: raise ParseError('Truncated embedded storage asset') name = asset_name(payload[2:2+n].decode('cp1252', errors='replace')) count = struct.unpack_from(' MAX_ASSET or not decoder.eof or decoder.unused_data: raise ParseError('Embedded raster exceeds decoding limit or has invalid compression') total += len(raster) if total > 32*1024*1024: raise ParseError('Total embedded raster size limit exceeded') with warnings.catch_warnings(): warnings.simplefilter('error', Image.DecompressionBombWarning) with Image.open(io.BytesIO(raster)) as source: if source.width*source.height > MAX_PIXELS: raise ParseError('Embedded raster pixel limit exceeded') if source.format not in {'PNG','BMP','JPEG','TIFF','GIF'}: errors.append('Unsupported embedded raster format'); continue if getattr(source, 'n_frames', 1) != 1: errors.append('Multi-frame raster skipped'); continue image = source.convert('RGBA') output = io.BytesIO(); image.save(output, format='PNG') png = output.getvalue() if len(png) > MAX_ASSET: raise ParseError('Converted raster exceeds asset limit') if name in assets: errors.append('Ambiguous embedded filename skipped'); assets[name] = None else: assets[name] = 'data:image/png;base64,' + base64.b64encode(png).decode('ascii') except Exception as exc: errors.append('Embedded storage not completely decoded: ' + str(exc)[:160]) return assets, errors