384 lines
14 KiB
Python
384 lines
14 KiB
Python
import io
|
|
import os
|
|
import re
|
|
import struct
|
|
from xml.etree import ElementTree
|
|
|
|
|
|
__all__ = ["get", "getDPI", "__version__"]
|
|
|
|
|
|
__version__ = "1.4.1"
|
|
|
|
|
|
_UNIT_KM = -3
|
|
_UNIT_100M = -2
|
|
_UNIT_10M = -1
|
|
_UNIT_1M = 0
|
|
_UNIT_10CM = 1
|
|
_UNIT_CM = 2
|
|
_UNIT_MM = 3
|
|
_UNIT_0_1MM = 4
|
|
_UNIT_0_01MM = 5
|
|
_UNIT_UM = 6
|
|
_UNIT_INCH = 6
|
|
|
|
_TIFF_TYPE_SIZES = {
|
|
1: 1,
|
|
2: 1,
|
|
3: 2,
|
|
4: 4,
|
|
5: 8,
|
|
6: 1,
|
|
7: 1,
|
|
8: 2,
|
|
9: 4,
|
|
10: 8,
|
|
11: 4,
|
|
12: 8,
|
|
}
|
|
|
|
|
|
def _convertToDPI(density, unit):
|
|
if unit == _UNIT_KM:
|
|
return int(density * 0.0000254 + 0.5)
|
|
elif unit == _UNIT_100M:
|
|
return int(density * 0.000254 + 0.5)
|
|
elif unit == _UNIT_10M:
|
|
return int(density * 0.00254 + 0.5)
|
|
elif unit == _UNIT_1M:
|
|
return int(density * 0.0254 + 0.5)
|
|
elif unit == _UNIT_10CM:
|
|
return int(density * 0.254 + 0.5)
|
|
elif unit == _UNIT_CM:
|
|
return int(density * 2.54 + 0.5)
|
|
elif unit == _UNIT_MM:
|
|
return int(density * 25.4 + 0.5)
|
|
elif unit == _UNIT_0_1MM:
|
|
return density * 254
|
|
elif unit == _UNIT_0_01MM:
|
|
return density * 2540
|
|
elif unit == _UNIT_UM:
|
|
return density * 25400
|
|
return density
|
|
|
|
|
|
def _convertToPx(value):
|
|
matched = re.match(r"(\d+(?:\.\d+)?)?([a-z]*)$", value)
|
|
if not matched:
|
|
raise ValueError("unknown length value: %s" % value)
|
|
|
|
length, unit = matched.groups()
|
|
if unit == "":
|
|
return float(length)
|
|
elif unit == "cm":
|
|
return float(length) * 96 / 2.54
|
|
elif unit == "mm":
|
|
return float(length) * 96 / 2.54 / 10
|
|
elif unit == "in":
|
|
return float(length) * 96
|
|
elif unit == "pc":
|
|
return float(length) * 96 / 6
|
|
elif unit == "pt":
|
|
return float(length) * 96 / 6
|
|
elif unit == "px":
|
|
return float(length)
|
|
|
|
raise ValueError("unknown unit type: %s" % unit)
|
|
|
|
|
|
def get(filepath):
|
|
"""
|
|
Return (width, height) for a given img file content
|
|
no requirements
|
|
:type filepath: Union[bytes, str, pathlib.Path]
|
|
:rtype Tuple[int, int]
|
|
"""
|
|
height = -1
|
|
width = -1
|
|
|
|
if isinstance(filepath, io.BytesIO): # file-like object
|
|
fhandle = filepath
|
|
else:
|
|
fhandle = open(filepath, 'rb')
|
|
|
|
try:
|
|
head = fhandle.read(31)
|
|
size = len(head)
|
|
# handle GIFs
|
|
if size >= 10 and head[:6] in (b'GIF87a', b'GIF89a'):
|
|
# Check to see if content_type is correct
|
|
try:
|
|
width, height = struct.unpack("<hh", head[6:10])
|
|
except struct.error:
|
|
raise ValueError("Invalid GIF file")
|
|
# see png edition spec bytes are below chunk length then and finally the
|
|
elif size >= 24 and head.startswith(b'\211PNG\r\n\032\n') and head[12:16] == b'IHDR':
|
|
try:
|
|
width, height = struct.unpack(">LL", head[16:24])
|
|
except struct.error:
|
|
raise ValueError("Invalid PNG file")
|
|
# Maybe this is for an older PNG version.
|
|
elif size >= 16 and head.startswith(b'\211PNG\r\n\032\n'):
|
|
# Check to see if we have the right content type
|
|
try:
|
|
width, height = struct.unpack(">LL", head[8:16])
|
|
except struct.error:
|
|
raise ValueError("Invalid PNG file")
|
|
# handle JPEGs
|
|
elif size >= 2 and head.startswith(b'\377\330'):
|
|
try:
|
|
fhandle.seek(0) # Read 0xff next
|
|
size = 2
|
|
ftype = 0
|
|
while not 0xc0 <= ftype <= 0xcf or ftype in [0xc4, 0xc8, 0xcc]:
|
|
fhandle.seek(size, 1)
|
|
byte = fhandle.read(1)
|
|
while ord(byte) == 0xff:
|
|
byte = fhandle.read(1)
|
|
ftype = ord(byte)
|
|
size = struct.unpack('>H', fhandle.read(2))[0] - 2
|
|
# We are at a SOFn block
|
|
fhandle.seek(1, 1) # Skip `precision' byte.
|
|
height, width = struct.unpack('>HH', fhandle.read(4))
|
|
except (struct.error, TypeError):
|
|
raise ValueError("Invalid JPEG file")
|
|
# handle JPEG2000s
|
|
elif size >= 12 and head.startswith(b'\x00\x00\x00\x0cjP \r\n\x87\n'):
|
|
fhandle.seek(48)
|
|
try:
|
|
height, width = struct.unpack('>LL', fhandle.read(8))
|
|
except struct.error:
|
|
raise ValueError("Invalid JPEG2000 file")
|
|
# handle big endian TIFF
|
|
elif size >= 8 and head.startswith(b"\x4d\x4d\x00\x2a"):
|
|
offset = struct.unpack('>L', head[4:8])[0]
|
|
fhandle.seek(offset)
|
|
ifdsize = struct.unpack(">H", fhandle.read(2))[0]
|
|
for i in range(ifdsize):
|
|
tag, datatype, count, data = struct.unpack(">HHLL", fhandle.read(12))
|
|
if tag == 256:
|
|
if datatype == 3:
|
|
width = int(data / 65536)
|
|
elif datatype == 4:
|
|
width = data
|
|
else:
|
|
raise ValueError("Invalid TIFF file: width column data type should be SHORT/LONG.")
|
|
elif tag == 257:
|
|
if datatype == 3:
|
|
height = int(data / 65536)
|
|
elif datatype == 4:
|
|
height = data
|
|
else:
|
|
raise ValueError("Invalid TIFF file: height column data type should be SHORT/LONG.")
|
|
if width != -1 and height != -1:
|
|
break
|
|
if width == -1 or height == -1:
|
|
raise ValueError("Invalid TIFF file: width and/or height IDS entries are missing.")
|
|
elif size >= 8 and head.startswith(b"\x49\x49\x2a\x00"):
|
|
offset = struct.unpack('<L', head[4:8])[0]
|
|
fhandle.seek(offset)
|
|
ifdsize = struct.unpack("<H", fhandle.read(2))[0]
|
|
for i in range(ifdsize):
|
|
tag, datatype, count, data = struct.unpack("<HHLL", fhandle.read(12))
|
|
if tag == 256:
|
|
width = data
|
|
elif tag == 257:
|
|
height = data
|
|
if width != -1 and height != -1:
|
|
break
|
|
if width == -1 or height == -1:
|
|
raise ValueError("Invalid TIFF file: width and/or height IDS entries are missing.")
|
|
# handle little endian BigTiff
|
|
elif size >= 8 and head.startswith(b"\x49\x49\x2b\x00"):
|
|
bytesize_offset = struct.unpack('<L', head[4:8])[0]
|
|
if bytesize_offset != 8:
|
|
raise ValueError('Invalid BigTIFF file: Expected offset to be 8, found {} instead.'.format(offset))
|
|
offset = struct.unpack('<Q', head[8:16])[0]
|
|
fhandle.seek(offset)
|
|
ifdsize = struct.unpack("<Q", fhandle.read(8))[0]
|
|
for i in range(ifdsize):
|
|
tag, datatype, count, data = struct.unpack("<HHQQ", fhandle.read(20))
|
|
if tag == 256:
|
|
width = data
|
|
elif tag == 257:
|
|
height = data
|
|
if width != -1 and height != -1:
|
|
break
|
|
if width == -1 or height == -1:
|
|
raise ValueError("Invalid BigTIFF file: width and/or height IDS entries are missing.")
|
|
|
|
# handle SVGs
|
|
elif size >= 5 and (head.startswith(b'<?xml') or head.startswith(b'<svg')):
|
|
fhandle.seek(0)
|
|
data = fhandle.read(1024)
|
|
try:
|
|
data = data.decode('utf-8')
|
|
width = re.search(r'[^-]width="(.*?)"', data).group(1)
|
|
height = re.search(r'[^-]height="(.*?)"', data).group(1)
|
|
except Exception:
|
|
raise ValueError("Invalid SVG file")
|
|
width = _convertToPx(width)
|
|
height = _convertToPx(height)
|
|
|
|
# handle Netpbm
|
|
elif head[:1] == b"P" and head[1:2] in b"123456":
|
|
fhandle.seek(2)
|
|
sizes = []
|
|
|
|
while True:
|
|
next_chr = fhandle.read(1)
|
|
|
|
if next_chr.isspace():
|
|
continue
|
|
|
|
if next_chr == b"":
|
|
raise ValueError("Invalid Netpbm file")
|
|
|
|
if next_chr == b"#":
|
|
fhandle.readline()
|
|
continue
|
|
|
|
if not next_chr.isdigit():
|
|
raise ValueError("Invalid character found on Netpbm file")
|
|
|
|
size = next_chr
|
|
next_chr = fhandle.read(1)
|
|
|
|
while next_chr.isdigit():
|
|
size += next_chr
|
|
next_chr = fhandle.read(1)
|
|
|
|
sizes.append(int(size))
|
|
|
|
if len(sizes) == 2:
|
|
break
|
|
|
|
fhandle.seek(-1, os.SEEK_CUR)
|
|
width, height = sizes
|
|
elif head.startswith(b"RIFF") and head[8:12] == b"WEBP":
|
|
if head[12:16] == b"VP8 ":
|
|
width, height = struct.unpack("<HH", head[26:30])
|
|
elif head[12:16] == b"VP8X":
|
|
width = struct.unpack("<I", head[24:27] + b"\0")[0]
|
|
height = struct.unpack("<I", head[27:30] + b"\0")[0]
|
|
elif head[12:16] == b"VP8L":
|
|
b = head[21:25]
|
|
width = (((b[1] & 63) << 8) | b[0]) + 1
|
|
height = (((b[3] & 15) << 10) | (b[2] << 2) | ((b[1] & 192) >> 6)) + 1
|
|
else:
|
|
raise ValueError("Unsupported WebP file")
|
|
|
|
finally:
|
|
fhandle.close()
|
|
|
|
return width, height
|
|
|
|
|
|
def getDPI(filepath):
|
|
"""
|
|
Return (x DPI, y DPI) for a given img file content
|
|
no requirements
|
|
:type filepath: Union[bytes, str, pathlib.Path]
|
|
:rtype Tuple[int, int]
|
|
"""
|
|
xDPI = -1
|
|
yDPI = -1
|
|
|
|
if not isinstance(filepath, bytes):
|
|
filepath = str(filepath)
|
|
|
|
with open(filepath, 'rb') as fhandle:
|
|
head = fhandle.read(24)
|
|
size = len(head)
|
|
# handle GIFs
|
|
# GIFs doesn't have density
|
|
if size >= 10 and head[:6] in (b'GIF87a', b'GIF89a'):
|
|
pass
|
|
# see png edition spec bytes are below chunk length then and finally the
|
|
elif size >= 24 and head.startswith(b'\211PNG\r\n\032\n'):
|
|
chunkOffset = 8
|
|
chunk = head[8:]
|
|
while True:
|
|
chunkType = chunk[4:8]
|
|
if chunkType == b'pHYs':
|
|
try:
|
|
xDensity, yDensity, unit = struct.unpack(">LLB", chunk[8:])
|
|
except struct.error:
|
|
raise ValueError("Invalid PNG file")
|
|
if unit:
|
|
xDPI = _convertToDPI(xDensity, _UNIT_1M)
|
|
yDPI = _convertToDPI(yDensity, _UNIT_1M)
|
|
else: # no unit
|
|
xDPI = xDensity
|
|
yDPI = yDensity
|
|
break
|
|
elif chunkType == b'IDAT':
|
|
break
|
|
else:
|
|
try:
|
|
dataSize, = struct.unpack(">L", chunk[0:4])
|
|
except struct.error:
|
|
raise ValueError("Invalid PNG file")
|
|
chunkOffset += dataSize + 12
|
|
fhandle.seek(chunkOffset)
|
|
chunk = fhandle.read(17)
|
|
# handle JPEGs
|
|
elif size >= 2 and head.startswith(b'\377\330'):
|
|
try:
|
|
fhandle.seek(0) # Read 0xff next
|
|
size = 2
|
|
ftype = 0
|
|
while not 0xc0 <= ftype <= 0xcf:
|
|
if ftype == 0xe0: # APP0 marker
|
|
fhandle.seek(7, 1)
|
|
unit, xDensity, yDensity = struct.unpack(">BHH", fhandle.read(5))
|
|
if unit == 1 or unit == 0:
|
|
xDPI = xDensity
|
|
yDPI = yDensity
|
|
elif unit == 2:
|
|
xDPI = _convertToDPI(xDensity, _UNIT_CM)
|
|
yDPI = _convertToDPI(yDensity, _UNIT_CM)
|
|
break
|
|
fhandle.seek(size, 1)
|
|
byte = fhandle.read(1)
|
|
while ord(byte) == 0xff:
|
|
byte = fhandle.read(1)
|
|
ftype = ord(byte)
|
|
size = struct.unpack('>H', fhandle.read(2))[0] - 2
|
|
except struct.error:
|
|
raise ValueError("Invalid JPEG file")
|
|
# handle JPEG2000s
|
|
elif size >= 12 and head.startswith(b'\x00\x00\x00\x0cjP \r\n\x87\n'):
|
|
fhandle.seek(32)
|
|
# skip JP2 image header box
|
|
headerSize = struct.unpack('>L', fhandle.read(4))[0] - 8
|
|
fhandle.seek(4, 1)
|
|
foundResBox = False
|
|
try:
|
|
while headerSize > 0:
|
|
boxHeader = fhandle.read(8)
|
|
boxType = boxHeader[4:]
|
|
if boxType == b'res ': # find resolution super box
|
|
foundResBox = True
|
|
headerSize -= 8
|
|
break
|
|
boxSize, = struct.unpack('>L', boxHeader[:4])
|
|
fhandle.seek(boxSize - 8, 1)
|
|
headerSize -= boxSize
|
|
if foundResBox:
|
|
while headerSize > 0:
|
|
boxHeader = fhandle.read(8)
|
|
boxType = boxHeader[4:]
|
|
if boxType == b'resd': # Display resolution box
|
|
yDensity, xDensity, yUnit, xUnit = struct.unpack(">HHBB", fhandle.read(10))
|
|
xDPI = _convertToDPI(xDensity, xUnit)
|
|
yDPI = _convertToDPI(yDensity, yUnit)
|
|
break
|
|
boxSize, = struct.unpack('>L', boxHeader[:4])
|
|
fhandle.seek(boxSize - 8, 1)
|
|
headerSize -= boxSize
|
|
except struct.error as e:
|
|
raise ValueError("Invalid JPEG2000 file")
|
|
return xDPI, yDPI
|