Pure Python library for saving and loading PNG images without external dependencies
—
High-level utility functions for quick PNG creation and manipulation without requiring detailed format knowledge or class instantiation. These functions provide the simplest interface for common PNG operations.
The primary convenience function for creating PNG images from Python data structures.
def from_array(a, mode=None, info={}):
"""
Create PNG Image from 2D array or nested sequences.
Parameters:
- a: 2D array-like structure containing pixel data
- mode: str, color mode specification:
- 'L': Greyscale (1 value per pixel)
- 'LA': Greyscale with alpha (2 values per pixel)
- 'RGB': Red, green, blue (3 values per pixel)
- 'RGBA': Red, green, blue, alpha (4 values per pixel)
- None: Auto-detect based on data structure
- info: dict, additional PNG metadata options
Returns:
Image: PNG Image object with save() and write() methods
"""
# Alternative name for from_array
fromarray = from_arrayDirect PNG file creation by writing chunk data for advanced users who need complete control over PNG structure.
def write_chunks(out, chunks):
"""
Create PNG file by writing chunk data directly.
Parameters:
- out: file-like object opened in binary write mode
- chunks: iterable of (chunk_type, chunk_data) tuples
Note: This is a low-level function. PNG signature is written automatically,
but all required chunks (IHDR, IDAT, IEND) must be provided.
"""Important module-level constants and utility information for PNG manipulation.
import collections
# Module version
__version__: str = "0.20220715.0"
# PNG file signature (8-byte header that identifies PNG files)
signature: bytes = b'\x89PNG\r\n\x1a\n'
# Adam7 interlacing pattern coordinates (xstart, ystart, xstep, ystep)
adam7: tuple = ((0, 0, 8, 8), (4, 0, 8, 8), (0, 4, 4, 8),
(2, 0, 4, 4), (0, 2, 2, 4), (1, 0, 2, 2), (0, 1, 1, 2))
# Alternative name for from_array function (PIL compatibility)
fromarray = from_array
# Named tuple for resolution metadata (from pHYs chunk)
Resolution = collections.namedtuple('_Resolution', 'x y unit_is_meter')import png
# Create greyscale image from simple 2D list
grey_pixels = [
[0, 64, 128, 192, 255], # Row 1: gradient from black to white
[255, 192, 128, 64, 0], # Row 2: gradient from white to black
[128, 128, 128, 128, 128] # Row 3: uniform grey
]
# Create and save PNG (mode automatically detected as 'L')
image = png.from_array(grey_pixels, 'L')
image.save('simple_grey.png')import png
# Create RGB image with explicit color values
rgb_pixels = [
[255, 0, 0, 0, 255, 0, 0, 0, 255], # Red, Green, Blue pixels
[255, 255, 0, 255, 0, 255, 0, 255, 255], # Yellow, Magenta, Cyan pixels
[255, 255, 255, 0, 0, 0, 128, 128, 128] # White, Black, Grey pixels
]
# Create RGB PNG
image = png.from_array(rgb_pixels, 'RGB')
image.save('rgb_colors.png')import png
# Create RGBA image with transparency
rgba_pixels = [
[255, 0, 0, 255, 255, 0, 0, 128, 255, 0, 0, 0], # Red: opaque, half, transparent
[0, 255, 0, 255, 0, 255, 0, 128, 0, 255, 0, 0], # Green: opaque, half, transparent
[0, 0, 255, 255, 0, 0, 255, 128, 0, 0, 255, 0] # Blue: opaque, half, transparent
]
# Create RGBA PNG
image = png.from_array(rgba_pixels, 'RGBA')
image.save('rgba_transparency.png')import png
# Let from_array auto-detect the color mode
pixels_1_channel = [[100, 150, 200]] # Detected as 'L' (greyscale)
pixels_2_channel = [[100, 255, 150, 128]] # Detected as 'LA' (greyscale + alpha)
pixels_3_channel = [[255, 0, 0, 0, 255, 0]] # Detected as 'RGB'
pixels_4_channel = [[255, 0, 0, 255, 0, 255, 0, 128]] # Detected as 'RGBA'
# Auto-detection based on values per pixel
image1 = png.from_array(pixels_1_channel) # Mode: 'L'
image2 = png.from_array(pixels_2_channel) # Mode: 'LA'
image3 = png.from_array(pixels_3_channel) # Mode: 'RGB'
image4 = png.from_array(pixels_4_channel) # Mode: 'RGBA'
image1.save('auto_grey.png')
image2.save('auto_grey_alpha.png')
image3.save('auto_rgb.png')
image4.save('auto_rgba.png')import png
import numpy as np
# Create NumPy array
width, height = 64, 64
x, y = np.meshgrid(np.linspace(0, 1, width), np.linspace(0, 1, height))
# Generate mathematical pattern
pattern = np.sin(x * 10) * np.cos(y * 10)
# Scale to 0-255 range
pattern = ((pattern + 1) * 127.5).astype(np.uint8)
# Convert to PNG (NumPy arrays work directly)
image = png.from_array(pattern, 'L')
image.save('numpy_pattern.png')
# For RGB, reshape or create 3D array
rgb_pattern = np.zeros((height, width * 3), dtype=np.uint8)
rgb_pattern[:, 0::3] = pattern # Red channel
rgb_pattern[:, 1::3] = pattern // 2 # Green channel
rgb_pattern[:, 2::3] = 255 - pattern # Blue channel
rgb_image = png.from_array(rgb_pattern, 'RGB')
rgb_image.save('numpy_rgb_pattern.png')import png
# 16-bit greyscale data (values 0-65535)
high_depth_pixels = [
[0, 16383, 32767, 49151, 65535], # Full 16-bit range
[65535, 49151, 32767, 16383, 0] # Reverse gradient
]
# Specify 16-bit depth in info dict
info = {'bitdepth': 16}
image = png.from_array(high_depth_pixels, 'L', info)
image.save('16bit_grey.png')
# For 16-bit RGB, values are still 0-65535 per channel
rgb_16bit = [
[65535, 0, 0, 0, 65535, 0, 0, 0, 65535], # Bright RGB
[32767, 32767, 32767, 16383, 16383, 16383] # Two greys
]
info_rgb = {'bitdepth': 16}
rgb_image = png.from_array(rgb_16bit, 'RGB', info_rgb)
rgb_image.save('16bit_rgb.png')import png
# Create palette-based image data (indices into palette)
palette_indices = [
[0, 1, 2, 1, 0],
[1, 2, 3, 2, 1],
[2, 3, 0, 3, 2]
]
# Define palette and include in info
palette = [(255, 0, 0), (0, 255, 0), (0, 0, 255), (255, 255, 0)]
info = {'palette': palette}
# Create palette PNG
image = png.from_array(palette_indices, mode=None, info=info)
image.save('palette_convenience.png')import png
import struct
import zlib
def create_minimal_png(width, height, pixel_data):
"""Create PNG using low-level chunk writing"""
# IHDR chunk data
ihdr_data = struct.pack('>IIBBBBB', width, height, 8, 2, 0, 0, 0)
# IDAT chunk - compress pixel data
# Add filter bytes (0 = no filter) to each row
filtered_data = b''
for row in pixel_data:
filtered_data += b'\x00' # Filter type
filtered_data += bytes(row)
idat_data = zlib.compress(filtered_data)
# Create chunk list
chunks = [
(b'IHDR', ihdr_data),
(b'IDAT', idat_data),
(b'IEND', b'')
]
# Write PNG file
with open('minimal.png', 'wb') as f:
png.write_chunks(f, chunks)
# Create simple 2x2 RGB image
pixel_data = [
[255, 0, 0, 0, 255, 0], # Red, Green
[0, 0, 255, 255, 255, 255] # Blue, White
]
create_minimal_png(2, 2, pixel_data)import png
try:
# Invalid mode
invalid_pixels = [[255, 0, 0]]
image = png.from_array(invalid_pixels, 'INVALID_MODE')
except png.ProtocolError as e:
print(f"Protocol error: {e}")
try:
# Inconsistent row lengths
inconsistent_pixels = [
[255, 0, 0], # 3 values
[0, 255, 0, 128] # 4 values - inconsistent!
]
image = png.from_array(inconsistent_pixels, 'RGB')
except png.ProtocolError as e:
print(f"Data format error: {e}")
try:
# Empty or invalid data
empty_pixels = []
image = png.from_array(empty_pixels)
except (png.ProtocolError, ValueError) as e:
print(f"Invalid data: {e}")Install with Tessl CLI
npx tessl i tessl/pypi-pypng