|
| 1 | +#!/usr/bin/python3 -B |
| 2 | + |
| 3 | +import os # built-in library |
| 4 | +import numpy as np # pip install numpy |
| 5 | +import piexif # pip install piexif |
| 6 | + |
| 7 | +try: |
| 8 | + # package mode |
| 9 | + from imsize import pnghdr # local import: pnghdr.py |
| 10 | + from imsize import jpeghdr # local import: jpeghdr.py |
| 11 | + from imsize import pnm # local import: pnm.py |
| 12 | +except ImportError: |
| 13 | + # stand-alone mode |
| 14 | + import pnghdr |
| 15 | + import jpeghdr |
| 16 | + import pnm |
| 17 | + |
| 18 | + |
| 19 | +###################################################################################### |
| 20 | +# |
| 21 | +# P U B L I C A P I |
| 22 | +# |
| 23 | +###################################################################################### |
| 24 | + |
| 25 | + |
| 26 | +class ImageInfo: |
| 27 | + """ |
| 28 | + A container for image metadata, filled in and returned by read(). |
| 29 | +
|
| 30 | + Attributes: |
| 31 | + filespec (str): The filespec given to read(), copied verbatim |
| 32 | + filetype (str): File type: "png", "pnm", "jpeg" or "exif" |
| 33 | + filesize (int): Size of the file on disk in bytes |
| 34 | + width (int): Width of the image in pixels (orientation ignored) |
| 35 | + height (int): Height of the image in pixels (orientation ignored) |
| 36 | + nchan (int): Number of color channels: 1, 2, 3 or 4 |
| 37 | + bitdepth (int): Bits per sample: 1 to 16 |
| 38 | + bytedepth (int): Bytes per sample: 1 or 2 |
| 39 | + maxval (int): Maximum representable sample value, e.g., 255 |
| 40 | + dtype (type): NumPy dtype for sample values: uint8 or uint16 |
| 41 | + nbytes (int): Size of the image in bytes, uncompressed |
| 42 | + """ |
| 43 | + def __init__(self): |
| 44 | + self.filespec = None |
| 45 | + self.filetype = None |
| 46 | + self.filesize = None |
| 47 | + self.width = None |
| 48 | + self.height = None |
| 49 | + self.nchan = None |
| 50 | + self.bitdepth = None |
| 51 | + self.bytedepth = None |
| 52 | + self.maxval = None |
| 53 | + self.dtype = None |
| 54 | + self.nbytes = None |
| 55 | + |
| 56 | + |
| 57 | +def read(filespec): |
| 58 | + """ |
| 59 | + Parses a lowest common denominator set of metadata from the given |
| 60 | + PNG/PNM/JPEG/TIFF image, i.e., the dimensions and bit depth. Does |
| 61 | + not read the entire file but only what's necessary. Returns an |
| 62 | + ImageInfo with all fields filled in, or None in case of failure. |
| 63 | +
|
| 64 | + Example: |
| 65 | + info = imsize.read("myfile.jpg") |
| 66 | + factor = info.nbytes / info.filesize |
| 67 | + print(f"{info.filespec}: compression factor = {factor.1f}") |
| 68 | + """ |
| 69 | + filename = os.path.basename(filespec) # "path/image.ext" => "image.ext" |
| 70 | + extension = os.path.splitext(filename)[-1] # "image.ext" => ("image", ".ext") |
| 71 | + filetype = extension.lower()[1:] # ".EXT" => "ext" |
| 72 | + handlers = {"png": _read_png, |
| 73 | + "pnm": _read_pnm, |
| 74 | + "pgm": _read_pnm, |
| 75 | + "ppm": _read_pnm, |
| 76 | + "jpeg": _read_jpeg, |
| 77 | + "jpg": _read_jpeg, |
| 78 | + "tiff": _read_exif, |
| 79 | + "tif": _read_exif, |
| 80 | + "webp": _read_exif} |
| 81 | + if filetype in handlers: |
| 82 | + handler = handlers[filetype] |
| 83 | + info = handler(filespec) |
| 84 | + return info |
| 85 | + return None |
| 86 | + |
| 87 | + |
| 88 | +###################################################################################### |
| 89 | +# |
| 90 | +# I N T E R N A L F U N C T I O N S |
| 91 | +# |
| 92 | +###################################################################################### |
| 93 | + |
| 94 | + |
| 95 | +def _read_png(filespec): |
| 96 | + header = pnghdr.Png.from_file(filespec) |
| 97 | + colortype = pnghdr.Png.ColorType |
| 98 | + nchannels = {colortype.greyscale: 1, |
| 99 | + colortype.truecolor: 3, |
| 100 | + colortype.indexed: 3, |
| 101 | + colortype.greyscale_alpha: 2, |
| 102 | + colortype.truecolor_alpha: 4} |
| 103 | + info = ImageInfo() |
| 104 | + info.filespec = filespec |
| 105 | + info.filetype = "png" |
| 106 | + info.width = header.ihdr.width |
| 107 | + info.height = header.ihdr.height |
| 108 | + info.nchan = nchannels[header.ihdr.color_type] |
| 109 | + info.bitdepth = header.ihdr.bit_depth |
| 110 | + info = _complete(info) |
| 111 | + return info |
| 112 | + |
| 113 | + |
| 114 | +def _read_pnm(filespec): |
| 115 | + shape, maxval = pnm.dims(filespec) |
| 116 | + info = ImageInfo() |
| 117 | + info.filespec = filespec |
| 118 | + info.filetype = "pnm" |
| 119 | + info.width = shape[1] |
| 120 | + info.height = shape[0] |
| 121 | + info.nchan = shape[2] |
| 122 | + info.maxval = maxval |
| 123 | + info = _complete(info) |
| 124 | + return info |
| 125 | + |
| 126 | + |
| 127 | +def _read_exif(filespec): |
| 128 | + try: |
| 129 | + exif = piexif.load(filespec).pop("0th") |
| 130 | + info = ImageInfo() |
| 131 | + info.filespec = filespec |
| 132 | + info.filetype = "exif" |
| 133 | + info.width = exif.get(piexif.ImageIFD.ImageWidth) |
| 134 | + info.height = exif.get(piexif.ImageIFD.ImageLength) |
| 135 | + info.nchan = exif.get(piexif.ImageIFD.SamplesPerPixel) |
| 136 | + info.bitdepth = exif.get(piexif.ImageIFD.BitsPerSample)[0] |
| 137 | + info = _complete(info) |
| 138 | + return info |
| 139 | + except TypeError: |
| 140 | + print(f"Unable to parse {filespec}: missing/broken EXIF metadata.") |
| 141 | + return None |
| 142 | + |
| 143 | + |
| 144 | +def _read_jpeg(filespec): |
| 145 | + info = ImageInfo() |
| 146 | + info.filespec = filespec |
| 147 | + info.filetype = "jpeg" |
| 148 | + data = jpeghdr.Jpeg.from_file(filespec) |
| 149 | + for seg in data.segments: |
| 150 | + if seg.marker == seg.MarkerEnum.sof0: |
| 151 | + info.width = seg.data.image_width |
| 152 | + info.height = seg.data.image_height |
| 153 | + info.nchan = seg.data.num_components |
| 154 | + info.bitdepth = seg.data.bits_per_sample |
| 155 | + info = _complete(info) |
| 156 | + break |
| 157 | + return info |
| 158 | + |
| 159 | + |
| 160 | +def _complete(info): |
| 161 | + info.filesize = os.path.getsize(info.filespec) |
| 162 | + info.maxval = info.maxval or 2 ** info.bitdepth - 1 |
| 163 | + info.bitdepth = info.bitdepth or int(np.log2(info.maxval + 1)) |
| 164 | + info.bytedepth = 2 if info.maxval > 255 else 1 |
| 165 | + info.dtype = np.uint16 if info.maxval > 255 else np.uint8 |
| 166 | + info.nbytes = info.width * info.height * info.nchan * info.bytedepth |
| 167 | + return info |
0 commit comments