25

I have an application where I would like to be able to generate PNG images from data in Python. I've done some searching and found "PIL" which looked pretty outdated. Is there some other library that would be better for this?

1
  • 1
    you use good old PIL (if you are not already developping beyond py3.2) Commented Dec 18, 2011 at 20:22

6 Answers 6

62

Simple PNG files can be generated quite easily from pure Python code - all you need is the standard zlib module and some bytes-encoding to write the chunks. Here is a complete example that the casual reader may use as a starter for their own png generator:

#! /usr/bin/python
""" Converts a list of list into gray-scale PNG image. """
__copyright__ = "Copyright (C) 2014 Guido Draheim"
__licence__ = "Public Domain"

import zlib
import struct

def makeGrayPNG(data, height = None, width = None):
    def I1(value):
        return struct.pack("!B", value & (2**8-1))
    def I4(value):
        return struct.pack("!I", value & (2**32-1))
    # compute width&height from data if not explicit
    if height is None:
        height = len(data) # rows
    if width is None:
        width = 0
        for row in data:
            if width < len(row):
                width = len(row)
    # generate these chunks depending on image type
    makeIHDR = True
    makeIDAT = True
    makeIEND = True
    png = b"\x89" + "PNG\r\n\x1A\n".encode('ascii')
    if makeIHDR:
        colortype = 0 # true gray image (no palette)
        bitdepth = 8 # with one byte per pixel (0..255)
        compression = 0 # zlib (no choice here)
        filtertype = 0 # adaptive (each scanline seperately)
        interlaced = 0 # no
        IHDR = I4(width) + I4(height) + I1(bitdepth)
        IHDR += I1(colortype) + I1(compression)
        IHDR += I1(filtertype) + I1(interlaced)
        block = "IHDR".encode('ascii') + IHDR
        png += I4(len(IHDR)) + block + I4(zlib.crc32(block))
    if makeIDAT:
        raw = b""
        for y in xrange(height):
            raw += b"\0" # no filter for this scanline
            for x in xrange(width):
                c = b"\0" # default black pixel
                if y < len(data) and x < len(data[y]):
                    c = I1(data[y][x])
                raw += c
        compressor = zlib.compressobj()
        compressed = compressor.compress(raw)
        compressed += compressor.flush() #!!
        block = "IDAT".encode('ascii') + compressed
        png += I4(len(compressed)) + block + I4(zlib.crc32(block))
    if makeIEND:
        block = "IEND".encode('ascii')
        png += I4(0) + block + I4(zlib.crc32(block))
    return png

def _example():
    with open("cross3x3.png","wb") as f:
        f.write(makeGrayPNG([[0,255,0],[255,255,255],[0,255,0]]))
Sign up to request clarification or add additional context in comments.

3 Comments

A little more depth than most of us, probably including OP, are looking for, but +1 for sharing so much information. This is great reference material if I ever want to go deeper with pngs.
This example is awesome. Any change you can change that to show how to add a palette (for some basic red/green/etc colors and have an alpha channel)?
I wanted to provide a very short answer. Obviously there are quite a number of options to build and encode a picture. Luckily the PNG standard is quite clear on what to put into each chunk (w3.org/TR/PNG). To avoid bit stuffing one would simply look for the options to always encode a value into exactly one byte. ///// For an RGBA that can be very simple: a PLTE chunk has no encoding options => it is always exactly 8bit per color channel, not more and not less. So each entry is 4 bytes (R,G,B,A). The IHDR bitdepth refers to the IDAT - with 8bit one can write just index bytes again.
21

Here's a Python3 example:

import png

width = 255
height = 255
img = []
for y in range(height):
    row = ()
    for x in range(width):
        row = row + (x, max(0, 255 - x - y), y)
    img.append(row)
with open('gradient.png', 'wb') as f:
    w = png.Writer(width, height, greyscale=False)
    w.write(f, img)

1 Comment

This answer does not list the package name.
14

Is there some other library that would be better for this?

The png package would be a reasonable common choice.

Here's the project description:

PyPNG allows PNG image files to be read and written using pure Python.

Comments

2

After doing some searching, I found this site: https://www.daniweb.com/programming/software-development/code/454765/save-a-pygame-drawing. It just uses good old pygame.

Line 37: (pg.image.save(win, fname)) saves whatever is drawn on the pygame surface as a file.

EDIT

Here is the actual code:

"""pg_draw_circle_save101.py
draw a blue solid circle on a white background
save the drawing to an image file
for result see http://prntscr.com/156wxi
tested with Python 2.7 and PyGame 1.9.2 by vegaseat  16may2013
"""

import pygame as pg

# pygame uses (r, g, b) color tuples
white = (255, 255, 255)
blue = (0, 0, 255)

width = 300
height = 300

# create the display window
win = pg.display.set_mode((width, height))
# optional title bar caption
pg.display.set_caption("Pygame draw circle and save")
# default background is black, so make it white
win.fill(white)

# draw a blue circle
# center coordinates (x, y)
center = (width//2, height//2)
radius = min(center)
# width of 0 (default) fills the circle
# otherwise it is thickness of outline
width = 0
# draw.circle(Surface, color, pos, radius, width)
pg.draw.circle(win, blue, center, radius, width)

# now save the drawing
# can save as .bmp .tga .png or .jpg
fname = "circle_blue.png"
pg.image.save(win, fname)
print("file {} has been saved".format(fname))

# update the display window to show the drawing
pg.display.flip()


# (press escape key or click window title bar x to exit)
while True:
for event in pg.event.get():
    if event.type == pg.QUIT:
        # most reliable exit on x click
        pg.quit()
        raise SystemExit
    elif event.type == pg.KEYDOWN:
        # optional exit with escape key
        if event.key == pg.K_ESCAPE:
            pg.quit()
            raise SystemExit

1 Comment

@DanielM Thank you for the tip, I will change it to include the actual code.
0
import matplotlib.pyplot as plt
import numpy as np

# Create the figure and axis
fig, ax = plt.subplots(figsize=(10, 8))

# Building outline (a two-floor building)
# Outer building boundary
building = plt.Rectangle((0.05, 0.05), 0.9, 0.9, fill=None, edgecolor='black', linewidth=3)
ax.add_patch(building)

# Floor division: horizontal line dividing floors
ax.plot([0.05, 0.95], [0.5, 0.5], color='black', linewidth=3)

# Vertical divisions for rooms on each floor
# Top floor: two rooms (left and right)
ax.plot([0.525, 0.525], [0.5, 0.95], color='black', linewidth=3)
# Bottom floor: three rooms (left, center, right)
ax.plot([0.35, 0.35], [0.05, 0.5], color='black', linewidth=3)
ax.plot([0.65, 0.65], [0.05, 0.5], color='black', linewidth=3)

# Function to draw a sound wave within a given room
def draw_sound_wave(x_start, x_end, y_base, amplitude, frequency, phase, color, linestyle='-'):
    x = np.linspace(x_start, x_end, 500)
    y = amplitude * np.sin(2 * np.pi * frequency * (x - x_start) + phase) + y_base
    ax.plot(x, y, color=color, linewidth=2, linestyle=linestyle)

# Top floor: left room (scientific interpretation: precise sine wave)
draw_sound_wave(0.07, 0.51, 0.75, 0.03, 4, 0, 'red')

# Top floor: right room (artistic interpretation: modulated wave with phase shift)
draw_sound_wave(0.54, 0.93, 0.75, 0.04, 6, np.pi/3, 'blue', linestyle='--')

# Bottom floor: left room (clean sine wave)
draw_sound_wave(0.07, 0.33, 0.3, 0.02, 8, np.pi/4, 'green')

# Bottom floor: center room (combination of two waves for a complex pattern)
draw_sound_wave(0.36, 0.64, 0.3, 0.02, 10, np.pi/2, 'purple')
draw_sound_wave(0.36, 0.64, 0.3, 0.015, 15, 0, 'orange', linestyle=':')

# Bottom floor: right room (abstract pattern)
draw_sound_wave(0.67, 0.93, 0.3, 0.03, 5, np.pi/6, 'magenta')

# Adding text labels for clarity
ax.text(0.5, 0.98, 'Building Sound Visualization', ha='center', fontsize=16, weight='bold', transform=ax.transAxes)
ax.text(0.3, 0.83, 'Scientific', ha='center', fontsize=12, color='red')
ax.text(0.8, 0.83, 'Artistic', ha='center', fontsize=12, color='blue')
ax.text(0.2, 0.38, 'Clean Wave', ha='center', fontsize=10, color='green')
ax.text(0.5, 0.38, 'Complex Pattern', ha='center', fontsize=10, color='purple')
ax.text(0.8, 0.38, 'Abstract', ha='center', fontsize=10, color='magenta')

# Remove ticks and set axis limits
ax.set_xticks([])
ax.set_yticks([])
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.set_aspect('equal')

# Save the image as PNG
plt.savefig("building_sound_visualization.png", dpi=300, bbox_inches='tight')
plt.show()

1 Comment

Hello, and welcome to Stack Overflow! Please add some explanation to your code, with regards to what it is doing and how it solves the problem.
-5

I'm aware that this question was asked over 9 years ago, but in the age of Python 3.8, you can just open the file in "Writing Binary" mode:

f = open("filename.png", 'wb')
# data being whatever data you wanted to write
f.write(data)

1 Comment

It doesn't address the actual problem though.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.