File size: 2,980 Bytes
932db78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import io
import math
import zlib
import base64
import pickle
import numpy as np
from PIL import Image

from os import PathLike
from typing import Any

HEADER_SIZE = 4  # bytes


def object_to_image(obj: Any) -> Image.Image:
    """Take a object and convert it to an image.
    
    The object is first pickled to bytes, then the array is padded and reshaped
    into a NxNx4 array and converted to an RGBA image

    Example of 1d pickle array to image array:
    [1,2,3,4,5,6,7] ->
    [
        [[0,0,0,7], [1,2,3,4]],
        [[5,6,7,0], [0,0,0,0]]
    ]
    The array begins with a 4 byte header representing the length of the data.
    Zeroes are added to the end the ensure that the number pixels is a suare number
    """
    data = compress(pickle.dumps(obj))
    header = len(data).to_bytes(length=HEADER_SIZE, byteorder='big')
    data = header + data

    # divide the data into pixels, add an extra if data doesn't perfectly fit
    whole_pixels, remainder = divmod(len(data), 4)
    n_pixels = whole_pixels + 1 * (remainder != 0)

    # ensure n_pixels is a square number
    side_length = math.ceil(math.sqrt(n_pixels))
    n_pixels = side_length ** 2
    n_bytes = n_pixels * 4

    # right pad the data with zeros so it can be shaped to (n,n,4)
    data += b'\x00' * (n_bytes - len(data))
    
    # create (n,n,4) array from pickle data
    data_arr = np.frombuffer(data, dtype=np.uint8)
    img_arr = np.reshape(data_arr, (side_length, side_length, 4))

    return Image.fromarray(img_arr)


def image_to_object(image: Image.Image) -> Any:
    """Take a PIL Image and unpickle it's data to an object
    
    Convert the image to an array, flatten to obtain serial bytes, then unpickle
    these bytes.
    """
    data_arr = np.array(image).flatten()
    data = data_arr.tobytes()

    # number of bytes containing meaningful data
    length = int.from_bytes(data[:HEADER_SIZE], 'big')

    # slice off header
    data = data[HEADER_SIZE:]
    # slice off zero padding if any
    data = data[:length]

    return pickle.loads(decompress(data))


def image_to_b64_string(img: Image.Image) -> str:
    """Return a str representing the image as b64 encoded bytes"""
    # save the image as PNG to a buffer
    buffer = io.BytesIO()
    img.save(buffer, 'png')
    buffer.seek(0)

    return base64.b64encode(buffer.read()).decode()


def bytes_to_image(b: bytes) -> Image.Image:
    """Create an Image using raw bytes"""
    buffer = io.BytesIO(b)
    buffer.seek(0)
    
    return Image.open(buffer)


def compress(b: bytes, level=9) -> bytes:
    """Compress the bytes using zlib"""
    return zlib.compress(b, level)


def decompress(b: bytes) -> bytes:
    """Decompress the bytes using zlib"""
    return zlib.decompress(b)


def write_compressed(data: bytes, fp: PathLike) -> None:
    with open(fp, 'wb') as f:
        f.write(compress(data))


def read_compressed(fp: PathLike) -> bytes:
    with open(fp, 'rb') as f:
        return decompress(f.read())