@wizard, are you saying we could have more pixels in the preview? If you’re willing to accept the knock-on impact on the pattern library or editor UI code, it might be nice if the preview matched the pixelCount
of the device. Plenty of off-the-shelf strings (144/m), matrices (8x32, 16x16), and rings (241, 256, fibonacci) have more than 100 pixels.
@scruffynerf, I’m not using the pixel map at the moment (just transposing each pixel into a rectangular height
x width
grid) but if @wizard wants to tell me how to get the final map coordinates from the pixelmap.dat
blob I could try to put the pixels in the right place for more complex layouts.
~-~-~-~-~
Anyway, if the previews will be fixed eventually then I might as well release it now. The forum won’t let me post .PY or .ZIP files, so here it is as a text blob:
animatePreview.py
import argparse, os, io, json, base64, struct
from PIL import Image
# ------------------------------------------------
# Here's where the magic happens.
def animatePreview(patternPreview, patternWidth, patternHeight, patternFrames, outputScale, borderColor, filenameBase, verbose):
# Unpack the bencode'd string into an Image so we can process it as RGB pixels.
with Image.open(io.BytesIO(base64.b64decode(patternPreview))) as imgPreview:
# Calculate how big things need to be.
if patternWidth == 0:
patternWidth = imgPreview.width
if patternFrames == 0:
patternFrames = imgPreview.height
outputWidth = patternWidth * (1 + outputScale) + 1
outputHeight = patternHeight * (1 + outputScale) + 1
# Debugging output
if verbose:
print(" imageWidth: ", imgPreview.width)
print(" imageHeight: ", imgPreview.height)
print(" patternWidth: ", patternWidth)
print(" patternHeight: ", patternHeight)
print(" patternFrames: ", patternFrames)
print(" outputScale: ", outputScale)
print(" outputWidth: ", outputWidth)
print(" outputHeight: ", outputHeight)
# Start pantographing pixels from the JPG into the animated PNG.
animationFrames = []
for iterRow in range(patternFrames):
# Create a new blank frame.
animationFrame = Image.new('RGB', (outputWidth, outputHeight), (borderColor, borderColor, borderColor))
maxBrightness = 0
for iterCol in range(patternWidth * patternHeight):
#for iterCol in range(min(patternWidth * patternHeight, imgPreview.width)):
r, g, b = imgPreview.getpixel((iterCol, iterRow))
maxBrightness = max(maxBrightness, r, g, b)
pixelX = 1 + (iterCol % patternWidth) * (1 + outputScale)
pixelY = 1 + (iterCol // patternWidth) * (1 + outputScale)
for hPixel in range(outputScale):
for vPixel in range(outputScale):
animationFrame.putpixel((pixelX + hPixel, pixelY + vPixel), (r, g, b))
# save the frame.
animationFrames.append(animationFrame)
# save the output file.
outputFilename = filenameBase + '.png'
if True or verbose:
print("Saving", len(animationFrames), "frames to", outputFilename)
animationFrames[0].save(outputFilename, save_all=True, append_images=animationFrames[1:], duration=40, loop=0)
# Test for excessive dimming of preview image.
if maxBrightness < 128:
print("Warning: the brightest pixel in the preview image is less than 50%")
print(" ...to get an accurate representation of the pattern, it is necessary ")
print(" ...to set both the Brightness slider and maxBrightness (under Settings)")
print(" ...to 100%, and then save and re-export the pattern.")
# ------------------------------------------------
# main
if __name__ == "__main__":
# Parse command line.
parser = argparse.ArgumentParser()
parser.add_argument("patternFile", help="The pattern (EPE) to be animated")
parser.add_argument("--width", type=int, default=0, help="The width of the pattern (if less than the preview size of 100)")
parser.add_argument("--height", type=int, default=1, help="The height of the pattern if 2D; defaults to 1")
parser.add_argument("--frames", type=int, default=0, help="The number of frames to render (if less than the preview size of 150)")
parser.add_argument("--scale", type=int, default=5, help="How much to magnify each pixel of the preview; defaults to 5")
parser.add_argument("--border", type=int, default=63, help="8-bit grayscale value for the border lines; defaults to 63")
parser.add_argument("--verbose", action='store_true', help="Display debugging output")
args = parser.parse_args()
# Read the pattern archive (.EPE) file and pass the preview image to our converter.
with open(args.patternFile, 'rb') as inputFile:
# Debugging output
if args.verbose:
print("Extracting preview image from", args.patternFile)
EPE = json.load(inputFile)
# Check whether this is ought to be a 2D render, but isn't.
if "render2D(" in EPE['sources']['main']:
if args.height == 1:
print("Warning: this pattern contains a 2D renderer; if you want a 2D preview you need to specify the height.")
# Create the preview animation.
inFilename, inExtension = os.path.splitext(args.patternFile)
animatePreview(EPE['preview'], args.width, args.height, args.frames, args.scale, args.border, inFilename, args.verbose)
# close the input file.
inputFile.close()
I wrote it in Python because I also call it from inside the Python-based backup script I built using the Pixelblaze client libraries developed by @zranger1 and @Nick_W. Anyone on a Mac should already have all the dependencies to run this; anyone on a PC can use Python for Windows or run it in their favorite Linux distribution using WSL.
Syntax is as follows:
usage: animatePreview.py [-h] [--width WIDTH] [--height HEIGHT] [--frames FRAMES] [--scale SCALE]
[--border BORDER] [--verbose]
patternFile
positional arguments:
patternFile The pattern (EPE) to be animated
optional arguments:
-h, --help show this help message and exit
--width WIDTH The width of the pattern (if less than the preview size of 100)
--height HEIGHT The height of the pattern if 2D; defaults to 1
--frames FRAMES The number of frames to render (if less than the preview size of 150)
--scale SCALE How much to magnify each pixel of the preview; defaults to 5
--border BORDER 8-bit grayscale value for the border lines; defaults to 63
--verbose Display debugging output
Some examples:
$ python3 animatePreview.py Cylon.epe
Saving 150 frames to Cylon.png
There’s not much going on with the rightmost 36 pixels because the pattern file was exported from an 8x8 matrix. The animation also jumps at the end because the default preview length of 150 frames isn’t an integer multiple of the pattern length. That’s easily fixed:
$ python3 animatePreview.py Cylon.epe --width=64 --frames=136
Saving 136 frames to Cylon.png
Or how about a 2D pattern?
$ python3 animatePreview.py --border=32 Breakout.epe
Warning: this pattern contains a 2D renderer; if you want a 2D preview you need to specify the height.
Saving 150 frames to Breakout.png
Oops! Trying again with the --width
and --height
options to use the 2D renderer:
$ python3 animatePreview.py --border=32 --width=8 --height=8 Breakout.epe
Saving 150 frames to Breakout.png
And if you like bigger pixels and black borders:
$ python3 animatePreview.py --scale=10 --border=0 --width=8 --height=8 cube\ fire\ 3D.epe
Saving 150 frames to cube fire 3D.png
So there it is. If you give it a try, leave any feedback here.