Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BIP2 #14

Merged
merged 11 commits into from
May 3, 2021
2 changes: 1 addition & 1 deletion bip/t3dn_bip/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
'''Load image previews in parallel and in any resolution. Supports BIP files.'''

__version__ = '0.0.6'
__version__ = '0.0.7'
23 changes: 14 additions & 9 deletions bip/t3dn_bip/previews.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,27 +126,30 @@ def _load_fallback(
preview = self._collection.load(name, filepath, filetype)

if not self._lazy_load:
preview.image_size[:] # Force Blender to load this preview now.
preview.icon_size[:] # Force Blender to load this icon now.
preview.image_size[:] # Force Blender to load this image now.

return preview

def _load_eager(self, name: str, filepath: str) -> ImagePreview:
'''Load image contents from file and load preview.'''
size, pixels = load_file(filepath, self._max_size)
data = load_file(filepath, self._max_size)

preview = self.new(name)
preview.image_size = size
preview.image_pixels = pixels
preview.icon_size = data['icon_size']
preview.icon_pixels = data['icon_pixels']
preview.image_size = data['image_size']
preview.image_pixels = data['image_pixels']

return preview

def _load_async(self, name: str, filepath: str, event: Event):
'''Load image contents from file and queue preview load.'''
if not event.is_set():
size, pixels = load_file(filepath, self._max_size)
data = load_file(filepath, self._max_size)

if not event.is_set():
self._queue.put((name, size, pixels, event))
self._queue.put((name, data, event))

def _timer(self):
'''Load queued image contents into previews.'''
Expand Down Expand Up @@ -175,13 +178,15 @@ def _timer(self):

return delay

def _load_queued(self, name: str, size: tuple, pixels: list, event: Event):
def _load_queued(self, name: str, data: dict, event: Event):
'''Load queued image contents into preview.'''
if not event.is_set():
if name in self:
preview = self[name]
preview.image_size = size
preview.image_pixels = pixels
preview.icon_size = data['icon_size']
preview.icon_pixels = data['icon_pixels']
preview.image_size = data['image_size']
preview.image_pixels = data['image_pixels']

def clear(self):
'''Clear all previews.'''
Expand Down
110 changes: 70 additions & 40 deletions bip/t3dn_bip/utils.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
from __future__ import annotations

import bpy
import io
import sys
import site
import subprocess
import importlib.util
from pathlib import Path
from zlib import decompress
from array import array
from typing import Tuple

USER_SITE = site.getusersitepackages()

Expand Down Expand Up @@ -46,72 +44,105 @@ def install_pillow():
def can_load(filepath: str) -> bool:
'''Return whether an image can be loaded.'''
with open(filepath, 'rb') as bip:
magic = bip.read(4)

if magic == b'BIP1':
if bip.read(4) == b'BIP2':
return True

return support_pillow()


def load_file(filepath: str, max_size: tuple) -> Tuple[tuple, list]:
def load_file(filepath: str, max_size: tuple) -> dict:
'''Load image preview data from file.

Args:
filepath: The input file path.
max_size: Scale images above this size down.

Returns:
The size and pixels of the image.
A dictionary with icon_size, icon_pixels, image_size, image_pixels.

Raises:
AssertionError: If pixel data type is not 32 bit.
AssertionError: If pixel count does not match size.
ValueError: If file is not BIP and Pillow is not installed.
'''
with open(filepath, 'rb') as bip:
magic = bip.read(4)

if magic == b'BIP1':
width = int.from_bytes(bip.read(2), 'big')
height = int.from_bytes(bip.read(2), 'big')
data = decompress(bip.read())

if support_pillow() and _should_resize((width, height), max_size):
image = Image.frombytes('RGBa', (width, height), data)
if bip.read(4) == b'BIP2':
count = int.from_bytes(bip.read(1), 'big')
bonjorno7 marked this conversation as resolved.
Show resolved Hide resolved
assert count > 0, 'the file contains no images'

icon_size = [int.from_bytes(bip.read(2), 'big') for _ in range(2)]
icon_length = int.from_bytes(bip.read(4), 'big')
bip.seek(8 * (count - 2), io.SEEK_CUR)
image_size = [int.from_bytes(bip.read(2), 'big') for _ in range(2)]
image_length = int.from_bytes(bip.read(4), 'big')

icon_content = decompress(bip.read(icon_length))
bip.seek(-image_length, io.SEEK_END)
image_content = decompress(bip.read(image_length))

if support_pillow() and _should_resize(image_size, max_size):
image = Image.frombytes('RGBa', image_size, image_content)
image = _resize_image(image, max_size)

width, height = image.size
data = image.tobytes()

pixels = array('i', data)
assert pixels.itemsize == 4, '32 bit type required for pixels'

count = width * height
assert len(pixels) == count, 'unexpected amount of pixels'

return ((width, height), pixels)
image_size = image.size
image_content = image.tobytes()

icon_pixels = array('i', icon_content)
assert icon_pixels.itemsize == 4, 'unexpected bytes per pixel'
length = icon_size[0] * icon_size[1]
assert len(icon_pixels) == length, 'unexpected amount of pixels'

image_pixels = array('i', image_content)
assert image_pixels.itemsize == 4, 'unexpected bytes per pixel'
length = image_size[0] * image_size[1]
assert len(image_pixels) == length, 'unexpected amount of pixels'

return {
'icon_size': icon_size,
'icon_pixels': icon_pixels,
'image_size': image_size,
'image_pixels': image_pixels,
}

if support_pillow():
with Image.open(filepath) as image:
image = image.transpose(Image.FLIP_TOP_BOTTOM)

if _should_resize(image.size, max_size):
image = _resize_image(image, max_size)

image = image.transpose(Image.FLIP_TOP_BOTTOM)

# Image modes ending with A have an alpha channel.
if not image.mode.endswith(('A', 'a')):
bonjorno7 marked this conversation as resolved.
Show resolved Hide resolved
# No need to pre-multiply alpha for an opaque image.
image = image.convert('RGBA')

# If we do have an alpha channel.
elif image.mode != 'RGBa':
# Then pre-multiply it.
image = image.convert('RGBa')

pixels = array('i', image.tobytes())
assert pixels.itemsize == 4, '32 bit type required for pixels'
image_pixels = array('i', image.tobytes())
assert image_pixels.itemsize == 4, 'unexpected bytes per pixel'
length = image.size[0] * image.size[1]
assert len(image_pixels) == length, 'unexpected amount of pixels'

data = {
'icon_size': image.size,
'icon_pixels': image_pixels,
'image_size': image.size,
'image_pixels': image_pixels,
}

if _should_resize(image.size, (32, 32)):
icon = image.resize(size=(32, 32))

icon_pixels = array('i', icon.tobytes())
assert icon_pixels.itemsize == 4, 'unexpected bytes per pixel'
length = icon.size[0] * icon.size[1]
assert len(icon_pixels) == length, 'unexpected amount of pixels'

count = image.size[0] * image.size[1]
assert len(pixels) == count, 'unexpected amount of pixels'
data['icon_size'] = icon.size
data['icon_pixels'] = icon_pixels

return (image.size, pixels)
return data

raise ValueError('input is not a supported file format')

Expand All @@ -127,16 +158,15 @@ def _should_resize(size: tuple, max_size: tuple) -> bool:
return False


def _resize_image(image: Image.Image, max_size: tuple) -> Image.Image:
def _resize_image(image: 'Image.Image', max_size: tuple) -> 'Image.Image':
bonjorno7 marked this conversation as resolved.
Show resolved Hide resolved
'''Resize image to fit inside maximum.'''
scale = min(
max_size[0] / image.size[0] if max_size[0] else 1,
max_size[1] / image.size[1] if max_size[1] else 1,
)

width = int(image.size[0] * scale)
height = int(image.size[1] * scale)
return image.resize(size=(width, height))
size = [int(n * scale) for n in image.size]
return image.resize(size=size)


def tag_redraw():
Expand Down
2 changes: 1 addition & 1 deletion bip_converter/t3dn_bip_converter/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
'''Convert between BIP files and various image formats.'''

__version__ = '0.0.6'
__version__ = '0.0.7'
48 changes: 32 additions & 16 deletions bip_converter/t3dn_bip_converter/convert.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import io
from typing import Union
from pathlib import Path
from PIL import Image
Expand All @@ -24,38 +25,53 @@ def convert_file(src: Union[str, Path], dst: Union[str, Path] = None):
raise ValueError('exactly one file must be in BIP format')


def _image_to_bip(src: Path, dst: Path):
def _image_to_bip(src: Union[str, Path], dst: Union[str, Path]):
'''Convert various image formats to BIP.'''
with Image.open(src) as image:
image = image.transpose(Image.FLIP_TOP_BOTTOM)
image = image.convert('RGBa')

width, height = image.size
data = image.tobytes()
# Image modes ending with A have an alpha channel.
if not image.mode.endswith(('A', 'a')):
bonjorno7 marked this conversation as resolved.
Show resolved Hide resolved
# No need to pre-multiply alpha for an opaque image.
image = image.convert('RGBA')
# If we do have an alpha channel.
elif image.mode != 'RGBa':
# Then pre-multiply it.
image = image.convert('RGBa')

images = [image.resize(size=(32, 32)), image]
contents = [compress(image.tobytes()) for image in images]

with open(dst, 'wb') as output:
output.write(b'BIP1')
output.write(b'BIP2')
output.write(len(images).to_bytes(1, 'big'))

output.write(width.to_bytes(2, 'big'))
output.write(height.to_bytes(2, 'big'))
for image, content in zip(images, contents):
for number in image.size:
output.write(number.to_bytes(2, 'big'))
output.write(len(content).to_bytes(4, 'big'))

output.write(compress(data))
for content in contents:
output.write(content)


def _bip_to_image(src: Path, dst: Path):
def _bip_to_image(src: Union[str, Path], dst: Union[str, Path]):
'''Convert BIP to various image formats.'''
with open(src, 'rb') as bip:
magic = bip.read(4)

if magic != b'BIP1':
if bip.read(4) != b'BIP2':
raise ValueError('input is not a supported file format')

width = int.from_bytes(bip.read(2), 'big')
height = int.from_bytes(bip.read(2), 'big')
count = int.from_bytes(bip.read(1), 'big')
bonjorno7 marked this conversation as resolved.
Show resolved Hide resolved
assert count > 0, 'the file contains no images'
bip.seek(8 * (count - 1), io.SEEK_CUR)

size = [int.from_bytes(bip.read(2), 'big') for _ in range(2)]
length = int.from_bytes(bip.read(4), 'big')

data = decompress(bip.read())
bip.seek(-length, io.SEEK_END)
content = decompress(bip.read(length))

image = Image.frombytes('RGBa', (width, height), data)
image = Image.frombytes('RGBa', size, content)
image = image.convert('RGBA')
image = image.transpose(Image.FLIP_TOP_BOTTOM)

Expand Down
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/alpha/rainbow.bip
Git LFS file not shown
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/bip/00.bip
Git LFS file not shown
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/bip/01.bip
Git LFS file not shown
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/bip/02.bip
Git LFS file not shown
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/bip/03.bip
Git LFS file not shown
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/bip/04.bip
Git LFS file not shown
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/bip/05.bip
Git LFS file not shown
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/bip/06.bip
Git LFS file not shown
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/bip/07.bip
Git LFS file not shown
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/bip/08.bip
Git LFS file not shown
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/bip/09.bip
Git LFS file not shown
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/bip/10.bip
Git LFS file not shown
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/bip/11.bip
Git LFS file not shown
4 changes: 2 additions & 2 deletions example/t3dn_bip_example/images/bip/12.bip
Git LFS file not shown
Loading