Created
February 17, 2018 17:07
-
-
Save jaames/aa77713839c1dc948eefd445442bf606 to your computer and use it in GitHub Desktop.
flipnote studio comment ppm (assumes one frame, single layer, black pen) -> image
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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