# Copyright 2018 The glTF-Blender-IO authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import typing import struct import re import zlib import numpy as np class ImageData: """Contains channels of an image with raw pixel data.""" # TODO: refactor to only operate on numpy arrays # FUTURE_WORK: as a method to allow the node graph to be better supported, we could model some of # the node graph elements with numpy functions def __init__(self, name: str, filepath: str, width: int, height: int, source: int, target: int, source_length: int, channels: typing.Optional[typing.List[np.ndarray]] = []): if width <= 0 or height <= 0: raise ValueError("Image data can not have zero width or height") if source + source_length > 4: raise ValueError("Source image data can not have more than 4 channels") if target + source_length > 4: raise ValueError("Target image data can not have more than 4 channels") self.channels = [None, None, None, None] for index in range(source, source + source_length): self.channels[target + index - source] = channels[index] self.name = name self.filepath = filepath self.width = width self.height = height def add_to_image(self, target: int, image_data): if self.width != image_data.width or self.height != image_data.height: raise ValueError("Image dimensions do not match") if target < 0 or target > 3: raise ValueError("Can't insert image: channels out of bounds") if len(image_data.channels) != 4: raise ValueError("Can't insert image: incomplete image") if self.name != image_data.name: self.name += image_data.name self.filepath = "" # Replace channel. self.channels[target] = image_data.channels[target] @property def r(self): if len(self.channels) <= 0: return None return self.channels[0] @property def g(self): if len(self.channels) <= 1: return None return self.channels[1] @property def b(self): if len(self.channels) <= 2: return None return self.channels[2] @property def a(self): if len(self.channels) <= 3: return None return self.channels[3] def get_extension(self): allowed_extensions = ['.png', '.jpg', '.jpeg'] fallback_extension = allowed_extensions[0] matches = re.findall(r'\.\w+$', self.filepath) extension = matches[0] if len(matches) > 0 else fallback_extension return extension if extension.lower() in allowed_extensions else fallback_extension def to_image_data(self, mime_type: str) -> bytes: if mime_type == 'image/png': return self.to_png_data() raise ValueError("Unsupported image file type {}".format(mime_type)) def to_png_data(self) -> bytes: channels = self.channels # if there is no data, create a single pixel image if not channels: channels = np.ones((1, 1)) # fill all channels of the png for _ in range(4 - len(channels)): channels.append(np.ones_like(channels[0])) else: template_index = None for index in range(0, 4): if channels[index] is not None: template_index = index break for index in range(0, 4): if channels[index] is None: channels[index] = np.ones_like(channels[template_index]) image = np.concatenate(channels, axis=1) image = image.flatten() image = (image * 255.0).astype(np.uint8) buf = image.tobytes() # # Taken from 'blender-thumbnailer.py' in Blender. # # reverse the vertical line order and add null bytes at the start width_byte_4 = self.width * 4 raw_data = b"".join( b'\x00' + buf[span:span + width_byte_4] for span in range( (self.height - 1) * self.width * 4, -1, - width_byte_4)) def png_pack(png_tag, data): chunk_head = png_tag + data return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head)) return b"".join([ b'\x89PNG\r\n\x1a\n', png_pack(b'IHDR', struct.pack("!2I5B", self.width, self.height, 8, 6, 0, 0, 0)), png_pack(b'IDAT', zlib.compress(raw_data)), png_pack(b'IEND', b'')])