Skip to content

Commit

Permalink
- BREAKING: Use 3D array to represent pixel data
Browse files Browse the repository at this point in the history
- WIP: Fix create_frame_from_rgb()
- Fix type hints
- Test build without compile on non-musl linux
  • Loading branch information
laggykiller committed Sep 23, 2023
1 parent e4bc7b8 commit 20a465b
Show file tree
Hide file tree
Showing 10 changed files with 124 additions and 108 deletions.
32 changes: 16 additions & 16 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ name: Build and upload to PyPI

on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
# tags:
# - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
# release:
# types:
# - [published]
Expand Down Expand Up @@ -139,18 +139,18 @@ jobs:
# password: ${{ secrets.TEST_PYPI_API_TOKEN }}
# repository_url: https://test.pypi.org/legacy/

upload_pypi:
needs: [build_wheels, build_sdist]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
with:
# unpacks default artifact into dist/
# if `name: artifact` is omitted, the action will create extra parent dir
name: artifact
path: dist
# upload_pypi:
# needs: [build_wheels, build_sdist]
# runs-on: ubuntu-latest
# steps:
# - uses: actions/download-artifact@v3
# with:
# # unpacks default artifact into dist/
# # if `name: artifact` is omitted, the action will create extra parent dir
# name: artifact
# path: dist

- uses: pypa/gh-action-pypi-publish@v1.5.0
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
# - uses: pypa/gh-action-pypi-publish@v1.5.0
# with:
# user: __token__
# password: ${{ secrets.PYPI_API_TOKEN }}
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ dist/
.py-build-cmake_cache/
fakeroot/
.vscode/
conan_output/
conan_output/
example/output/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ apng.assemble('result-from-file.apng')
apngasm.reset()
for file_name in sorted(os.listdir('frames')):
image = Image.open(os.path.join('frames', file_name)).convert('RGBA')
frame = create_frame_from_rgba(np.array(image).flatten(), image.width, image.height)
frame = create_frame_from_rgba(np.array(image), image.width, image.height)
frame.delay_num = 50
frame.delay_den = 1000
apngasm.add_frame(frame)
Expand Down
2 changes: 1 addition & 1 deletion docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ apng.assemble('result-from-file.apng')
apngasm.reset()
for file_name in sorted(os.listdir('frames')):
image = Image.open(os.path.join('frames', file_name)).convert('RGBA')
frame = create_frame_from_rgba(np.array(image).flatten(), image.width, image.height)
frame = create_frame_from_rgba(np.array(image), image.width, image.height)
frame.delay_num = 50
frame.delay_den = 1000
apngasm.add_frame(frame)
Expand Down
8 changes: 4 additions & 4 deletions example/example_binder.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,16 @@
# Assemble palette and grey PNGs
# You can use with statement to avoid calling reset()
with APNGAsmBinder() as apng:
apng.add_frame_from_file('input/palette.png')
apng.add_frame_from_file('input/grey.png')
apng.add_frame_from_file('input/palette.png', delay_num=1, delay_den=1)
apng.add_frame_from_file('input/grey.png', delay_num=1, delay_den=1)
success = apng.assemble('output/birds.apng')
print(f'{success = }')

# Assemble palette and grey PNGs, but with Pillow
image0 = Image.open('input/grey.png')
frame0 = apngasm.add_frame_from_pillow(image0)
frame0 = apngasm.add_frame_from_pillow(image0, delay_num=1, delay_den=1)
image1 = Image.open('input/palette.png')
apngasm.add_frame_from_pillow(image1)
apngasm.add_frame_from_pillow(image1, delay_num=1, delay_den=1)

success = apngasm.assemble('output/birds-pillow.apng')
print(f'{success = }')
14 changes: 7 additions & 7 deletions example/example_direct.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def frame_info(frame):
for file_name in sorted(os.listdir('frames')):
image = Image.open(os.path.join('frames', file_name))
image = image.rotate(angle)
frame = create_frame_from_rgba(np.array(image).flatten(), image.width, image.height)
frame = create_frame_from_rgba(np.array(image), image.width, image.height)
apngasm.add_frame(frame)

angle += angle_step
Expand All @@ -111,22 +111,22 @@ def frame_info(frame):
del apngasm

# Assemble palette and grey PNGs, but with Pillow
image0 = Image.open('input/grey.png').convert('RGBA')
frame0 = create_frame_from_rgba(np.array(image0).flatten(), image0.width, image0.height)
image0 = Image.open('input/grey.png').convert('RGB')
frame0 = create_frame_from_rgb(np.array(image0), image0.width, image0.height, np.array([0,0,0]), 1, 1)
frame_info(frame0)

# You may even set the variables manually
image1 = Image.open('input/palette.png').convert('RGBA')
frame1 = APNGFrame()
frame1.delay_num = 100
frame1.delay_den = 1000
frame1.delay_num = 1
frame1.delay_den = 1
frame1.color_type = color_type_dict[image1.mode]
frame1.width = image1.width
frame1.height = image1.height
frame1.pixels = np.array(image1).flatten()
frame1.pixels = np.array(image1)
frame_info(frame1)

# # Another way of creating APNGAsm object
# Another way of creating APNGAsm object
apngasm = APNGAsm([frame0, frame1])

success = apngasm.assemble('output/birds-pillow.apng')
Expand Down
8 changes: 6 additions & 2 deletions get_deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ def install_deps(arch):
settings.append('arch=' + arch)

build = []
if platform.system() == 'Linux':
# Need to compile dependencies if Linux
if (platform.system() == 'Linux' and
os.path.isdir('/lib') and
len([i for i in os.listdir('/lib') if i.startswith('libc.musl')]) != 0):

# Need to compile dependencies if musllinux
build.append('*')
elif (not shutil.which('cmake') and
(platform.architecture()[0] == '32bit' or
platform.machine().lower() not in (conan_archs['armv8'] + conan_archs['x86']))):

build.append('cmake*')

if build == []:
Expand Down
18 changes: 9 additions & 9 deletions src-python/apngasm_python/_apngasm_python/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class APNGAsm:
Class representing APNG file, storing APNGFrame(s) and other metadata.
"""

def __init__(self, frames: list[_apngasm_python.APNGFrame]) -> None:
def __init__(self, frames: List[_apngasm_python.APNGFrame]) -> None:
"""
Construct APNGAsm object from an existing vector of apngasm frames.
Expand Down Expand Up @@ -99,7 +99,7 @@ class APNGAsm:
"""
...

def disassemble(self, file_path: str) -> list[_apngasm_python.APNGFrame]:
def disassemble(self, file_path: str) -> List[_apngasm_python.APNGFrame]:
"""
Disassembles an APNG file.
Expand All @@ -119,7 +119,7 @@ class APNGAsm:
"""
...

def get_frames(self) -> list[_apngasm_python.APNGFrame]:
def get_frames(self) -> List[_apngasm_python.APNGFrame]:
"""
Returns the frame vector.
Expand All @@ -146,7 +146,7 @@ class APNGAsm:
"""
...

def load_animation_spec(self, file_path: str) -> list[_apngasm_python.APNGFrame]:
def load_animation_spec(self, file_path: str) -> List[_apngasm_python.APNGFrame]:
"""
Loads an animation spec from JSON or XML.
Loaded frames are added to the end of the frame vector.
Expand Down Expand Up @@ -363,7 +363,7 @@ class APNGFrame:
...

@property
def palette(self) -> object:
def palette(self) -> numpy.typing.NDArray:
"""
The palette data of frame. Only applies to 'P' mode Image (i.e. Not RGB, RGBA)
Expressed as 2D numpy array in format of [[r0, g0, b0], [r1, g1, b1], ..., [r255, g255, b255]] in Python
Expand Down Expand Up @@ -391,7 +391,7 @@ class APNGFrame:
...

@property
def pixels(self) -> object:
def pixels(self) -> numpy.typing.NDArray:
"""
The raw pixel data of frame, expressed as a 1D numpy array in Python.
Note that setting this value will also set the variable 'rows' internally.
Expand Down Expand Up @@ -419,7 +419,7 @@ class APNGFrame:
...

@property
def transparency(self) -> object:
def transparency(self) -> numpy.typing.NDArray:
"""
The transparency data of frame. Expressed as 1D numpy array.
"""
Expand Down Expand Up @@ -468,7 +468,7 @@ class IAPNGAsmListener:
"""
...

def create_frame_from_rgb(pixels: numpy.typing.NDArray, width: int, height: int, trns_color: numpy.typing.NDArray = 0, delay_num: int = 100, delay_den: int = 1000) -> object:
def create_frame_from_rgb(pixels: numpy.typing.NDArray, width: int, height: int, trns_color: numpy.typing.NDArray = 0, delay_num: int = 100, delay_den: int = 1000) -> _apngasm_python.APNGFrame:
"""
Creates an APNGFrame from a bitmapped array of RBG pixel data.
Expand All @@ -484,7 +484,7 @@ def create_frame_from_rgb(pixels: numpy.typing.NDArray, width: int, height: int,
"""
...

def create_frame_from_rgba(pixels: numpy.typing.NDArray, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> object:
def create_frame_from_rgba(pixels: numpy.typing.NDArray, width: int, height: int, delay_num: int = 100, delay_den: int = 1000) -> _apngasm_python.APNGFrame:
"""
Creates an APNGFrame from a bitmapped array of RBGA pixel data.
Expand Down
49 changes: 25 additions & 24 deletions src-python/apngasm_python/apngasm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from ._apngasm_python import APNGAsm, APNGFrame, IAPNGAsmListener, create_frame_from_rgb, create_frame_from_rgba
from ._apngasm_python import __version__
import numpy as np
import numpy.typing
from PIL import Image
from typing import Optional

Expand All @@ -28,35 +29,35 @@ def __enter__(self):
def __exit__(self, exc_type, exc_val, exc_tb):
self.apngasm.reset()

def frame_pixels_as_pillow(self, frame: int, new_value: Optional[list] = None) -> Optional[Image.Image]:
def frame_pixels_as_pillow(self, frame: int, new_value: Optional[Image.Image] = None) -> Optional[Image.Image]:
'''
Get/Set the raw pixel data of frame, expressed as a Pillow object.
This should be set AFTER you set the width, height and color_type.
:param int frame: Target frame number.
:param list new_value: (Optional) If set, then the raw pixel data of frame
:param Image.Image new_value: (Optional) If set, then the raw pixel data of frame
is set with this value.
:return: Pillow image object of the frame (get) or None (set)
:rtype: PIL.Image.Image or None
'''
if new_value:
self.apngasm.get_frames()[frame].pixels = np.array(new_value).flatten()
self.apngasm.get_frames()[frame].pixels = np.array(new_value)
else:
mode = self.color_type_dict[self.apngasm.get_frames()[frame].color_type]
return Image.frombytes(mode, (self.apngasm.get_frames()[frame].width, self.apngasm.get_frames()[frame].height), self.apngasm.get_frames()[frame].pixels)

def frame_pixels_as_numpy(self, frame: int, new_value: Optional[list] = None) -> Optional[np.ndarray]:
def frame_pixels_as_numpy(self, frame: int, new_value: Optional[numpy.typing.NDArray] = None) -> Optional[numpy.typing.NDArray]:
'''
Get/Set the raw pixel data of frame, expressed as a 1D numpy array.
Get/Set the raw pixel data of frame, expressed as a 3D numpy array.
This should be set AFTER you set the width, height and color_type.
:param int frame: Target frame number.
:param list new_value: (Optional) If set, then the raw pixel data of frame
:param numpy.typing.NDArray new_value: (Optional) If set, then the raw pixel data of frame
is set with this value.
:return: 1D numpy array representation of raw pixel data of frame (get) or None (set)
:rtype: numpy.ndarray or None
:return: 3D numpy array representation of raw pixel data of frame (get) or None (set)
:rtype: numpy.typing.NDArray or None
'''
if new_value:
self.apngasm.get_frames()[frame].pixels = new_value
Expand Down Expand Up @@ -117,33 +118,34 @@ def frame_color_type(self, frame: int, new_value: Optional[int] = None) -> Optio
else:
return self.apngasm.get_frames()[frame].color_type

def frame_palette(self, frame: int, new_value: Optional[np.ndarray] = None) -> Optional[np.ndarray]:
def frame_palette(self, frame: int, new_value: Optional[numpy.typing.NDArray] = None) -> Optional[numpy.typing.NDArray]:
'''
Get/Set the palette data of frame. Only applies to 'P' mode Image (i.e. Not RGB, RGBA)
Expressed as 2D numpy array in format of [[r0, g0, b0], [r1, g1, b1], ..., [r255, g255, b255]]
:param int frame: Target frame number.
:param np.ndarray new_value: (Optional) If set, then the palette data of frame
:param numpy.typing.NDArray new_value: (Optional) If set, then the palette data of frame
is set with this value.
:return: 2D numpy array representation of palette data of frame (get) or None (set)
:rtype: numpy.ndarray or None
:rtype: numpy.typing.NDArray or None
'''
if new_value:
self.apngasm.get_frames()[frame].palette = new_value
else:
return self.apngasm.get_frames()[frame].palette

def frame_transparency(self, frame: int, new_value: Optional[int] = None) -> Optional[np.ndarray]:
def frame_transparency(self, frame: int, new_value: Optional[numpy.typing.NDArray] = None) -> Optional[numpy.typing.NDArray]:
'''
Get/Set the transparency data of frame. Expressed as 1D numpy array.
Get/Set the color [r, g, b] to be treated as transparent in the frame, expressed as 1D numpy array.
For more info, refer to 'tRNS Transparency' in https://libpng.org/pub/png/spec/1.2/PNG-Chunks.html
:param int frame: Target frame number.
:param int new_value: (Optional) If set, then the transparency of frame
:param numpy.typing.NDArray new_value: (Optional) If set, then the transparency of frame
is set with this value.
:return: 1D numpy array representation of transparency data of frame (get) or None (set)
:rtype: numpy.ndarray or None
:return: The color [r, g, b] to be treated as transparent in the frame (get) or None (set)
:rtype: numpy.typing.NDArray or None
'''
if new_value:
self.apngasm.get_frames()[frame].transparency = new_value
Expand Down Expand Up @@ -249,27 +251,26 @@ def add_frame_from_pillow(self, pillow_image: Image.Image, delay_num: int = 100,
if pillow_image.mode not in ('RGB', 'RGBA'):
pillow_image = pillow_image.convert('RGBA')
return self.add_frame_from_numpy(
numpy_data=np.array(pillow_image).flatten(),
numpy_data=np.array(pillow_image),
width=pillow_image.width,
height=pillow_image.height,
mode=pillow_image.mode,
delay_num=delay_num,
delay_den=delay_den
)

def add_frame_from_numpy(self, numpy_data: int, width: int, height: int, trns_color: int = None,
def add_frame_from_numpy(self, numpy_data: numpy.typing.NDArray, width: int, height: int, trns_color: Optional[numpy.typing.NDArray] = None,
mode: str = 'RGBA', delay_num: int = 100, delay_den: int = 1000) -> int:
'''
Add frame from numpy array.
The frame duration is equal to delay_num / delay_den seconds.
Default frame duration is 100/1000 second, or 0.1 second.
:param int numpy_data: The pixel data, expressed as 1D numpy array.
For example: [r0, g0, b0, r1, g1, b1, ...]
:param numpy.typing.NDArray numpy_data: The pixel data, expressed as 3D numpy array.
:param int width: The width of the pixel data.
:param int height: The height of the pixel data.
:param str mode: The color mode of data. Possible values are RGB or RGBA.
:param int trns_color: An array of transparency data, expressed as 1D numpy array.
:param Optional[numpy.typing.NDArray] trns_color: The color [r, g, b] to be treated as transparent, expressed as 1D numpy array.
Only use if RGB mode.
:param int delay_num: The delay numerator for this frame (defaults to 100).
:param int delay_den: The delay denominator for this frame (defaults to 1000).
Expand Down Expand Up @@ -312,7 +313,7 @@ def assemble(self, output_path: str) -> bool:

def disassemble_as_numpy(self, file_path: str) -> list:
'''
Disassembles an APNG file to a list of frames, expressed as 1D numpy array.
Disassembles an APNG file to a list of frames, expressed as 3D numpy array.
:param str file_path: The file path to the PNG image to be disassembled.
Expand Down Expand Up @@ -419,12 +420,12 @@ def set_skip_first(self, skip_first: int):
'''
return self.apngasm.set_skip_first(skip_first)

def get_frames(self) -> np.ndarray:
def get_frames(self) -> numpy.typing.NDArray:
'''
Returns the frame vector.
:return: frame vector
:rtype: numpy.ndarray
:rtype: numpy.typing.NDArray
'''
return self.apngasm.get_frames()

Expand Down
Loading

0 comments on commit 20a465b

Please sign in to comment.