File: imfile.py

package info (click to toggle)
displaycal-py3 3.9.16-1
  • links: PTS
  • area: main
  • in suites: forky, sid, trixie
  • size: 29,120 kB
  • sloc: python: 115,777; javascript: 11,540; xml: 598; sh: 257; makefile: 173
file content (417 lines) | stat: -rw-r--r-- 15,925 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
# -*- coding: utf-8 -*-


import math
import os
import struct
import time
import zlib

from DisplayCAL.meta import name as appname, version
from DisplayCAL.util_str import safe_str


TIFF_TAG_TYPE_BYTE = 1
TIFF_TAG_TYPE_ASCII = 2
TIFF_TAG_TYPE_WORD = 3
TIFF_TAG_TYPE_DWORD = 4
TIFF_TAG_TYPE_RATIONAL = 5  # 2 DWORDs


def tiff_get_header(w, h, samples_per_pixel, bitdepth):
    # Very helpful: http://www.fileformat.info/format/tiff/corion.htm

    header = []
    header.append(b"MM\0*")  # Note: We use big-endian byte order

    # Offset of image directory
    header.append(b"\0\0\0\x08")

    pixelcount = w * h * samples_per_pixel
    if bitdepth == 16:
        bytecount = pixelcount * 2
    else:
        bytecount = pixelcount

    # Image file directory (IFD)

    # PhotometricInterpretation
    if samples_per_pixel == 3:
        pmi = 2  # RGB
    else:
        pmi = 5  # Separated (usually CMYK)

    # Tag, type, length, offset or data, is data (otherwise offset)
    if w > 65535:
        tag_type_w = TIFF_TAG_TYPE_DWORD
    else:
        tag_type_w = TIFF_TAG_TYPE_WORD
    if h > 65535:
        tag_type_h = TIFF_TAG_TYPE_DWORD
    else:
        tag_type_h = TIFF_TAG_TYPE_WORD
    ifd = [
        (0x100, tag_type_w, 1, w, True),  # ImageWidth
        (0x101, tag_type_h, 1, h, True),  # ImageLength
        (0x106, TIFF_TAG_TYPE_WORD, 1, pmi, True),  # PhotometricInterpretation
        (0x115, TIFF_TAG_TYPE_WORD, 1, samples_per_pixel, True),  # SamplesPerPixel
        (0x117, TIFF_TAG_TYPE_DWORD, 1, bytecount, True),  # StripByteCounts
    ]
    # BitsPerSample
    ifd.append((0x102, TIFF_TAG_TYPE_WORD, 3, 10 + (len(ifd) + 2) * 12 + 4, False))
    # StripOffsets
    ifd.append((0x111, TIFF_TAG_TYPE_WORD, 1, 10 + (len(ifd) + 1) * 12 + 4 + 6, True))

    ifd.sort()  # Must be ascending order!

    header.append(struct.pack(">H", len(ifd)))  # Number of entries

    for tag, tagtype, length, payload, is_data in ifd:
        header.append(struct.pack(">H", tag))
        header.append(struct.pack(">H", tagtype))
        header.append(struct.pack(">I", length))
        if is_data and tagtype == 3:
            # A word left-aligned in a dword
            header.append(struct.pack(">H", payload))
            header.append(b"\0\0")
        else:
            header.append(struct.pack(">I", payload))

    # PlanarConfiguration default is 1 = RGBRGBRGB...

    # End of IFD
    header.append(b"\0" * 4)

    # BitsPerSample (6 bytes)
    header.append(struct.pack(">H", bitdepth) * 3)

    return b"".join(header)


def write(
    data, stream_or_filename, bitdepth=16, format=None, dimensions=None, extrainfo=None
):
    Image(data, bitdepth, extrainfo).write(stream_or_filename, format, dimensions)


def write_rgb_clut(stream_or_filename, clutres=33, bitdepth=16, format=None):
    clut = []
    for R in range(clutres):
        for G in range(clutres):
            clut.append([])
            for B in range(clutres):
                RGB = [v * (1.0 / (clutres - 1)) for v in (R, G, B)]
                clut[-1].append([v * (2**bitdepth - 1) for v in RGB])
    write(clut, stream_or_filename, bitdepth, format)


class Image:
    """Write 8 or 16 bit image files in DPX, PNG or TIFF format.

    Writing of single color images is highly optimized when using a single
    pixel as image data and setting dimensions explicitly.

    """

    def __init__(self, data, bitdepth=16, extrainfo=None):
        self.bitdepth = bitdepth
        self.data = data
        self.extrainfo = extrainfo or {}

    def _pack(self, n):
        n = int(round(n))
        if self.bitdepth == 16:
            data = struct.pack(">H", n)
        elif self.bitdepth == 8:
            data = n.to_bytes(1, "big")
        else:
            raise ValueError("Unsupported bitdepth: %r" % self.bitdepth)
        return data

    def _write_dpx(self, stream, dimensions=None):
        # Very helpful: http://www.fileformat.info/format/dpx/egff.htm
        # http://www.simplesystems.org/users/bfriesen/dpx/S268M_Revised.pdf

        # Generic file header (768 bytes)
        stream.write(b"SDPX")  # Magic number
        stream.write(struct.pack(">I", 8192))  # Offset to image data
        stream.write(b"V2.0\0\0\0\0")  # ASCII version

        # Optimize for single color
        optimize = len(self.data) == 1 and len(self.data[0]) == 1 and dimensions

        # Image data
        imgdata = []
        # 10-bit code adapted from GraphicsMagick dpx.c:WriteSamples
        if self.bitdepth == 10:
            shifts = (22, 12, 2)  # RGB
        for _i, scanline in enumerate(self.data):
            if self.bitdepth == 10:
                packed = []
                for RGB in scanline:
                    packed_u32 = 0
                    for datum, sample in enumerate(RGB):
                        packed_u32 |= sample << shifts[datum]
                    packed.append(struct.pack(">I", packed_u32))
                scanline = b"".join(packed)
            else:
                scanline = b"".join(
                    b"".join(self._pack(v) for v in RGB) for RGB in scanline
                )
            if not optimize:
                # Pad lines with binary zeros so they end on 4-byte boundaries
                scanline = scanline.ljust(
                    int(math.ceil(len(scanline) / 4.0)) * 4, b"\0"
                )
            imgdata.append(scanline)
        imgdata = "".join(imgdata)
        if optimize:
            # Optimize for single color
            imgdata *= dimensions[0]
            # Pad lines with binary zeros so they end on 4-byte boundaries
            imgdata = imgdata.ljust(int(math.ceil(len(imgdata) / 4.0)) * 4, b"\0")
            imgdata *= dimensions[1]
            w, h = dimensions
        else:
            w, h = len(self.data[0]), len(self.data)

        # Generic file header (cont.)
        stream.write(struct.pack(">I", 8192 + len(imgdata)))  # File size
        stream.write(b"\0\0\0\1")  # DittoKey (1 = not same as previous frame)
        stream.write(
            struct.pack(">I", 768 + 640 + 256)
        )  # Generic section header length
        stream.write(
            struct.pack(">I", 256 + 128)
        )  # Industry-specific section header length
        stream.write(struct.pack(">I", 0))  # User-defined data length
        stream.write(safe_str(stream.name or b"").ljust(100, b"\0")[-100:])  # File name
        # Date & timestamp
        tzoffset = round(
            (time.mktime(time.localtime()) - time.mktime(time.gmtime())) / 60.0 / 60.0
        )
        if tzoffset < 0:
            tzoffset = b"%.2i" % tzoffset
        else:
            tzoffset = b"+%.2i" % tzoffset
        stream.write(
            time.strftime("%Y:%m:%d:%H:%M:%S").encode() + tzoffset.encode() + b"\0\0"
        )
        stream.write(
            safe_str(b"%s %s" % (appname, version)).ljust(100, b"\0")
        )  # Creator
        stream.write(b"\0" * 200)  # Project
        stream.write(b"\0" * 200)  # Copyright
        stream.write(b"\xff" * 4)  # EncryptKey 0xffffffff = not encrypted
        stream.write(b"\0" * 104)  # Reserved

        # Generic image header (640 bytes)
        stream.write(b"\0\0")  # Orientation 0 = left to right, top to bottom
        stream.write(b"\0\1")  # Number of image elements
        stream.write(struct.pack(">I", w))  # Pixels per line
        stream.write(struct.pack(">I", h))  # Lines per image element

        # Generic image header - image element
        stream.write(b"\0" * 4)  # 0 = unsigned data
        stream.write(b"\0" * 4)  # Reference low data code value
        stream.write(b"\xff" * 4)  # Reference low quantity
        stream.write(
            struct.pack(">I", 2**self.bitdepth - 1)
        )  # Reference high data code value
        stream.write(b"\xff" * 4)  # Reference high quantity
        stream.write(chr(50).encode())  # Descriptor 50 = RGB
        stream.write(b"\2")  # Transfer 2 = linear
        stream.write(b"\2")  # Colorimetric 2 = not applicable
        stream.write(chr(self.bitdepth).encode())  # BitSize
        stream.write(b"\0\1")  # Packing 1 = filled 32-bit words
        stream.write(b"\0\0")  # Encoding 0 = not encoded
        stream.write(struct.pack(">I", 8192))  # Image data offset
        stream.write(b"\0" * 4)  # End of line padding
        stream.write(b"\0" * 4)  # End of image padding
        stream.write(b"RGB / Linear".ljust(32, b"\0"))  # Description

        # Seven additional unused image elements
        stream.write(b"\0" * 72 * 7)

        # Generic image header (cont.)
        stream.write(b"\0" * 52)  # Reserved

        # Generic image source header (256 bytes)
        sw, sh = [
            self.extrainfo.get("original_" + dim, locals()[dim[0]])
            for dim in ("width", "height")
        ]
        # X offset
        stream.write(struct.pack(">I", self.extrainfo.get("offset_x", (sw - w) / 2)))
        # Y offset
        stream.write(struct.pack(">I", self.extrainfo.get("offset_y", (sh - h) / 2)))
        # X center
        stream.write(struct.pack(">f", self.extrainfo.get("center_x", sw / 2.0)))
        # Y center
        stream.write(struct.pack(">f", self.extrainfo.get("center_y", sh / 2.0)))
        stream.write(struct.pack(">I", sw))  # X original size
        stream.write(struct.pack(">I", sh))  # Y original size
        stream.write(b"\0" * 100)  # Source image file name
        stream.write(b"\0" * 24)  # Source image date & timestamp
        stream.write(b"\0" * 32)  # Input device name
        stream.write(b"\0" * 32)  # Input device serial number
        stream.write(b"\0" * 2 * 4)  # Border
        stream.write(b"\0\0\0\1" * 2)  # Pixel aspect ratio
        stream.write(b"\xff" * 4)  # X scanned size
        stream.write(b"\xff" * 4)  # Y scanned size
        stream.write(b"\0" * 20)  # Reserved

        # Industry-specific film info header (256 bytes)
        stream.write(b"\0" * 2)  # Film mfg. ID code
        stream.write(b"\0" * 2)  # Film type
        stream.write(b"\0" * 2)  # Offset in perfs
        stream.write(b"\0" * 6)  # Prefix
        stream.write(b"\0" * 4)  # Count
        stream.write(b"\0" * 32)  # Format
        # Frame position in sequence
        stream.write(struct.pack(">I", self.extrainfo.get("frame_position", 2**32 - 1)))
        # Sequence length
        stream.write(
            struct.pack(">I", self.extrainfo.get("sequence_length", 2**32 - 1))
        )
        # Held count
        stream.write(struct.pack(">I", self.extrainfo.get("held_count", 1)))
        # Frame rate of original
        if "frame_rate" in self.extrainfo:
            stream.write(struct.pack(">f", self.extrainfo["frame_rate"]))
        else:
            stream.write(b"\xff" * 4)
        # Shutter angle of camera in degrees
        stream.write(b"\xff" * 4)
        stream.write(b"\0" * 32)  # Frame identification - e.g. keyframe
        stream.write(b"\0" * 100)  # Slate
        stream.write(b"\0" * 56)  # Reserved

        # Industry-specific TV info header (128 bytes)
        # SMPTE time code
        stream.write(
            b"".join(
                chr(int(str(v), 16)).encode()
                for v in self.extrainfo.get("timecode", [b"ff"] * 4)
            )
        )
        stream.write(b"\xff" * 4)  # User bits
        stream.write(b"\xff")  # Interlace
        stream.write(b"\xff")  # Field number
        stream.write(b"\xff")  # Video signal standard
        stream.write(b"\0")  # Zero for byte alignment
        stream.write(b"\xff" * 4)  # H sampling rate Hz
        stream.write(b"\xff" * 4)  # V sampling rate Hz
        # Temporal sampling or frame rate Hz
        if "frame_rate" in self.extrainfo:
            stream.write(struct.pack(">f", self.extrainfo["frame_rate"]))
        else:
            stream.write(b"\xff" * 4)
        stream.write(b"\xff" * 4)  # Time offset in ms from sync to 1st pixel
        stream.write(b"\xff" * 4)  # Gamma
        stream.write(b"\xff" * 4)  # Black level code value
        stream.write(b"\xff" * 4)  # Black gain
        stream.write(b"\xff" * 4)  # Breakpoint
        stream.write(b"\xff" * 4)  # Reference white level code value
        stream.write(b"\xff" * 4)  # Integration time in s
        stream.write(b"\0" * 76)  # Reserved

        # Padding so image data begins at 8K boundary
        stream.write("\0" * 6144)

        # Write image data
        stream.write(imgdata)

    def _write_png(self, stream, dimensions=None):
        # Header
        stream.write(b"\x89PNG\r\n\x1a\n")
        # IHDR image header length
        stream.write(struct.pack(">I", 13))
        # IHDR image header chunk type
        ihdr = [b"IHDR"]
        # Optimize for single color
        optimize = len(self.data) == 1 and len(self.data[0]) == 1 and dimensions
        # IHDR: width, height
        if optimize:
            w, h = dimensions
        else:
            w, h = len(self.data[0]), len(self.data)
        ihdr.extend([struct.pack(">I", w), struct.pack(">I", h)])
        # IHDR: Bit depth
        ihdr.append(self.bitdepth.to_bytes(1, "big"))
        # IHDR: Color type 2 (truecolor)
        ihdr.append(b"\2")
        # IHDR: Compression method 0 (deflate)
        ihdr.append(b"\0")
        # IHDR: Filter method 0 (adaptive)
        ihdr.append(b"\0")
        # IHDR: Interlace method 0 (none)
        ihdr.append(b"\0")
        ihdr = b"".join(ihdr)
        stream.write(ihdr)
        stream.write(struct.pack(">I", zlib.crc32(ihdr) & 0xFFFFFFFF))
        # IDAT image data chunk type
        imgdata = []
        for _i, scanline in enumerate(self.data):
            # Add a scanline, filter type 0
            imgdata.append(b"\0")
            for RGB in scanline:
                RGB = b"".join(self._pack(v) for v in RGB)
                if optimize:
                    RGB *= dimensions[0]
                imgdata.append(RGB)
        imgdata = b"".join(imgdata)
        if optimize:
            imgdata *= dimensions[1]
        imgdata = zlib.compress(imgdata, 9)
        stream.write(struct.pack(">I", len(imgdata)))
        idat = [b"IDAT"]
        idat.append(imgdata)
        idat = b"".join(idat)
        stream.write(idat)
        stream.write(struct.pack(">I", zlib.crc32(idat) & 0xFFFFFFFF))
        # IEND chunk
        stream.write(b"\0" * 4)
        stream.write(b"IEND")
        stream.write(struct.pack(">I", zlib.crc32(b"IEND") & 0xFFFFFFFF))

    def _write_tiff(self, stream, dimensions=None):
        # Very helpful: http://www.fileformat.info/format/tiff/corion.htm

        # Image data
        if len(self.data) == 1 and len(self.data[0]) == 1 and dimensions:
            # Optimize for single color
            w, h = dimensions
            imgdata = list(self.data)
            imgdata[0] *= w
            imgdata *= h
        else:
            imgdata = self.data
            w, h = len(self.data[0]), len(self.data)

        samples_per_pixel = len(self.data[0][0])

        # Header
        stream.write(tiff_get_header(w, h, samples_per_pixel, self.bitdepth))

        # Write image data
        for _i, scanline in enumerate(imgdata):
            for sample in scanline:
                stream.write(b"".join(self._pack(v) for v in sample))

    def write(self, stream_or_filename, format=None, dimensions=None):
        if not format:
            if isinstance(stream_or_filename, str):
                format = os.path.splitext(stream_or_filename)[1].lstrip(".").upper()
                if format == "TIF":
                    format += "F"
            else:
                format = "PNG"
        if not hasattr(self, "_write_" + format.lower()):
            raise ValueError("Unsupported format: %r" % format)
        if isinstance(stream_or_filename, str):
            stream = open(stream_or_filename, "wb")
        else:
            stream = stream_or_filename
        with stream:
            getattr(self, "_write_" + format.lower())(stream, dimensions)