59 خطوط
3.0 KiB
Python
59 خطوط
3.0 KiB
Python
"""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('<I', payload, 2+n)[0]
|
|
if count != len(payload)-6-n: raise ParseError('Invalid embedded compressed size')
|
|
decoder = zlib.decompressobj()
|
|
raster = decoder.decompress(payload[6+n:], MAX_ASSET+1)
|
|
if len(raster) > 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
|