textpixel
This commit is contained in:
Voussoir 2015-05-17 23:48:27 -07:00
parent 98146327d7
commit 2a0017371e
6 changed files with 205 additions and 2 deletions

24
TextPixel/README.md Normal file
View file

@ -0,0 +1,24 @@
TextPixel
==============
Encode / Decode between strings and PNG images. Can be imported or used on the commandline. Since it uses one channel to store each character, this program is only compatible with characters between 0 and 255 in unicode.
One channel is used to store the character, and the other two are randomized, so the output looks different every time.
python example:
encoded_string = textpixel.encode_string('Wow, look!')
textpixel.write_pixels(encoded_string, 'wowlook.png')
commandline example:
> textpixel encode bears.txt bears
Done.
> textpixel decode bears
Once upon a time there
was a book.
It was about bears.
So many bears.

BIN
TextPixel/bears.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

5
TextPixel/bears.txt Normal file
View file

@ -0,0 +1,5 @@
Once upon a time there
was a book.
It was about bears.
So many bears.

BIN
TextPixel/example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

169
TextPixel/textpixel.py Normal file
View file

@ -0,0 +1,169 @@
import sys
from PIL import Image as PILImage
import random
import string
ARGS_ENCODE = ['encode', 'save', 'write']
ARGS_DECODE = ['decode', 'read']
# Alternate operation names for commandline use.
def png_filename(filename):
if filename[-4:].lower() != '.png':
filename += '.png'
return filename
def encode_character(text, densitymin=0, densitymax=300):
'''
Given a character, take its ord() value and return an
RGB pixel representation.
The RGB channels will become the ord value, a number
lower than the ord value, and a number higher than the
ord value, though the order will be randomized.
`densitymin` and `densitymax` control how much lower and higher
the additional values will be relative to the ord value, in %.
Returns a tuple (R,G,B)
'''
densitymin = abs(densitymin)
densitymax = abs(densitymax)
asciivalue = ord(text)
percentmin = int((asciivalue * densitymin) / 100)
percentmax = int((asciivalue * densitymax) / 100)
lowerfill = random.randint(asciivalue - percentmax, asciivalue - percentmin)
upperfill = random.randint(asciivalue + percentmin, asciivalue + percentmax)
lowerfill = max(0, lowerfill)
upperfill = min(255, upperfill)
rgb = [lowerfill, asciivalue, upperfill]
random.shuffle(rgb)
return tuple(rgb)
def encode_string(text, densitymin=0, densitymax=300):
'''
Given a string `text`, map each character to an RGB
pixel, and return these pixels so they can be written to a file.
`densitymin` and `densitymax` are passed to encode_character
Returns a list where
[0] is (width,height),
[1] is a dict of pixels: {(x,y) : (R,G,B)}
'''
text = text.strip()
lines = text.split('\n')
encoded_height = len(lines)
encoded_width = max([len(line) for line in lines])
encoded_pixels = {}
for y in range(encoded_height):
line = lines[y]
for x in range(len(line)):
character = line[x]
pixel = encode_character(character, densitymin, densitymax)
encoded_pixels[(x,y)] = pixel
dim = (encoded_width, encoded_height)
out = [dim, encoded_pixels]
return out
def write_pixels(dimout, filename):
'''
Given pixel data of the form returned by `encode_string()`,
write it to a PNG file named `filename`.
'''
filename = png_filename(filename)
dim = dimout[0]
encoded_pixels = dimout[1]
image = PILImage.new('RGBA', dim)
for pixel in encoded_pixels:
image.putpixel(pixel, encoded_pixels[pixel])
image.save(filename)
##############################################################################
def decode_pixel(pixel):
'''
Given a tuple (R,G,B), return the decoded text character.
The character is the str() for the middle value of the tuple.
("middle" referring to numeric value, not index)
'''
pixel = sorted(list(pixel))
character = chr(pixel[1])
return character
def decode_image(image):
'''
Given an image, return the decoded string
`image` may be a string representing the filename
or a PIL Image object
'''
if isinstance(image, str):
if image[-4:].lower() != '.png':
image += '.png'
image = png_filename(image)
image = PILImage.open(image)
width = image.size[0]
height = image.size[1]
decoded_string = ''
for y in range(height):
for x in range(width):
pixel = image.getpixel((x,y))
if pixel[3] == 0:
break
decoded_string += decode_pixel(pixel)
decoded_string += '\n'
decoded_string = decoded_string.strip()
return decoded_string
def argsfail():
print('\ninvalid parameters.')
print('> textpixel.py encode text filename')
print('> textpixel.py decode filename')
quit()
if __name__ == '__main__':
if len(sys.argv) < 3:
argsfail()
op = sys.argv[1].lower()
if op in ARGS_ENCODE and len(sys.argv) == 4:
text = sys.argv[2]
if text[-4:].lower() == '.txt':
try:
tfile = open(text, 'r')
text = tfile.read()
tfile.close()
except FileNotFoundError:
pass
filename = sys.argv[3]
pixels = encode_string(text)
write_pixels(pixels, filename)
print('Done.')
elif op in ARGS_DECODE and len(sys.argv) == 3:
filename = sys.argv[2]
print(decode_image(filename))
else:
argsfail()

View file

@ -252,12 +252,14 @@ def handle_twitter(url, customname=None):
try:
link = pagedata.split('data-url="')[1]
link = link.split('"')[0]
if link != url:
handle_master(link, customname=customname)
return
except IndexError:
try:
link = pagedata.split('data-expanded-url="')[1]
link = link.split('"')[0]
if link != url:
handle_master(link, customname=customname)
return
except IndexError:
@ -366,6 +368,9 @@ def test_twitter():
# Twitter plain text
handle_master('https://twitter.com/cp_orange_x3/status/599700702382817280')
# Twitter plain text
handle_master('https://twitter.com/SyriacMFS/status/556513635913437184')
def test_generic():
# Some link that might work
handle_master('https://raw.githubusercontent.com/voussoir/reddit/master/SubredditBirthdays/show/statistics.txt')