Skip to content

Instantly share code, notes, and snippets.

@jaames
Created February 17, 2018 17:07
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jaames/aa77713839c1dc948eefd445442bf606 to your computer and use it in GitHub Desktop.
Save jaames/aa77713839c1dc948eefd445442bf606 to your computer and use it in GitHub Desktop.
flipnote studio comment ppm (assumes one frame, single layer, black pen) -> image
#!/usr/bin/python
# Comment PPM -> NPF or Image script for Sudomemo
# github.com/Sudomemo | www.sudomemo.net
#
# Written by James Daniel
# github.com/jaames | rakujira.jp
#
# Command Line Args:
#
# Pen Color - (optional) use before opening to set the PPM pen color to any RGB value
# <-p | --pencolor> <r> <g> <b>
#
# BG Color - (optional) use before opening to set the PPM paper color to any RGB value
# <-b | --bgcolor> <r> <g> <b>
#
# Input - Open a comment PPM and parse into an image
# <-i | --input> <path>
#
# Resize - (optional) use after opening to resize the image
# <-r | --resize> <new width> <new height>
#
# Crop - (optional) use after opening to crop the image, pass in the amount to crop off each side
# <-c | --crop> <left> <upper> <right> <bottom>
#
# Output - output the resulting image to file, where type = npf to output an npf, or png / gif / jpeg etc. to output a regular image
# <-o | --output> <type> <path>
#
# Base64 Output - base64 encode the image data and print it to stdout, where type = npf to output an npf, or png / gif / jpeg etc. to output a regular image
# <-b64 | --base64> <type>
#
#
# Quick Examples:
#
# Read PPM, resize to 128x96, crop 12 pixels off the bottom, then print out base64 npf data (for DSi site thumbnails)
# python3 commentImage.py -i path/to/comment.ppm -r 128 96 -c 0 0 0 12 -b64 npf
#
# Read PPM, crop off bottom 24 pixels, hen print out base64 png data
# python3 commentImage.py -i path/to/comment.ppm -c 0 0 0 24 -b64 png (for sude theatre thumbnails)
import numpy as np
from PIL import Image
class commentImage:
def __init__(self, ppmBuffer, penColor=(14, 14, 14), paperColor=(255, 250, 250)):
# pen color, default is rgb(14, 14, 14):
self.penColor = bytes(penColor)
# paper color, default is rgb(250, 250, 250):
self.paperColor = bytes(paperColor)
# unpack ppm to self.image
self.unpackPPM(ppmBuffer)
# unpack the first layer of the first frame
def unpackPPM(self, ppm):
# Color indecies
paperColor = b"\x00"
penColor = b"\x01"
# Frame image data will go here
frame = np.full((192, 256), paperColor, dtype="V1")
# Jump to start of frame section
# + skip the header + frame offset table length + frame flag byte
ppm.seek(0x06A0 + 8 + 4 + 1)
# Read line encoding for layer 1
lineEncoding = int.from_bytes(ppm.read(48), byteorder='little')
# Skip the line encoding for layer 2
ppm.seek(48, 1)
# Loop through each line
# We read the line encoding value from the right
# Because we shift the value two bits to the right after reading each line, there will eventually be a point where it equals 0
# If the line encoding equals 0, then all unread lines left in the layer are type 0
# A type 0 line is empty and has no data, aand therefore we can skip looping over the rest since they're empty
line = 0
while lineEncoding > 0:
# Get line encoding by reading the rightmost 2 bits
lineType = lineEncoding & 0x03
# Type 0 - empty line
if lineType == 0:
pass
# Type 1 - compressed line
elif lineType == 1:
# Line header tells us which chunks are used
lineHeader = np.fromstring(ppm.read(4), dtype=">u4")
# Pixel position along the line
pix = 0
# Loop through each bit in the line header
# Each bit represts whether a 8-pixel chunk is stored or whether it is blank
while lineHeader & 0xFFFFFFFF:
# if chunk is used
if lineHeader & 0x80000000:
# read chunk
chunkByte = ord(ppm.read(1))
# unpack chunk into bits
for _ in range(8):
if chunkByte & 0x01 == 1:
frame[line][pix] = penColor
pix += 1
chunkByte >>= 1
# else if chunk is not used, these 8 pixels are blank
else:
pix += 8
lineHeader <<= 1
# Type 2 - inverse compressed line
elif lineType == 2:
# Line header tells us which chunks are used
lineHeader = np.fromstring(ppm.read(4), dtype=">u4")
# invert the line
frame[line] = np.full((256), penColor, dtype="V1")
# Pixel position along the line
pix = 0
# Loop through each bit in the line header
# Each bit represts whether a 8-pixel chunk is stored or whether it is blank
while lineHeader & 0xFFFFFFFF:
# if chunk is used
if lineHeader & 0x80000000:
# read chunk
chunkByte = ord(ppm.read(1))
# unpack chunk into bits
for _ in range(8):
if chunkByte & 0x01 == 0:
frame[line][pix] = paperColor
pix += 1
chunkByte >>= 1
# else if chunk is not used, these 8 pixels are blank
else:
pix += 8
lineHeader <<= 1
# Type 3 - raw line data
elif lineType == 3:
# in this type, there is no line header as all chunks are used
# So unpack them all at once:
lineData = np.fromstring(ppm.read(32), dtype=np.uint8)
# Loop through each chunck:
pix = 0
for chunkByte in lineData:
# unpack chunk into bits
for _ in range(8):
if chunkByte & 0x01 == 1:
frame[line][pix] = penColor
pix += 1
chunkByte >>= 1
line += 1
# Shift the line encoding right by two bits
# Now the two rightmost bits represent the encoding for the next line
lineEncoding >>= 2
self.image = Image.fromarray(frame, "P")
self.image.putpalette(bytes(self.paperColor + self.penColor))
# resize image
def resize(self, size):
self.image = self.image.convert("RGB")
self.image = self.image.resize(size, Image.BICUBIC)
# crop image
def crop(self, bounds):
w, h = self.image.size
left, upper, right, lower = bounds
self.image = self.image.crop((left, upper, w-right, h-lower))
# write image data to byte buffer
def writeImageData(self, buffer, extention="png"):
self.image.save(buffer, extention)
# write NPF image data to byte buffer
def writeNpfData(self, buffer):
# convert image to 15-color palleted format
image = self.image.convert("P", palette=Image.ADAPTIVE, colors=15)
# get image pallette
palette = np.reshape(image.getpalette()[0:15*3], (-1, 3))
# get the image data as an array of pallette indecies
image = np.reshape(image.getdata(), (-1, 2))
# convert the pallete colors to RGB555
paletteData = np.fromiter((((col[0] >> 3) | ((col[1] & 0xF8) << 2) | ((col[2] & 0xF8) << 7) | 0x00) for col in palette), dtype=np.uint16)
# insert 0 as the first pallete entry -- it's never used
paletteData = np.insert(paletteData, 0, 0)
# convert image data
imageData = np.fromiter(((pix[0]+1) | ((pix[1]+1) << 4) for pix in image), dtype=np.uint8)
# write header
buffer.write(b"UGAR")
buffer.write(np.array([2, len(paletteData)*2, len(imageData)], dtype=np.uint32).tobytes())
# write palette
buffer.write(paletteData.tobytes())
# write imagedata
buffer.write(imageData.tobytes())
if __name__ == "__main__":
from io import BytesIO
from base64 import b64encode
from sys import argv, stdout
# default pen and paper colors
penColor = (14, 14, 14)
bgColor = (250, 250, 250)
if (len(argv)) < 2:
print("commentImage.py")
print("Comment PPM (one frame, one layer) -> NPF or Image")
print("")
print("Command Line Args:")
print("")
print(" Pen Color - (optional) use before opening to set the PPM pen color to any RGB value")
print(" <-p | --pencolor> <r> <g> <b>")
print("")
print(" BG Color - (optional) use before opening to set the PPM paper color to any RGB value")
print(" <-b | --bgcolor> <r> <g> <b>")
print("")
print(" Input - Open a comment PPM and parse into an image")
print(" <-i | --input> <path>")
print("")
print(" Resize - (optional) use after opening to resize the image")
print(" <-r | --resize> <new width> <new height>")
print("")
print(" Crop - (optional) use after opening to crop the image, pass in the amount to crop off each side")
print(" <-c | --crop> <left> <upper> <right> <bottom>")
print("")
print(" Output - output the resulting image to file, where type = npf to output an npf, or png / gif / jpeg etc. to output a regular image")
print(" <-o | --output> <type> <path>")
print("")
print(" Base64 Output - base64 encode the image data and print it to stdout, where type = npf to output an npf, or png / gif / jpeg etc. to output a regular image")
print(" <-b64 | --base64> <type>")
else:
i = 0
while i < len(argv):
arg = argv[i]
# set pen color
if arg in ["-p", "--pencolor"]:
penColor = (int(argv[i+1]), int(argv[i+2]), int(argv[i+3]))
i += 3
# set paper color
elif arg in ["-b", "--bgcolor"]:
bgColor = (int(argv[i+1]), int(argv[i+2]), int(argv[i+3]))
i += 3
# open + parse ppm
elif arg in ["-i", "--input"]:
with open(argv[i+1], "rb") as ppm:
comment = commentImage(ppm, penColor, bgColor)
i += 2
# resize image
elif arg in ["-r", "--resize"]:
comment.resize((int(argv[i+1]), int(argv[i+2])))
i += 3
# crop image
elif arg in ["-c", "--crop"]:
comment.crop((int(argv[i+1]), int(argv[i+2]), int(argv[i+3]), int(argv[i+4])))
i += 5
# output file
elif arg in ["-o", "--output"]:
with open(argv[i+2], "wb") as outFile:
if argv[i+1] == "npf":
comment.writeNpfData(outFile)
else:
comment.writeImageData(outFile, argv[i+1])
i += 3
# outbut base64 to stdout
elif arg in ["-b64", "--base64"]:
with BytesIO() as buffer:
if argv[i+1] == "npf":
comment.writeNpfData(buffer)
stdout.buffer.write(b64encode(buffer.getvalue()))
else:
comment.writeImageData(buffer, argv[i+1])
stdout.buffer.write(b64encode(buffer.getvalue()))
i += 2
else:
i += 1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment