else
2
.gitignore
vendored
|
@ -1,5 +1,7 @@
|
||||||
AwfulCrateBox/
|
AwfulCrateBox/
|
||||||
Classifieds/
|
Classifieds/
|
||||||
|
Toddo/toddo.db
|
||||||
|
Meal/meal.db
|
||||||
|
|
||||||
# Windows image file caches
|
# Windows image file caches
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
102
DynamicColumnFormatter/dynamic_column_formatter.py
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
HEADER_TEXT = 'number name age address misc. "blank column?"'
|
||||||
|
BODY_TEXT = '''
|
||||||
|
4 "John Smith" 26 "123 north street"
|
||||||
|
17 "Jenny Smith" 8 "123 north street"
|
||||||
|
55 "Veronica Dove" 20 "456 west street"
|
||||||
|
77 "Austin Texas" 33 "789 south avenue"
|
||||||
|
89 "Mister Super Long Name" 123 "planet earth" "he's really old"
|
||||||
|
120 "Bill" "" "" "Deceased"
|
||||||
|
999 "Nine Nine" 999 "999 ninth boulevard" "favorite number is 9"
|
||||||
|
'''
|
||||||
|
BODY_TEXT = BODY_TEXT.strip()
|
||||||
|
DELIMITER = ' | '
|
||||||
|
|
||||||
|
output_text = ''
|
||||||
|
# Keep track of the longest entry in each column, to determine
|
||||||
|
# how wide we should make them.
|
||||||
|
# Also, look for numerical columns so we can right-justify them.
|
||||||
|
header = shlex.split(HEADER_TEXT)
|
||||||
|
column_widths = {index:len(item) for (index, item) in enumerate(header)}
|
||||||
|
column_types = {}
|
||||||
|
document_lines = [shlex.split(line) for line in BODY_TEXT.splitlines()]
|
||||||
|
for line in document_lines:
|
||||||
|
for (index, word) in enumerate(line):
|
||||||
|
current_width = column_widths.get(index, 0)
|
||||||
|
column_widths[index] = max(len(word), current_width)
|
||||||
|
|
||||||
|
try:
|
||||||
|
float(word)
|
||||||
|
except ValueError:
|
||||||
|
# It only takes one failure to make the whole column
|
||||||
|
# string type.
|
||||||
|
if word != "":
|
||||||
|
column_types[index] = 's'
|
||||||
|
|
||||||
|
# Move the dictionary into a list where the index is the column
|
||||||
|
# number, and the value is how wide it should be.
|
||||||
|
column_widths = list(column_widths.items())
|
||||||
|
column_widths.sort(key=lambda x: x[0])
|
||||||
|
column_widths = [x[1] for x in column_widths]
|
||||||
|
|
||||||
|
# Format column widths into a string which will become the basis
|
||||||
|
# for each row.
|
||||||
|
column_format = '{:%s%d%s}'
|
||||||
|
column_formats = []
|
||||||
|
for (index, width) in enumerate(column_widths):
|
||||||
|
formtype = column_types.get(index, 'g')
|
||||||
|
justify = '<' if formtype == 's' else '>'
|
||||||
|
form = column_format % (justify, width, formtype)
|
||||||
|
column_formats.append(form)
|
||||||
|
|
||||||
|
# Format the header.
|
||||||
|
column_count = len(column_widths)
|
||||||
|
diff = len(header) - column_count
|
||||||
|
if diff > 0:
|
||||||
|
# We have labels for columns that were empty.
|
||||||
|
column_count = len(header)
|
||||||
|
else:
|
||||||
|
diff *= -1
|
||||||
|
header += [''] * diff
|
||||||
|
|
||||||
|
for (index, label) in enumerate(header):
|
||||||
|
form = '{:<%ds}' % column_widths[index]
|
||||||
|
header[index] = form.format(label)
|
||||||
|
header = DELIMITER.join(header)
|
||||||
|
output_text += header + '\n'
|
||||||
|
|
||||||
|
# Format the rows.
|
||||||
|
for (rowindex, line) in enumerate(document_lines):
|
||||||
|
# Does this row need any blank columns?
|
||||||
|
diff = column_count - len(line)
|
||||||
|
if diff > 0:
|
||||||
|
line += [''] * diff
|
||||||
|
document_lines[rowindex] = line
|
||||||
|
|
||||||
|
# Format and replace it into the list.
|
||||||
|
for (columnindex, word) in enumerate(line):
|
||||||
|
if word == '':
|
||||||
|
line[columnindex] = ' ' * column_widths[columnindex]
|
||||||
|
else:
|
||||||
|
if column_types.get(columnindex, 'g') == 'g':
|
||||||
|
word = float(word)
|
||||||
|
else:
|
||||||
|
word = str(word)
|
||||||
|
line[columnindex] = column_formats[columnindex].format(word)
|
||||||
|
|
||||||
|
document_lines = [DELIMITER.join(line) for line in document_lines]
|
||||||
|
document_lines = '\n'.join(document_lines)
|
||||||
|
output_text += document_lines
|
||||||
|
print(output_text)
|
||||||
|
|
||||||
|
'''
|
||||||
|
number | name | age | address | misc. | blank column?
|
||||||
|
4 | John Smith | 26 | 123 north street | |
|
||||||
|
17 | Jenny Smith | 8 | 123 north street | |
|
||||||
|
55 | Veronica Dove | 20 | 456 west street | |
|
||||||
|
77 | Austin Texas | 33 | 789 south avenue | |
|
||||||
|
89 | Mister Super Long Name | 123 | planet earth | he's really old |
|
||||||
|
120 | Bill | | | Deceased |
|
||||||
|
999 | Nine Nine | 999 | 999 ninth boulevard | favorite number is 9 |
|
||||||
|
'''
|
4
MassStitching/README.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
Mass Stitch
|
||||||
|
===========
|
||||||
|
|
||||||
|
Given the name of a directory, stich together all the images in that directory into one large iamge.
|
BIN
MassStitching/example/DSC01328.jpg
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
MassStitching/example/dosmilla.jpg
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
MassStitching/example/p1010063.jpg
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
MassStitching/example/pizza.jpg
Normal file
After Width: | Height: | Size: 205 KiB |
58
MassStitching/massstitch.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
from PIL import Image
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def load_all_images(iterable):
|
||||||
|
images = []
|
||||||
|
for filename in iterable:
|
||||||
|
print('Loading "%s"' % filename)
|
||||||
|
try:
|
||||||
|
image = Image.open(filename)
|
||||||
|
image.filename = filename
|
||||||
|
print('Loaded "%s"' % filename)
|
||||||
|
images.append(image)
|
||||||
|
except OSError:
|
||||||
|
print('Could not load "%s"' % filename)
|
||||||
|
return images
|
||||||
|
|
||||||
|
def listfiles(directory):
|
||||||
|
files = [name for name in os.listdir(directory)]
|
||||||
|
files = [os.path.join(directory, name) for name in files]
|
||||||
|
files = [name for name in files if os.path.isfile(name)]
|
||||||
|
return files
|
||||||
|
|
||||||
|
def stitch(images, outputfilename):
|
||||||
|
largest_width = max(image.size[0] for image in images)
|
||||||
|
largest_height = max(image.size[1] for image in images)
|
||||||
|
print('Using cell size of %dx%dpx' % (largest_width, largest_height))
|
||||||
|
grid_width = round(len(images) ** 0.5)
|
||||||
|
# overflow adds an extra line for nonperfect squares.
|
||||||
|
overflow = 1 if (len(images) % grid_width != 0) else 0
|
||||||
|
grid_height = (len(images) // grid_width) + overflow
|
||||||
|
grid_width_pixels = grid_width * largest_width
|
||||||
|
grid_height_pixels = grid_height * largest_height
|
||||||
|
|
||||||
|
print('Creating image of size: %dx%d (%dx%dpx)' % (grid_width, grid_height, grid_width_pixels, grid_height_pixels))
|
||||||
|
stitched_image = Image.new('RGBA', [grid_width_pixels, grid_height_pixels])
|
||||||
|
print('Pasting components')
|
||||||
|
for (index, image) in enumerate(images):
|
||||||
|
pad_x = int((largest_width - image.size[0]) / 2)
|
||||||
|
pad_y = int((largest_height - image.size[1]) / 2)
|
||||||
|
gridspot_x = index % grid_width
|
||||||
|
gridspot_y = index // grid_width
|
||||||
|
pixel_x = (gridspot_x * largest_width) + pad_x
|
||||||
|
pixel_y = (gridspot_y * largest_height) + pad_y
|
||||||
|
print(index, image.filename, gridspot_x, gridspot_y, pixel_x, pixel_y)
|
||||||
|
stitched_image.paste(image, (pixel_x, pixel_y))
|
||||||
|
print('Saving "%s"' % outputfilename)
|
||||||
|
stitched_image.save(outputfilename)
|
||||||
|
|
||||||
|
|
||||||
|
directory = sys.argv[1]
|
||||||
|
images = listfiles(directory)
|
||||||
|
directory_id = 'massstitch_%s.png' % directory
|
||||||
|
if directory_id in images:
|
||||||
|
images.remove(directory_id)
|
||||||
|
images = load_all_images(images)
|
||||||
|
stitch(images, directory_id)
|
BIN
MassStitching/massstitch_example.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
6
Meal/README.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
Meal
|
||||||
|
=======
|
||||||
|
|
||||||
|
Just how much pizza do you eat, anyway?
|
||||||
|
|
||||||
|
Read HELP_TEXT inside the meal.py file.
|
330
Meal/meal.py
Normal file
|
@ -0,0 +1,330 @@
|
||||||
|
import datetime
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
UID_CHARACTERS = 16
|
||||||
|
RECENT_COUNT = 12
|
||||||
|
|
||||||
|
STRFTIME = '%Y %m %d %H:%M'
|
||||||
|
|
||||||
|
SQL_MEAL_ID = 0
|
||||||
|
SQL_MEAL_CREATED = 1
|
||||||
|
SQL_MEAL_HUMAN = 2
|
||||||
|
|
||||||
|
SQL_REL_FOOD = 1
|
||||||
|
|
||||||
|
SQL_GROUP_FOOD = 0
|
||||||
|
SQL_GROUP_GROUP = 1
|
||||||
|
|
||||||
|
DB_INIT = '''
|
||||||
|
CREATE TABLE IF NOT EXISTS meals(id TEXT, created INT, human TEXT);
|
||||||
|
CREATE TABLE IF NOT EXISTS meal_foods(mealid TEXT, food TEXT);
|
||||||
|
CREATE TABLE IF NOT EXISTS food_groups(food TEXT, foodgroup TEXT);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS index_meal_id on meals(id);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_meal_created on meals(created);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_food_mealid on meal_foods(mealid);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_food_food on meal_foods(food);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_group_food on food_groups(food);
|
||||||
|
'''.strip()
|
||||||
|
|
||||||
|
HELP_TEXT = '''
|
||||||
|
> meal add pizza, soda : Add a new meal with the foods "pizza" and "soda".
|
||||||
|
> meal adjust ec2 +10 : Adjust the timestamp of the meal starting with "ec2" by +10 seconds.
|
||||||
|
> meal adjust ec2 +10*60 : Adjusting timestamps supports math operations.
|
||||||
|
> meal group water drinks : Add "water" to foodgroup "drinks". Used for organization & reports.
|
||||||
|
> meal group water : Display the name of the group "water" belongs to.
|
||||||
|
> meal recent : Display info and foods for recent meals. Default {recent_count}.
|
||||||
|
> meal recent 4 : Display the last 4 meals.
|
||||||
|
> meal recent all : Display ALL meals.
|
||||||
|
> meal remove ec2 : Remove the meal whose ID starts with "ec2".
|
||||||
|
> meal show ec2 : Display info and foods for the meal whose ID starts with "ec2".
|
||||||
|
> meal ungroup water : Remove "water" from its foodgroup.
|
||||||
|
'''.format(recent_count=RECENT_COUNT)
|
||||||
|
|
||||||
|
def listget(li, index, fallback=None):
|
||||||
|
try:
|
||||||
|
return li[index]
|
||||||
|
except IndexError:
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
def uid(length=None):
|
||||||
|
'''
|
||||||
|
Generate a u-random hex string..
|
||||||
|
'''
|
||||||
|
if length is None:
|
||||||
|
length = UID_CHARACTERS
|
||||||
|
identifier = ''.join('{:02x}'.format(x) for x in os.urandom(math.ceil(length / 2)))
|
||||||
|
if len(identifier) > length:
|
||||||
|
identifier = identifier[:length]
|
||||||
|
return identifier
|
||||||
|
|
||||||
|
|
||||||
|
class MealDB():
|
||||||
|
def __init__(self, dbname='C:/Git/else/Meal/meal.db'):
|
||||||
|
self.dbname = dbname
|
||||||
|
self._sql = None
|
||||||
|
self._cur = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sql(self):
|
||||||
|
if self._sql is None:
|
||||||
|
self._sql = sqlite3.connect(self.dbname)
|
||||||
|
return self._sql
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cur(self):
|
||||||
|
if self._cur is None:
|
||||||
|
self._cur = self.sql.cursor()
|
||||||
|
statements = DB_INIT.split(';')
|
||||||
|
for statement in statements:
|
||||||
|
#print(statement)
|
||||||
|
self._cur.execute(statement)
|
||||||
|
return self._cur
|
||||||
|
|
||||||
|
def add_meal(self, foods=None):
|
||||||
|
if foods is None:
|
||||||
|
raise Exception('Empty meal!')
|
||||||
|
assert isinstance(foods, (list, tuple))
|
||||||
|
foods = set(foods)
|
||||||
|
|
||||||
|
if ''.join(foods).replace(' ', '') == '':
|
||||||
|
raise Exception('Empty meal!')
|
||||||
|
|
||||||
|
mealid = self.new_uid('meals')
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
now_stamp = int(now.timestamp())
|
||||||
|
now_string = now.strftime(STRFTIME)
|
||||||
|
|
||||||
|
self.normalized_query('INSERT INTO meals VALUES(?, ?, ?)', [mealid, now_stamp, now_string])
|
||||||
|
for food in foods:
|
||||||
|
self.normalized_query('INSERT INTO meal_foods VALUES(?, ?)', [mealid, food])
|
||||||
|
self.sql.commit()
|
||||||
|
foods = ', '.join(foods)
|
||||||
|
print('Added meal %s at %s with %s' % (mealid, now_string, foods))
|
||||||
|
return mealid
|
||||||
|
|
||||||
|
def adjust_timestamp(self, mealid, adjustment):
|
||||||
|
'''
|
||||||
|
Move a certain meal by `adjustment` seconds. This is useful when you need to
|
||||||
|
report a meal that happened a while ago, rather than the current timestamp.
|
||||||
|
'''
|
||||||
|
meal = self.get_meal_by_id(mealid)
|
||||||
|
mealid = meal[SQL_MEAL_ID]
|
||||||
|
meal_time = meal[SQL_MEAL_CREATED]
|
||||||
|
meal_time += adjustment
|
||||||
|
time_string = datetime.datetime.fromtimestamp(meal_time).strftime(STRFTIME)
|
||||||
|
self.normalized_query('UPDATE meals SET created=?, human=? WHERE id=?', [meal_time, time_string, mealid])
|
||||||
|
self.sql.commit()
|
||||||
|
print('Adjusted %s to %s' % (mealid, time_string))
|
||||||
|
|
||||||
|
def normalized_query(self, query, bindings):
|
||||||
|
nbindings = []
|
||||||
|
for binding in bindings:
|
||||||
|
if isinstance(binding, str):
|
||||||
|
nbindings.append(binding.lower())
|
||||||
|
continue
|
||||||
|
nbindings.append(binding)
|
||||||
|
self.cur.execute(query, nbindings)
|
||||||
|
|
||||||
|
def get_foods_for_meal(self, mealid):
|
||||||
|
meal = self.get_meal_by_id(mealid)
|
||||||
|
mealid = meal[SQL_MEAL_ID]
|
||||||
|
self.normalized_query('SELECT food FROM meal_foods WHERE mealid == ?', [mealid])
|
||||||
|
items = self.cur.fetchall()
|
||||||
|
items = [item[0] for item in items]
|
||||||
|
return items
|
||||||
|
|
||||||
|
def get_meal_by_id(self, mealid):
|
||||||
|
if len(mealid) == UID_CHARACTERS:
|
||||||
|
meal_q = mealid
|
||||||
|
self.normalized_query('SELECT * FROM meals WHERE id == ?', [meal_q])
|
||||||
|
else:
|
||||||
|
meal_q = mealid + '%'
|
||||||
|
self.normalized_query('SELECT * FROM meals WHERE id LIKE ?', [meal_q])
|
||||||
|
|
||||||
|
items = self.cur.fetchall()
|
||||||
|
if len(items) > 1:
|
||||||
|
items = [str(item) for item in items]
|
||||||
|
items = '\n'.join(items)
|
||||||
|
raise Exception('Found multiple meals for id "%s"\n%s' % (meal_q, items))
|
||||||
|
if len(items) == 0:
|
||||||
|
raise Exception('Found no meal for id "%s"' % (meal_q))
|
||||||
|
|
||||||
|
meal = items[0]
|
||||||
|
return meal
|
||||||
|
|
||||||
|
def group(self, food, groupname):
|
||||||
|
'''
|
||||||
|
Insert `food` into the foodgroup `groupname`. This is used for organization,
|
||||||
|
normalization, and creating dietary reports.
|
||||||
|
'''
|
||||||
|
self.normalized_query('SELECT * FROM food_groups WHERE food == ?', [food])
|
||||||
|
belongs = self.cur.fetchone()
|
||||||
|
|
||||||
|
if groupname is None:
|
||||||
|
self.normalized_query('SELECT * FROM food_groups where foodgroup == ?', [food])
|
||||||
|
contains = self.cur.fetchall()
|
||||||
|
|
||||||
|
if belongs is not None:
|
||||||
|
print('"%s" belongs to group "%s".' % (food, belongs[1]))
|
||||||
|
else:
|
||||||
|
print('"%s" is not in any group.' % (food))
|
||||||
|
|
||||||
|
if contains is not None and len(contains) > 0:
|
||||||
|
contains = [x[0] for x in contains]
|
||||||
|
contains = [repr(x) for x in contains]
|
||||||
|
contains = ', '.join(contains)
|
||||||
|
print('The "%s" group contains: %s' % (food, contains))
|
||||||
|
return
|
||||||
|
|
||||||
|
if belongs is not None:
|
||||||
|
raise Exception('"%s" is already in group "%s"' % (f[0], f[1]))
|
||||||
|
self.normalized_query('INSERT INTO food_groups VALUES(?, ?)', [food, groupname])
|
||||||
|
self.sql.commit()
|
||||||
|
print('Added "%s" to group "%s"' % (food, groupname))
|
||||||
|
|
||||||
|
def new_uid(self, table):
|
||||||
|
'''
|
||||||
|
Create a new UID that is unique to the given table.
|
||||||
|
'''
|
||||||
|
result = None
|
||||||
|
query = 'SELECT * FROM {table} WHERE id == ?'.format(table=table)
|
||||||
|
while result is None:
|
||||||
|
i = uid()
|
||||||
|
# Just gotta be sure, man.
|
||||||
|
self.normalized_query(query, [i])
|
||||||
|
if self.cur.fetchone() is None:
|
||||||
|
result = i
|
||||||
|
return result
|
||||||
|
|
||||||
|
def remove_meal(self, mealid):
|
||||||
|
meal = self.get_meal_by_id(mealid)
|
||||||
|
mealid = meal[SQL_MEAL_ID]
|
||||||
|
self.normalized_query('DELETE FROM meals WHERE id == ?', [mealid])
|
||||||
|
self.normalized_query('DELETE FROM meal_foods WHERE mealid == ?', [mealid])
|
||||||
|
self.sql.commit()
|
||||||
|
print('Removed meal %s' % (mealid))
|
||||||
|
|
||||||
|
def show_meal(self, mealid):
|
||||||
|
'''
|
||||||
|
Display:
|
||||||
|
id
|
||||||
|
timestamp
|
||||||
|
foods
|
||||||
|
for the meal with the given ID.
|
||||||
|
'''
|
||||||
|
meal = self.get_meal_by_id(mealid)
|
||||||
|
foods = self.get_foods_for_meal(mealid)
|
||||||
|
print(meal[SQL_MEAL_ID])
|
||||||
|
print(meal[SQL_MEAL_HUMAN])
|
||||||
|
foods = ', '.join(foods)
|
||||||
|
print(foods)
|
||||||
|
|
||||||
|
def show_recent(self, count=RECENT_COUNT):
|
||||||
|
'''
|
||||||
|
Display:
|
||||||
|
id : timestamp : foods
|
||||||
|
for the `count` most recent meals. If count is "all" or "*", show ALL meals.
|
||||||
|
'''
|
||||||
|
if count in ('all', '*'):
|
||||||
|
self.normalized_query('SELECT * FROM meals ORDER BY created DESC', [])
|
||||||
|
else:
|
||||||
|
self.normalized_query('SELECT * FROM meals ORDER BY created DESC LIMIT ?', [count])
|
||||||
|
meals = self.cur.fetchall()
|
||||||
|
output = []
|
||||||
|
for meal in meals:
|
||||||
|
mealid = meal[SQL_MEAL_ID]
|
||||||
|
human = meal[SQL_MEAL_HUMAN]
|
||||||
|
foods = self.get_foods_for_meal(mealid)
|
||||||
|
foods = ', '.join(foods)
|
||||||
|
output.append('%s : %s : %s' % (mealid, human, foods))
|
||||||
|
output = '\n'.join(output)
|
||||||
|
print(output)
|
||||||
|
|
||||||
|
def ungroup(self, food):
|
||||||
|
'''
|
||||||
|
Remove `food` from whatever group it is in.
|
||||||
|
'''
|
||||||
|
self.normalized_query('SELECT * FROM food_groups WHERE food == ?', [food])
|
||||||
|
f = self.cur.fetchone()
|
||||||
|
if f is None:
|
||||||
|
raise Exception('"%s" is not part of a group' % (food))
|
||||||
|
groupname = f[1]
|
||||||
|
self.normalized_query('DELETE FROM food_groups WHERE food == ?', [food])
|
||||||
|
self.sql.commit()
|
||||||
|
print('Removed "%s" from group "%s"' % (food, groupname))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
mealdb = MealDB()
|
||||||
|
|
||||||
|
args = sys.argv[1:]
|
||||||
|
if len(args) == 0:
|
||||||
|
command = ''
|
||||||
|
else:
|
||||||
|
command = args[0].lower()
|
||||||
|
|
||||||
|
if command == 'add':
|
||||||
|
args = args[1:]
|
||||||
|
|
||||||
|
elif command == 'adjust':
|
||||||
|
mealid = args[1]
|
||||||
|
adjustment = args[2]
|
||||||
|
adjustment = eval(adjustment)
|
||||||
|
mealdb.adjust_timestamp(mealid, adjustment)
|
||||||
|
quit()
|
||||||
|
|
||||||
|
elif command == 'group':
|
||||||
|
food = args[1]
|
||||||
|
groupname = listget(args, 2, None)
|
||||||
|
mealdb.group(food, groupname)
|
||||||
|
quit()
|
||||||
|
|
||||||
|
elif command == 'recent':
|
||||||
|
count = listget(args, 1, RECENT_COUNT)
|
||||||
|
mealdb.show_recent(count)
|
||||||
|
quit()
|
||||||
|
|
||||||
|
elif command == 'remove':
|
||||||
|
mealids = args[1]
|
||||||
|
mealids = mealids.replace(' ', '')
|
||||||
|
mealids = mealids.split(',')
|
||||||
|
for mealid in mealids:
|
||||||
|
mealdb.remove_meal(mealid)
|
||||||
|
quit()
|
||||||
|
|
||||||
|
elif command == 'show':
|
||||||
|
mealid = args[1]
|
||||||
|
mealdb.show_meal(mealid)
|
||||||
|
quit()
|
||||||
|
|
||||||
|
elif command == 'ungroup':
|
||||||
|
food = args[1]
|
||||||
|
mealdb.ungroup(food)
|
||||||
|
quit()
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(HELP_TEXT)
|
||||||
|
quit()
|
||||||
|
|
||||||
|
args = ' '.join(args)
|
||||||
|
|
||||||
|
if ';' in args:
|
||||||
|
(args, adjustment) = args.split(';')
|
||||||
|
adjustment = adjustment.strip()
|
||||||
|
adjustment = eval(adjustment)
|
||||||
|
else:
|
||||||
|
adjustment = 0
|
||||||
|
|
||||||
|
args = args.strip()
|
||||||
|
args = args.split(',')
|
||||||
|
args = [food.strip() for food in args]
|
||||||
|
meal = mealdb.add_meal(args)
|
||||||
|
if adjustment != 0:
|
||||||
|
mealdb.adjust_timestamp(meal, adjustment)
|
||||||
|
quit()
|
BIN
MeshGenerator/mesh_1366x768_1_2200.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
MeshGenerator/mesh_1366x768_3_2200.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
MeshGenerator/mesh_1920x1080_1_2200.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
MeshGenerator/mesh_1920x1080_2_2120.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
MeshGenerator/mesh_1920x1080_2_2200.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
MeshGenerator/mesh_1920x1080_3_2200.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
MeshGenerator/mesh_1920x1080_40_2110.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
MeshGenerator/mesh_1920x1080_40_2120.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
MeshGenerator/mesh_1920x1080_40_2200.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
MeshGenerator/mesh_1920x1080_4_2110.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
MeshGenerator/mesh_1920x1080_4_2200.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
MeshGenerator/mesh_1920x1080_6_2110.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
MeshGenerator/mesh_2732x1536_2_2200.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
MeshGenerator/mesh_400x400_4_2200.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
MeshGenerator/mesh_50x50_1_2200.png
Normal file
After Width: | Height: | Size: 186 B |
89
MeshGenerator/meshgenerator.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
from PIL import Image
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def chunk_iterable(iterable, chunk_length, allow_incomplete=True):
|
||||||
|
'''
|
||||||
|
Given an iterable, divide it into chunks of length `chunk_length`.
|
||||||
|
If `allow_incomplete` is True, the final element of the returned list may be shorter
|
||||||
|
than `chunk_length`. If it is False, those items are discarded.
|
||||||
|
'''
|
||||||
|
if len(iterable) % chunk_length != 0 and allow_incomplete:
|
||||||
|
overflow = 1
|
||||||
|
else:
|
||||||
|
overflow = 0
|
||||||
|
|
||||||
|
steps = (len(iterable) // chunk_length) + overflow
|
||||||
|
return [iterable[chunk_length * x : (chunk_length * x) + chunk_length] for x in range(steps)]
|
||||||
|
|
||||||
|
def hex_to_rgb(x):
|
||||||
|
x = x.replace('#', '')
|
||||||
|
x = chunk_iterable(x, 2)
|
||||||
|
print(x)
|
||||||
|
x = tuple(int(i, 16) for i in x)
|
||||||
|
return x
|
||||||
|
|
||||||
|
def mesh_generator(image_width, image_height, square_size, mode):
|
||||||
|
square = square_size * 2
|
||||||
|
print(mode)
|
||||||
|
x_space = square_size * mode[0]
|
||||||
|
y_space = square_size * mode[1]
|
||||||
|
odd_x = False
|
||||||
|
odd_y = False
|
||||||
|
for y in range(0, image_height, y_space):
|
||||||
|
odd_y = not odd_y
|
||||||
|
for x in range(0, image_width, x_space):
|
||||||
|
odd_x = not odd_x
|
||||||
|
boost_x = int(odd_y) * square_size * mode[2]
|
||||||
|
boost_y = int(odd_x) * square_size * mode[3]
|
||||||
|
#print(boost_x, boost_y)
|
||||||
|
#x += boost_x
|
||||||
|
#y += boost_y
|
||||||
|
yield (x + boost_x, y + boost_y)
|
||||||
|
|
||||||
|
def make_image(image_width, image_height, square_size, mode, bgcolor='#00000000', fgcolor='#000000'):
|
||||||
|
pattern_repeat_width = (2 * square_size * mode[0])
|
||||||
|
pattern_repeat_height = (2 * square_size * mode[1])
|
||||||
|
print(pattern_repeat_width, pattern_repeat_height)
|
||||||
|
|
||||||
|
bgcolor = hex_to_rgb(bgcolor)
|
||||||
|
fgcolor = hex_to_rgb(fgcolor)
|
||||||
|
|
||||||
|
pattern = Image.new('RGBA', (pattern_repeat_width, pattern_repeat_height), bgcolor)
|
||||||
|
#image = Image.new('RGBA', (image_width, image_height))
|
||||||
|
blackbox = Image.new('RGBA', (square_size, square_size), fgcolor)
|
||||||
|
for pair in mesh_generator(pattern_repeat_width, pattern_repeat_height, square_size, mode):
|
||||||
|
pattern.paste(blackbox, pair)
|
||||||
|
while pattern.size[0] < image_width or pattern.size[1] < image_height:
|
||||||
|
p = pattern
|
||||||
|
(w, h) = p.size
|
||||||
|
print('expanding from', w, h)
|
||||||
|
pattern = Image.new('RGBA', (w * 2, h * 2))
|
||||||
|
for y in range(2):
|
||||||
|
for x in range(2):
|
||||||
|
pattern.paste(p, (w * x, h * y))
|
||||||
|
image = pattern.crop((0, 0, image_width, image_height))
|
||||||
|
|
||||||
|
mode = [str(x) for x in mode]
|
||||||
|
mode = ''.join(mode)
|
||||||
|
filename = 'mesh_%dx%d_%d_%s.png' % (image_width, image_height, square_size, mode)
|
||||||
|
image.save(filename)
|
||||||
|
print('Saved %s' % filename)
|
||||||
|
|
||||||
|
def listget(li, index, fallback=None):
|
||||||
|
try:
|
||||||
|
return li[index]
|
||||||
|
except IndexError:
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
image_width = int(sys.argv[1])
|
||||||
|
image_height = int(sys.argv[2])
|
||||||
|
square_size = int(listget(sys.argv, 3, 1))
|
||||||
|
x_spacing = int(listget(sys.argv, 4, 2))
|
||||||
|
y_spacing = int(listget(sys.argv, 5, 2))
|
||||||
|
x_alternator = int(listget(sys.argv, 6, 0))
|
||||||
|
y_alternator = int(listget(sys.argv, 7, 0))
|
||||||
|
bgcolor = listget(sys.argv, 8, '#00000000')
|
||||||
|
fgcolor = listget(sys.argv, 9, '#000000')
|
||||||
|
mode = (x_spacing, y_spacing, x_alternator, y_alternator)
|
||||||
|
make_image(image_width, image_height, square_size, mode, bgcolor, fgcolor)
|
332
OpenDirDL/opendirdl.py
Normal file
|
@ -0,0 +1,332 @@
|
||||||
|
import bs4
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
FILENAME_BADCHARS = '/\\:*?"<>|'
|
||||||
|
DOWNLOAD_CHUNK = 2048
|
||||||
|
|
||||||
|
# When doing a basic scan, we will not send HEAD requests to URLs that end in these strings,
|
||||||
|
# because they're probably files.
|
||||||
|
# This isn't meant to be a comprehensive filetype library, but it covers enough of the
|
||||||
|
# typical opendir to speed things up.
|
||||||
|
SKIPPABLE_FILETYPES = [
|
||||||
|
'.avi',
|
||||||
|
'.bmp',
|
||||||
|
'.epub',
|
||||||
|
'.db',
|
||||||
|
'.flac',
|
||||||
|
'.ico',
|
||||||
|
'.iso',
|
||||||
|
'.jpg',
|
||||||
|
'.m4a',
|
||||||
|
'.mkv',
|
||||||
|
'.mov',
|
||||||
|
'.mp3',
|
||||||
|
'.mp4',
|
||||||
|
'.pdf',
|
||||||
|
'.png',
|
||||||
|
'.srt',
|
||||||
|
'.txt',
|
||||||
|
'.webm',
|
||||||
|
'.zip',
|
||||||
|
]
|
||||||
|
|
||||||
|
SKIPPABLE_FILETYPES = [x.lower() for x in SKIPPABLE_FILETYPES]
|
||||||
|
|
||||||
|
class Downloader:
|
||||||
|
def __init__(self, urlfile, outputdir=None, headers=None):
|
||||||
|
jdict = file_to_dict(urlfile)
|
||||||
|
self.urls = [item[0] for item in jdict.items()]
|
||||||
|
self.urls.sort(key=str.lower)
|
||||||
|
self.outputdir = outputdir
|
||||||
|
|
||||||
|
if self.outputdir is None or self.outputdir == "":
|
||||||
|
# returns (root, path, filename). Keep root.
|
||||||
|
self.outputdir = url_to_filepath(self.urls[0])[0]
|
||||||
|
|
||||||
|
def download(self, overwrite=False):
|
||||||
|
overwrite = bool(overwrite)
|
||||||
|
for url in self.urls:
|
||||||
|
''' Creating the Path '''
|
||||||
|
(root, folder, filename) = url_to_filepath(url)
|
||||||
|
# In case the user has set a custom download directory,
|
||||||
|
# ignore the above value of `root`.
|
||||||
|
root = self.outputdir
|
||||||
|
folder = os.path.join(root, folder)
|
||||||
|
if not os.path.exists(folder):
|
||||||
|
os.makedirs(folder)
|
||||||
|
localname = os.path.join(folder, filename)
|
||||||
|
temporary_basename = hashit(url, 16) + '.oddltemporary'
|
||||||
|
temporary_localname = os.path.join(folder, temporary_basename)
|
||||||
|
|
||||||
|
''' Managing overwrite '''
|
||||||
|
if os.path.isfile(localname):
|
||||||
|
if overwrite is True:
|
||||||
|
os.remove(localname)
|
||||||
|
else:
|
||||||
|
safeprint('Skipping "%s". Use `overwrite=True`' % localname)
|
||||||
|
continue
|
||||||
|
|
||||||
|
safeprint('Downloading "%s" as "%s"' % (localname, temporary_basename))
|
||||||
|
filehandle = open(temporary_localname, 'wb')
|
||||||
|
try:
|
||||||
|
download_file(url, filehandle, hookfunction=hook1)
|
||||||
|
os.rename(temporary_localname, localname)
|
||||||
|
except:
|
||||||
|
filehandle.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
class Walker:
|
||||||
|
def __init__(self, website, outputfile, fullscan=False):
|
||||||
|
self.website = website
|
||||||
|
self.fullscan = bool(fullscan)
|
||||||
|
if os.path.exists(outputfile):
|
||||||
|
self.results = file_to_dict(outputfile)
|
||||||
|
else:
|
||||||
|
self.results = {}
|
||||||
|
self.already_seen = set()
|
||||||
|
|
||||||
|
def add_head_to_results(self, head):
|
||||||
|
if isinstance(head, str):
|
||||||
|
# For when we're doing a basic scan, which skips urls that
|
||||||
|
# look like a file.
|
||||||
|
self.results[head] = {
|
||||||
|
'Content-Length': -1,
|
||||||
|
'Content-Type': '?',
|
||||||
|
}
|
||||||
|
self.already_seen.add(head)
|
||||||
|
else:
|
||||||
|
# For when we're doing a full scan, which does a HEAD request
|
||||||
|
# for all urls.
|
||||||
|
self.results[head.url] = {
|
||||||
|
'Content-Length': int(head.headers.get('Content-Length', -1)),
|
||||||
|
'Content-Type': head.headers.get('Content-Type', '?'),
|
||||||
|
}
|
||||||
|
self.already_seen.add(head.url)
|
||||||
|
|
||||||
|
def extract_hrefs(self, response):
|
||||||
|
soup = bs4.BeautifulSoup(response.text)
|
||||||
|
elements = soup.findAll('a')
|
||||||
|
hrefs = []
|
||||||
|
for element in elements:
|
||||||
|
try:
|
||||||
|
href = element['href']
|
||||||
|
except KeyError:
|
||||||
|
continue
|
||||||
|
href = urllib.parse.urljoin(response.url, href)
|
||||||
|
if not href.startswith(self.website):
|
||||||
|
# Don't go to other sites or parent directories
|
||||||
|
continue
|
||||||
|
if 'C=' in href and 'O=' in href:
|
||||||
|
# Alternative sort modes for index pages
|
||||||
|
continue
|
||||||
|
if href.endswith('desktop.ini'):
|
||||||
|
# I hate these things
|
||||||
|
continue
|
||||||
|
hrefs.append(href)
|
||||||
|
return hrefs
|
||||||
|
|
||||||
|
def walk(self, url=None):
|
||||||
|
if url is None:
|
||||||
|
url = self.website
|
||||||
|
else:
|
||||||
|
url = urllib.parse.urljoin(self.website, url)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
urll = url.lower()
|
||||||
|
if self.fullscan is False and any(urll.endswith(ext) for ext in SKIPPABLE_FILETYPES):
|
||||||
|
print('Skipping "%s" due to extension' % url)
|
||||||
|
self.add_head_to_results(url)
|
||||||
|
return results
|
||||||
|
|
||||||
|
if not url.startswith(self.website):
|
||||||
|
# Don't follow external links or parent directory.
|
||||||
|
return results
|
||||||
|
|
||||||
|
head = requests.head(url)
|
||||||
|
head.raise_for_status()
|
||||||
|
|
||||||
|
safeprint('HEAD: %s : %s' % (url, head))
|
||||||
|
content_type = head.headers.get('Content-Type', '?')
|
||||||
|
self.already_seen.add(head.url)
|
||||||
|
|
||||||
|
if content_type.startswith('text/html') and head.url.endswith('/'):
|
||||||
|
# This is an index page, let's get recursive.
|
||||||
|
page = requests.get(url)
|
||||||
|
safeprint(' GET: %s : %s' % (url, page))
|
||||||
|
hrefs = self.extract_hrefs(page)
|
||||||
|
for url in hrefs:
|
||||||
|
if url not in self.results and url not in self.already_seen:
|
||||||
|
results += self.walk(url)
|
||||||
|
else:
|
||||||
|
# Don't add index pages to the results.
|
||||||
|
self.add_head_to_results(head)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def dict_to_file(jdict, filename):
|
||||||
|
filehandle = open(filename, 'wb')
|
||||||
|
text = json.dumps(jdict, indent=4, sort_keys=True)
|
||||||
|
text = text.encode('utf-8')
|
||||||
|
filehandle.write(text)
|
||||||
|
filehandle.close()
|
||||||
|
|
||||||
|
def download_file(url, filehandle, getsizeheaders=True, hookfunction=None, headers={}, auth=None):
|
||||||
|
if getsizeheaders:
|
||||||
|
totalsize = requests.head(url, headers=headers, auth=auth)
|
||||||
|
totalsize = int(totalsize.headers['content-length'])
|
||||||
|
else:
|
||||||
|
totalsize = 1
|
||||||
|
currentblock = 0
|
||||||
|
downloading = requests.get(url, stream=True, headers=headers, auth=auth)
|
||||||
|
for chunk in downloading.iter_content(chunk_size=DOWNLOAD_CHUNK):
|
||||||
|
if chunk:
|
||||||
|
currentblock += 1
|
||||||
|
filehandle.write(chunk)
|
||||||
|
if hookfunction is not None:
|
||||||
|
hookfunction(currentblock, DOWNLOAD_CHUNK, totalsize)
|
||||||
|
filehandle.close()
|
||||||
|
size = os.path.getsize(filehandle.name)
|
||||||
|
if size < totalsize:
|
||||||
|
raise Exception('Did not receive expected total size. %d / %d' % (size, totalsize))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def file_to_dict(filename):
|
||||||
|
filehandle = open(filename, 'rb')
|
||||||
|
jdict = json.loads(filehandle.read().decode('utf-8'))
|
||||||
|
filehandle.close()
|
||||||
|
return jdict
|
||||||
|
|
||||||
|
def filepath_sanitize(text, exclusions=''):
|
||||||
|
bet = FILENAME_BADCHARS.replace(exclusions, '')
|
||||||
|
for char in bet:
|
||||||
|
text = text.replace(char, '')
|
||||||
|
return text
|
||||||
|
|
||||||
|
def hashit(text, length=None):
|
||||||
|
h = hashlib.sha512(text.encode('utf-8')).hexdigest()
|
||||||
|
if length is not None:
|
||||||
|
h = h[:length]
|
||||||
|
return h
|
||||||
|
|
||||||
|
def hook1(currentblock, chunksize, totalsize):
|
||||||
|
currentbytes = currentblock * chunksize
|
||||||
|
if currentbytes > totalsize:
|
||||||
|
currentbytes = totalsize
|
||||||
|
currentbytes = '{:,}'.format(currentbytes)
|
||||||
|
totalsize = '{:,}'.format(totalsize)
|
||||||
|
currentbytes = currentbytes.rjust(len(totalsize), ' ')
|
||||||
|
print('%s / %s bytes' % (currentbytes, totalsize), end='\r')
|
||||||
|
if currentbytes == totalsize:
|
||||||
|
print()
|
||||||
|
|
||||||
|
def safeprint(text, **kwargs):
|
||||||
|
text = str(text)
|
||||||
|
text = text.encode('ascii', 'replace').decode()
|
||||||
|
text = text.replace('?', '_')
|
||||||
|
print(text, **kwargs)
|
||||||
|
|
||||||
|
def url_to_filepath(text):
|
||||||
|
text = urllib.parse.unquote(text)
|
||||||
|
parts = urllib.parse.urlsplit(text)
|
||||||
|
root = parts.netloc
|
||||||
|
(folder, filename) = os.path.split(parts.path)
|
||||||
|
while folder.startswith('/'):
|
||||||
|
folder = folder[1:]
|
||||||
|
|
||||||
|
# Folders are allowed to have slashes
|
||||||
|
folder = filepath_sanitize(folder, exclusions='/\\')
|
||||||
|
folder = folder.replace('\\', os.path.sep)
|
||||||
|
folder = folder.replace('/', os.path.sep)
|
||||||
|
# But Files are not.
|
||||||
|
filename = filepath_sanitize(filename)
|
||||||
|
return (root, folder, filename)
|
||||||
|
|
||||||
|
## Commandline functions ####################################################\\
|
||||||
|
def digest(website, outputfile, fullscan, *trash):
|
||||||
|
fullscan = bool(fullscan)
|
||||||
|
if website[-1] != '/':
|
||||||
|
website += '/'
|
||||||
|
walker = Walker(website, outputfile, fullscan=fullscan)
|
||||||
|
try:
|
||||||
|
walker.walk()
|
||||||
|
dict_to_file(walker.results, outputfile)
|
||||||
|
except:
|
||||||
|
dict_to_file(walker.results, outputfile)
|
||||||
|
traceback.print_exc()
|
||||||
|
print('SAVED PROGRESS SO FAR')
|
||||||
|
|
||||||
|
def download(urlfile, outputdir, overwrite, *trash):
|
||||||
|
downloader = Downloader(urlfile, outputdir)
|
||||||
|
downloader.download(overwrite)
|
||||||
|
|
||||||
|
def filter_pattern(urlfile, patterns, negative=False, *trash):
|
||||||
|
'''
|
||||||
|
When `negative` is True, items are kept when they do NOT match the pattern,
|
||||||
|
allowing you to delete trash files.
|
||||||
|
|
||||||
|
When `negative` is False, items are keep when they DO match the pattern,
|
||||||
|
allowing you to keep items of interest.
|
||||||
|
'''
|
||||||
|
if isinstance(patterns, str):
|
||||||
|
patterns = [patterns]
|
||||||
|
jdict = file_to_dict(urlfile)
|
||||||
|
keys = list(jdict.keys())
|
||||||
|
for key in keys:
|
||||||
|
for pattern in patterns:
|
||||||
|
contains = re.search(pattern, key) is not None
|
||||||
|
if contains ^ negative:
|
||||||
|
safeprint('Removing "%s"' % key)
|
||||||
|
del jdict[key]
|
||||||
|
dict_to_file(jdict, urlfile)
|
||||||
|
|
||||||
|
def keep_pattern(urlfile, patterns, *trash):
|
||||||
|
filter_pattern(urlfile=urlfile, patterns=patterns, negative=True)
|
||||||
|
|
||||||
|
def measure(urlfile, *trash):
|
||||||
|
jdict = file_to_dict(urlfile)
|
||||||
|
totalbytes = 0
|
||||||
|
for (url, info) in jdict.items():
|
||||||
|
bytes = info['Content-Length']
|
||||||
|
if bytes > 0:
|
||||||
|
totalbytes += bytes
|
||||||
|
bytestring = '{:,}'.format(totalbytes)
|
||||||
|
print(bytestring)
|
||||||
|
return totalbytes
|
||||||
|
|
||||||
|
def remove_pattern(urlfile, patterns, *trash):
|
||||||
|
filter_pattern(urlfile=urlfile, patterns=patterns, negative=False)
|
||||||
|
|
||||||
|
def listget(l, index, default=None):
|
||||||
|
try:
|
||||||
|
return l[index]
|
||||||
|
except IndexError:
|
||||||
|
return default
|
||||||
|
cmdfunctions = [digest, download, keep_pattern, measure, remove_pattern]
|
||||||
|
## End of commandline functions #############################################//
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
command = listget(sys.argv, 1, None)
|
||||||
|
arg1 = listget(sys.argv, 2, None)
|
||||||
|
arg2 = listget(sys.argv, 3, None)
|
||||||
|
arg3 = listget(sys.argv, 4, None)
|
||||||
|
if command is None:
|
||||||
|
quit()
|
||||||
|
did_something = False
|
||||||
|
for function in cmdfunctions:
|
||||||
|
if command == function.__name__:
|
||||||
|
function(arg1, arg2, arg3)
|
||||||
|
did_something = True
|
||||||
|
break
|
||||||
|
if not did_something:
|
||||||
|
print('No matching function')
|
|
@ -5,35 +5,72 @@ import sys
|
||||||
DEFAULT_LENGTH = 32
|
DEFAULT_LENGTH = 32
|
||||||
DEFAULT_SENTENCE = 5
|
DEFAULT_SENTENCE = 5
|
||||||
HELP_MESSAGE = '''
|
HELP_MESSAGE = '''
|
||||||
---------------------------------------------------------------
|
===============================================================================
|
||||||
|Generates a randomized password. |
|
Generates a randomized password.
|
||||||
| |
|
|
||||||
|> passwordy [length] ["p"] ["d"] |
|
|
||||||
| |
|
|
||||||
| length : How many characters. Default %03d. |
|
|
||||||
| p : If present, the password will contain punctuation |
|
|
||||||
| characters. Otherwise not. |
|
|
||||||
| d : If present, the password will contain digits. |
|
|
||||||
| Otherwise not. |
|
|
||||||
| |
|
|
||||||
| The password can always contain upper and lowercase |
|
|
||||||
| letters. |
|
|
||||||
---------------------------------------------------------------
|
|
||||||
'''[1:-1] % (DEFAULT_LENGTH)
|
|
||||||
|
|
||||||
HELP_SENTENCE = '''
|
> passwordy [length] [options]
|
||||||
---------------------------------------------------------------
|
|
||||||
|Generates a randomized sentence |
|
|
||||||
| |
|
|
||||||
|> passwordy sent [length] [join] |
|
|
||||||
| |
|
|
||||||
| length : How many words to retrieve. Default %03d. |
|
|
||||||
| join : The character that will join the words together. |
|
|
||||||
| Default space. |
|
|
||||||
---------------------------------------------------------------
|
|
||||||
'''[1:-1] % (DEFAULT_SENTENCE)
|
|
||||||
|
|
||||||
def make_password(length=None, allowpunctuation=False, allowdigits=False, digits_only=False, binary=False):
|
length: How many characters. Default %03d.
|
||||||
|
options:
|
||||||
|
h : consist entirely of hexadecimal characters.
|
||||||
|
b : consist entirely of binary characters.
|
||||||
|
dd : consist entirely of decimal characters.
|
||||||
|
default : consist entirely of upper+lower letters.
|
||||||
|
|
||||||
|
p : allow punctuation in conjunction with above.
|
||||||
|
d : allow digits in conjunction with above.
|
||||||
|
|
||||||
|
l : convert to lowercase.
|
||||||
|
u : convert to uppercase.
|
||||||
|
nd : no duplicates. Each character can only appear once.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
> passwordy 32 h l
|
||||||
|
98f17b6016cf08cc00f2aeecc8d8afeb
|
||||||
|
|
||||||
|
> passwordy 32 h u
|
||||||
|
2AA706866BF7A5C18328BF866136A261
|
||||||
|
|
||||||
|
> passwordy 32 u
|
||||||
|
JHEPTKCEFZRFXILMASHNPSTFFNWQHTTN
|
||||||
|
|
||||||
|
> passwordy 32 p
|
||||||
|
Q+:iSKX!Nt)ewUvlE*!+^D}hp+|<wpJ}
|
||||||
|
|
||||||
|
> passwordy 32 l p
|
||||||
|
m*'otz/"!qo?-^wwdu@fasf:|ldkosi`
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
|
||||||
|
Generates a randomized sentence of words.
|
||||||
|
|
||||||
|
> passwordy sent [length] [join]
|
||||||
|
|
||||||
|
length : How many words. Default %03d.
|
||||||
|
join : The character that will join words together.
|
||||||
|
Default space.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
> passwordy sent
|
||||||
|
arrowroot sheared rustproof undo propionic acid
|
||||||
|
|
||||||
|
> passwordy sent 8
|
||||||
|
cipher competition solid angle rigmarole lachrymal social class critter consequently
|
||||||
|
|
||||||
|
> passwordy sent 8 _
|
||||||
|
Kahn_secondary_emission_unskilled_superior_court_straight_ticket_voltameter_hopper_crass
|
||||||
|
|
||||||
|
===============================================================================
|
||||||
|
'''.strip() % (DEFAULT_LENGTH, DEFAULT_SENTENCE)
|
||||||
|
|
||||||
|
|
||||||
|
def listget(li, index, fallback=None):
|
||||||
|
try:
|
||||||
|
return li[index]
|
||||||
|
except IndexError:
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
def make_password(length=None, passtype='standard'):
|
||||||
'''
|
'''
|
||||||
Returns a string of length `length` consisting of a random selection
|
Returns a string of length `length` consisting of a random selection
|
||||||
of uppercase and lowercase letters, as well as punctuation and digits
|
of uppercase and lowercase letters, as well as punctuation and digits
|
||||||
|
@ -42,18 +79,39 @@ def make_password(length=None, allowpunctuation=False, allowdigits=False, digits
|
||||||
if length is None:
|
if length is None:
|
||||||
length = DEFAULT_LENGTH
|
length = DEFAULT_LENGTH
|
||||||
|
|
||||||
if digits_only is False and binary is False:
|
alphabet = ''
|
||||||
s = string.ascii_letters
|
|
||||||
if allowpunctuation is True:
|
|
||||||
s += string.punctuation
|
|
||||||
if allowdigits is True:
|
|
||||||
s += string.digits
|
|
||||||
elif digits_only:
|
|
||||||
s = string.digits
|
|
||||||
elif binary:
|
|
||||||
s = '01'
|
|
||||||
|
|
||||||
password = ''.join([random.choice(s) for x in range(length)])
|
if 'standard' in passtype:
|
||||||
|
alphabet = string.ascii_letters
|
||||||
|
elif 'digit_only' in passtype:
|
||||||
|
alphabet = string.digits
|
||||||
|
elif 'hex' in passtype:
|
||||||
|
alphabet = '0123456789abcdef'
|
||||||
|
elif 'binary' in passtype:
|
||||||
|
alphabet = '01'
|
||||||
|
|
||||||
|
if '+digits' in passtype:
|
||||||
|
alphabet += string.digits
|
||||||
|
if '+punctuation' in passtype:
|
||||||
|
alphabet += string.punctuation
|
||||||
|
if '+lowercase' in passtype:
|
||||||
|
alphabet = alphabet.lower()
|
||||||
|
elif '+uppercase' in passtype:
|
||||||
|
alphabet = alphabet.upper()
|
||||||
|
|
||||||
|
alphabet = list(set(alphabet))
|
||||||
|
|
||||||
|
if '+noduplicates' in passtype:
|
||||||
|
if len(alphabet) < length:
|
||||||
|
message = 'Alphabet "%s" is not long enough to support no-dupe password of length %d'
|
||||||
|
message = message % (alphabet, length)
|
||||||
|
raise Exception(message)
|
||||||
|
password = ''
|
||||||
|
for x in range(length):
|
||||||
|
random.shuffle(alphabet)
|
||||||
|
password += alphabet.pop(0)
|
||||||
|
else:
|
||||||
|
password = ''.join([random.choice(alphabet) for x in range(length)])
|
||||||
return password
|
return password
|
||||||
|
|
||||||
def make_sentence(length=None, joiner=' '):
|
def make_sentence(length=None, joiner=' '):
|
||||||
|
@ -70,51 +128,58 @@ def make_sentence(length=None, joiner=' '):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
args = sys.argv
|
args = sys.argv[1:]
|
||||||
argc = len(args) - 1
|
argc = len(args)
|
||||||
|
|
||||||
if argc == 0:
|
mode = listget(args, 0, 'password')
|
||||||
mode = 'password'
|
if 'help' in mode:
|
||||||
length = DEFAULT_LENGTH
|
|
||||||
|
|
||||||
elif args[1].isdigit():
|
|
||||||
mode = 'password'
|
|
||||||
length = int(args[1])
|
|
||||||
|
|
||||||
elif args[1] in 'DdPp':
|
|
||||||
mode = 'password'
|
|
||||||
length = DEFAULT_LENGTH
|
|
||||||
|
|
||||||
elif 'sent' in args[1].lower() and argc == 1:
|
|
||||||
mode = 'sentence'
|
|
||||||
length = DEFAULT_SENTENCE
|
|
||||||
|
|
||||||
elif argc == 1:
|
|
||||||
mode = None
|
|
||||||
print(HELP_MESSAGE)
|
print(HELP_MESSAGE)
|
||||||
print(HELP_SENTENCE)
|
quit()
|
||||||
|
|
||||||
elif 'sent' in args[1].lower() and args[2].isdigit():
|
if 'sent' not in mode:
|
||||||
mode = 'sentence'
|
length = listget(args, 0, str(DEFAULT_LENGTH))
|
||||||
length = int(args[2])
|
options = [a.lower() for a in args[1:]]
|
||||||
|
|
||||||
elif 'sent' in args[1].lower():
|
if '-' in length:
|
||||||
mode = 'sentence'
|
length = length.replace(' ', '')
|
||||||
|
length = [int(x) for x in length.split('-', 1)]
|
||||||
|
length = random.randint(*length)
|
||||||
|
|
||||||
|
elif not length.isdigit() and options == []:
|
||||||
|
options = [length]
|
||||||
|
length = DEFAULT_LENGTH
|
||||||
|
|
||||||
|
length = int(length)
|
||||||
|
|
||||||
|
passtype = 'standard'
|
||||||
|
if 'dd' in options:
|
||||||
|
passtype = 'digit_only'
|
||||||
|
if 'b' in options:
|
||||||
|
passtype = 'binary'
|
||||||
|
if 'h' in options:
|
||||||
|
passtype = 'hex'
|
||||||
|
|
||||||
|
if 'l' in options:
|
||||||
|
passtype += '+lowercase'
|
||||||
|
elif 'u' in options:
|
||||||
|
passtype += '+uppercase'
|
||||||
|
if 'p' in options:
|
||||||
|
passtype += '+punctuation'
|
||||||
|
if 'd' in options:
|
||||||
|
passtype += '+digits'
|
||||||
|
if 'nd' in options:
|
||||||
|
passtype += '+noduplicates'
|
||||||
|
|
||||||
|
print(make_password(length, passtype=passtype))
|
||||||
|
|
||||||
|
else:
|
||||||
|
length = listget(args, 1, str(DEFAULT_SENTENCE))
|
||||||
|
joiner = listget(args, 2, ' ')
|
||||||
|
|
||||||
|
if not length.isdigit():
|
||||||
|
joiner = length
|
||||||
length = DEFAULT_SENTENCE
|
length = DEFAULT_SENTENCE
|
||||||
|
|
||||||
if mode == 'password':
|
length = int(length)
|
||||||
punc = 'p' in args
|
|
||||||
digi = 'd' in args
|
|
||||||
digi_only = 'dd' in args
|
|
||||||
binary = 'b' in args
|
|
||||||
print(make_password(length, punc, digi, digi_only, binary))
|
|
||||||
|
|
||||||
elif mode == 'sentence':
|
|
||||||
if argc == 3:
|
|
||||||
joiner = args[3]
|
|
||||||
else:
|
|
||||||
joiner = ' '
|
|
||||||
print(make_sentence(length, joiner))
|
print(make_sentence(length, joiner))
|
||||||
|
|
||||||
else:
|
|
||||||
pass
|
|
624
Phototagger/phototagger.py
Normal file
|
@ -0,0 +1,624 @@
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import PIL.Image
|
||||||
|
import random
|
||||||
|
import sqlite3
|
||||||
|
import string
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
# UIDs consist of hex characters, so keyspace is 16 ** UID_CHARACTERS.
|
||||||
|
UID_CHARACTERS = 16
|
||||||
|
VALID_TAG_CHARS = string.ascii_lowercase + string.digits + '_-'
|
||||||
|
MAX_TAG_NAME_LENGTH = 32
|
||||||
|
|
||||||
|
SQL_PHOTO_COLUMNCOUNT = 8
|
||||||
|
SQL_PHOTO_ID = 0
|
||||||
|
SQL_PHOTO_FILEPATH = 1
|
||||||
|
SQL_PHOTO_EXTENSION = 2
|
||||||
|
SQL_PHOTO_WIDTH = 3
|
||||||
|
SQL_PHOTO_HEIGHT = 4
|
||||||
|
SQL_PHOTO_AREA = 5
|
||||||
|
SQL_PHOTO_BYTES = 6
|
||||||
|
SQL_PHOTO_CREATED = 7
|
||||||
|
|
||||||
|
SQL_PHOTOTAG_COLUMNCOUNT = 2
|
||||||
|
SQL_PHOTOTAG_PHOTOID = 0
|
||||||
|
SQL_PHOTOTAG_TAGID = 1
|
||||||
|
|
||||||
|
SQL_SYN_COLUMNCOUNT = 2
|
||||||
|
SQL_SYN_NAME = 0
|
||||||
|
SQL_SYN_MASTER = 1
|
||||||
|
|
||||||
|
SQL_TAG_COLUMNCOUNT = 2
|
||||||
|
SQL_TAG_ID = 0
|
||||||
|
SQL_TAG_NAME = 1
|
||||||
|
|
||||||
|
DB_INIT = '''
|
||||||
|
CREATE TABLE IF NOT EXISTS photos(
|
||||||
|
id TEXT,
|
||||||
|
filepath TEXT,
|
||||||
|
extension TEXT,
|
||||||
|
width INT,
|
||||||
|
height INT,
|
||||||
|
area INT,
|
||||||
|
bytes INT,
|
||||||
|
created INT
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS tags(
|
||||||
|
id TEXT,
|
||||||
|
name TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS photo_tag_rel(
|
||||||
|
photoid TEXT,
|
||||||
|
tagid TEXT
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS tag_synonyms(
|
||||||
|
name TEXT,
|
||||||
|
mastername TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_photo_id on photos(id);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_photo_path on photos(filepath);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_photo_created on photos(created);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS index_tag_id on tags(id);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_tag_name on tags(name);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS index_tagrel_photoid on photo_tag_rel(photoid);
|
||||||
|
CREATE INDEX IF NOT EXISTS index_tagrel_tagid on photo_tag_rel(tagid);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS index_tagsyn_name on tag_synonyms(name);
|
||||||
|
'''
|
||||||
|
|
||||||
|
def basex(number, base, alphabet='0123456789abcdefghijklmnopqrstuvwxyz'):
|
||||||
|
'''
|
||||||
|
Converts an integer to a different base string.
|
||||||
|
Based on http://stackoverflow.com/a/1181922/5430534
|
||||||
|
'''
|
||||||
|
if base > len(alphabet):
|
||||||
|
raise Exception('alphabet %s does not support base %d' % (
|
||||||
|
repr(alphabet), base))
|
||||||
|
alphabet = alphabet[:base]
|
||||||
|
if not isinstance(number, (int, str)):
|
||||||
|
raise TypeError('number must be an integer')
|
||||||
|
number = int(number)
|
||||||
|
based = ''
|
||||||
|
sign = ''
|
||||||
|
if number < 0:
|
||||||
|
sign = '-'
|
||||||
|
number = -number
|
||||||
|
if 0 <= number < len(alphabet):
|
||||||
|
return sign + alphabet[number]
|
||||||
|
while number != 0:
|
||||||
|
number, i = divmod(number, len(alphabet))
|
||||||
|
based = alphabet[i] + based
|
||||||
|
return sign + based
|
||||||
|
|
||||||
|
def getnow(timestamp=True):
|
||||||
|
'''
|
||||||
|
Return the current UTC timestamp or datetime object.
|
||||||
|
'''
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
if timestamp:
|
||||||
|
return now.timestamp()
|
||||||
|
return now
|
||||||
|
|
||||||
|
def is_xor(x, y):
|
||||||
|
'''
|
||||||
|
Return True if and only if one of (x, y) is truthy.
|
||||||
|
'''
|
||||||
|
same = (bool(x) == bool(y))
|
||||||
|
return not same
|
||||||
|
|
||||||
|
def normalize_tagname(tagname):
|
||||||
|
'''
|
||||||
|
Tag names can only consist of lowercase letters, underscores,
|
||||||
|
and hyphens. The given tagname is lowercased, gets its spaces
|
||||||
|
replaced by underscores, and is stripped of any not-whitelisted
|
||||||
|
characters.
|
||||||
|
'''
|
||||||
|
tagname = tagname.lower()
|
||||||
|
tagname = tagname.replace(' ', '_')
|
||||||
|
tagname = (c for c in tagname if c in VALID_TAG_CHARS)
|
||||||
|
tagname = ''.join(tagname)
|
||||||
|
if len(tagname) == 0:
|
||||||
|
raise ValueError('Normalized tagname of length 0.')
|
||||||
|
return tagname
|
||||||
|
|
||||||
|
def not_implemented(function):
|
||||||
|
'''
|
||||||
|
Great for keeping track of which functions still need to be filled out.
|
||||||
|
'''
|
||||||
|
warnings.warn('%s is not implemented' % function.__name__)
|
||||||
|
return function
|
||||||
|
|
||||||
|
def uid(length=None):
|
||||||
|
'''
|
||||||
|
Generate a u-random hex string..
|
||||||
|
'''
|
||||||
|
if length is None:
|
||||||
|
length = UID_CHARACTERS
|
||||||
|
identifier = ''.join('{:02x}'.format(x) for x in os.urandom(math.ceil(length / 2)))
|
||||||
|
if len(identifier) > length:
|
||||||
|
identifier = identifier[:length]
|
||||||
|
return identifier
|
||||||
|
|
||||||
|
|
||||||
|
class NoSuchPhoto(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoSuchTag(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class PhotoExists(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TagExists(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class XORException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class PhotoDB:
|
||||||
|
'''
|
||||||
|
This class represents an SQLite3 database containing the following tables:
|
||||||
|
|
||||||
|
photos:
|
||||||
|
Rows represent image files on the local disk.
|
||||||
|
Entries contain a unique ID, the image's filepath, and metadata
|
||||||
|
like dimensions and filesize.
|
||||||
|
|
||||||
|
tags:
|
||||||
|
Rows represent labels, which can be applied to an arbitrary number of
|
||||||
|
photos. Photos may be selected by which tags they contain.
|
||||||
|
Entries contain a unique ID and a name.
|
||||||
|
|
||||||
|
photo_tag_rel:
|
||||||
|
Rows represent a Photo's ownership of a particular Tag.
|
||||||
|
|
||||||
|
tag_synonyms:
|
||||||
|
Rows represent relationships between two tag names, so that they both
|
||||||
|
resolve to the same Tag object when selected. Entries contain the
|
||||||
|
subordinate name and master name.
|
||||||
|
The master name MUST also exist in the `tags` table.
|
||||||
|
If a new synonym is created referring to another synoym, the master name
|
||||||
|
will be resolved and used instead, so a synonym is never in the master
|
||||||
|
column.
|
||||||
|
Tag objects will ALWAYS represent the master tag.
|
||||||
|
|
||||||
|
Note that the entries in this table do not contain ID numbers.
|
||||||
|
The rationale here is that "coco" is a synonym for "chocolate" regardless
|
||||||
|
of the "chocolate" tag's ID, or the fact that you decided to rename your
|
||||||
|
"chocolate" tag to "candy" after applying it to a few photos.
|
||||||
|
The `rename_tag` method includes a parameter `apply_to_synonyms` if you do
|
||||||
|
want them to follow.
|
||||||
|
'''
|
||||||
|
def __init__(self, databasename='phototagger.db'):
|
||||||
|
self.databasename = databasename
|
||||||
|
self.sql = sqlite3.connect(databasename)
|
||||||
|
self.cur = self.sql.cursor()
|
||||||
|
statements = DB_INIT.split(';')
|
||||||
|
for statement in statements:
|
||||||
|
self.cur.execute(statement)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'PhotoDB(databasename={dbname})'.format(dbname=repr(self.databasename))
|
||||||
|
|
||||||
|
def add_photo_tag(self, photoid, tag=None, commit=True):
|
||||||
|
'''
|
||||||
|
Apply a tag to a photo. `tag` may be the name of the tag or a Tag
|
||||||
|
object from the same PhotoDB.
|
||||||
|
|
||||||
|
`tag` may NOT be the tag's ID, since an ID would also have been a valid name.
|
||||||
|
'''
|
||||||
|
if isinstance(tag, Tag) and tag.photodb is self:
|
||||||
|
tagid = tag.id
|
||||||
|
else:
|
||||||
|
tag = self.get_tag_by_name(tag)
|
||||||
|
if tag is None:
|
||||||
|
raise NoSuchTag(tag)
|
||||||
|
tagid = tag.id
|
||||||
|
|
||||||
|
self.cur.execute('SELECT * FROM photos WHERE id == ?', [photoid])
|
||||||
|
if self.cur.fetchone() is None:
|
||||||
|
raise NoSuchPhoto(photoid)
|
||||||
|
|
||||||
|
self.cur.execute('SELECT * FROM photo_tag_rel WHERE photoid == ? AND tagid == ?', [photoid, tagid])
|
||||||
|
if self.cur.fetchone() is not None:
|
||||||
|
warning = 'Photo {photoid} already has tag {tagid}'.format(photoid=photoid, tagid=tagid)
|
||||||
|
warnings.warn(warning)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.cur.execute('INSERT INTO photo_tag_rel VALUES(?, ?)', [photoid, tagid])
|
||||||
|
if commit:
|
||||||
|
self.sql.commit()
|
||||||
|
|
||||||
|
@not_implemented
|
||||||
|
def convert_tag_to_synonym(self, tagname, mastertag):
|
||||||
|
'''
|
||||||
|
Convert an independent tag into a synonym for a different tag.
|
||||||
|
All photos which possess the current tag will have it replaced
|
||||||
|
with the master tag.
|
||||||
|
'''
|
||||||
|
photos = self.get_photos_by_tag(musts=[tagname])
|
||||||
|
|
||||||
|
def get_photo_by_id(self, photoid):
|
||||||
|
'''
|
||||||
|
Return this Photo object, or None if it does not exist.
|
||||||
|
'''
|
||||||
|
self.cur.execute('SELECT * FROM photos WHERE id == ?', [photoid])
|
||||||
|
photo = cur.fetchone()
|
||||||
|
if photo is None:
|
||||||
|
return None
|
||||||
|
photo = self.tuple_to_photo(photo)
|
||||||
|
return photo
|
||||||
|
|
||||||
|
def get_photo_by_path(self, filepath):
|
||||||
|
'''
|
||||||
|
Return this Photo object, or None if it does not exist.
|
||||||
|
'''
|
||||||
|
filepath = os.path.abspath(filepath)
|
||||||
|
self.cur.execute('SELECT * FROM photos WHERE filepath == ?', [filepath])
|
||||||
|
photo = self.cur.fetchone()
|
||||||
|
if photo is None:
|
||||||
|
return None
|
||||||
|
photo = self.tuple_to_photo(photo)
|
||||||
|
return photo
|
||||||
|
|
||||||
|
def get_photos_by_recent(self):
|
||||||
|
'''
|
||||||
|
Yield photo objects in order of creation time.
|
||||||
|
'''
|
||||||
|
# We're going to use a second cursor because the first one may
|
||||||
|
# get used for something else, deactivating this query.
|
||||||
|
cur2 = self.sql.cursor()
|
||||||
|
cur2.execute('SELECT * FROM photos ORDER BY created DESC')
|
||||||
|
while True:
|
||||||
|
f = cur2.fetchone()
|
||||||
|
if f is None:
|
||||||
|
return
|
||||||
|
photo = self.tuple_to_photo(f)
|
||||||
|
yield photo
|
||||||
|
|
||||||
|
@not_implemented
|
||||||
|
def get_photos_by_tag(
|
||||||
|
self,
|
||||||
|
musts=None,
|
||||||
|
mays=None,
|
||||||
|
forbids=None,
|
||||||
|
forbid_unspecified=False,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Given one or multiple tags, yield photos possessing those tags.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
musts :
|
||||||
|
A list of strings or Tag objects.
|
||||||
|
Photos MUST have ALL tags in this list.
|
||||||
|
mays :
|
||||||
|
A list of strings or Tag objects.
|
||||||
|
If `forbid_unspecified` is True, then Photos MUST have AT LEAST ONE tag in this list.
|
||||||
|
If `forbid_unspecified` is False, then Photos MAY or MAY NOT have ANY tag in this list.
|
||||||
|
forbids :
|
||||||
|
A list of strings or Tag objects.
|
||||||
|
Photos MUST NOT have ANY tag in the list.
|
||||||
|
forbid_unspecified :
|
||||||
|
True or False.
|
||||||
|
If False, Photos need only comply with the `musts`.
|
||||||
|
If True, Photos need to comply with both `musts` and `mays`.
|
||||||
|
'''
|
||||||
|
if all(arg is None for arg in (musts, mays, forbids)):
|
||||||
|
raise TypeError('All arguments cannot be None')
|
||||||
|
|
||||||
|
def get_tag_by_id(self, tagid):
|
||||||
|
self.cur.execute('SELECT * FROM tags WHERE id == ?', [tagid])
|
||||||
|
tag = self.cur.fetchone()
|
||||||
|
if tag is None:
|
||||||
|
return None
|
||||||
|
tag = self.tuple_to_tag(tag)
|
||||||
|
return tag
|
||||||
|
|
||||||
|
def get_tag_by_name(self, tagname):
|
||||||
|
'''
|
||||||
|
Return the Tag object that the given tagname resolves to.
|
||||||
|
|
||||||
|
If the given tagname is a synonym, the master tag will be returned.
|
||||||
|
'''
|
||||||
|
if isinstance(tagname, Tag):
|
||||||
|
return tagname
|
||||||
|
|
||||||
|
self.cur.execute('SELECT * FROM tag_synonyms WHERE name == ?', [tagname])
|
||||||
|
fetch = self.cur.fetchone()
|
||||||
|
if fetch is not None:
|
||||||
|
mastertagid = fetch[SQL_SYN_MASTER]
|
||||||
|
tag = self.get_tag_by_id(mastertagid)
|
||||||
|
return tag
|
||||||
|
|
||||||
|
self.cur.execute('SELECT * FROM tags WHERE name == ?', [tagname])
|
||||||
|
fetch = self.cur.fetchone()
|
||||||
|
if fetch is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
tag = self.tuple_to_tag(fetch)
|
||||||
|
return tag
|
||||||
|
|
||||||
|
def get_tags_by_photo(self, photoid):
|
||||||
|
'''
|
||||||
|
Return the tags assigned to the given photo.
|
||||||
|
'''
|
||||||
|
self.cur.execute('SELECT * FROM photo_tag_rel WHERE photoid == ?', [photoid])
|
||||||
|
tags = self.cur.fetchall()
|
||||||
|
tagobjects = []
|
||||||
|
for tag in tags:
|
||||||
|
tagid = tag[SQL_PHOTOTAG_TAGID]
|
||||||
|
tagobj = self.get_tag_by_id(tagid)
|
||||||
|
if tagobj is None:
|
||||||
|
warnings.warn('Photo {photid} contains unkown tagid {tagid}'.format(photoid=photoid, tagid=tagid))
|
||||||
|
continue
|
||||||
|
tagobjects.append(tagobj)
|
||||||
|
return tagobjects
|
||||||
|
|
||||||
|
def new_photo(self, filename, tags=[], allow_duplicates=False):
|
||||||
|
'''
|
||||||
|
Given a filepath, determine its attributes and create a new Photo object in the
|
||||||
|
database. Tags may be applied now or later.
|
||||||
|
|
||||||
|
If `allow_duplicates` is False, we will first check the database for any files
|
||||||
|
with the same path and raise PhotoExists if found.
|
||||||
|
'''
|
||||||
|
filename = os.path.abspath(filename)
|
||||||
|
if not allow_duplicates:
|
||||||
|
existing = self.get_photo_by_path(filename)
|
||||||
|
if existing is not None:
|
||||||
|
raise PhotoExists(filename, existing)
|
||||||
|
|
||||||
|
# I want the caller to receive any exceptions this raises.
|
||||||
|
image = PIL.Image.open(filename)
|
||||||
|
|
||||||
|
extension = os.path.splitext(filename)[1]
|
||||||
|
extension = extension.replace('.', '')
|
||||||
|
(width, height) = image.size
|
||||||
|
area = width * height
|
||||||
|
bytes = os.path.getsize(filename)
|
||||||
|
created = int(getnow())
|
||||||
|
photoid = self.new_uid('photos')
|
||||||
|
data = [None] * SQL_PHOTO_COLUMNCOUNT
|
||||||
|
data[SQL_PHOTO_ID] = photoid
|
||||||
|
data[SQL_PHOTO_FILEPATH] = filename
|
||||||
|
data[SQL_PHOTO_EXTENSION] = extension
|
||||||
|
data[SQL_PHOTO_WIDTH] = width
|
||||||
|
data[SQL_PHOTO_HEIGHT] = height
|
||||||
|
data[SQL_PHOTO_AREA] = area
|
||||||
|
data[SQL_PHOTO_BYTES] = bytes
|
||||||
|
data[SQL_PHOTO_CREATED] = created
|
||||||
|
photo = self.tuple_to_photo(data)
|
||||||
|
self.cur.execute('INSERT INTO photos VALUES(?, ?, ?, ?, ?, ?, ?, ?)', data)
|
||||||
|
for tag in tags:
|
||||||
|
try:
|
||||||
|
self.add_photo_tag(photoid, tag, commit=False)
|
||||||
|
except NoSuchTag:
|
||||||
|
self.sql.rollback()
|
||||||
|
raise
|
||||||
|
self.sql.commit()
|
||||||
|
return photo
|
||||||
|
|
||||||
|
def new_tag(self, tagname):
|
||||||
|
'''
|
||||||
|
Register a new tag.
|
||||||
|
'''
|
||||||
|
tagname = normalize_tagname(tagname)
|
||||||
|
if self.get_tag_by_name(tagname) is not None:
|
||||||
|
raise TagExists(tagname)
|
||||||
|
tagid = self.new_uid('tags')
|
||||||
|
self.cur.execute('INSERT INTO tags VALUES(?, ?)', [tagid, tagname])
|
||||||
|
self.sql.commit()
|
||||||
|
tag = self.tuple_to_tag([tagid, tagname])
|
||||||
|
return tag
|
||||||
|
|
||||||
|
def new_tag_synonym(self, tagname, mastertagname):
|
||||||
|
'''
|
||||||
|
Register a new synonym for an existing tag.
|
||||||
|
'''
|
||||||
|
tagname = normalize_tagname(tagname)
|
||||||
|
mastertagname = normalize_tagname(mastertagname)
|
||||||
|
|
||||||
|
if tagname == mastertagname:
|
||||||
|
raise TagExists(tagname)
|
||||||
|
|
||||||
|
tag = self.get_tag_by_name(tagname)
|
||||||
|
if tag is not None:
|
||||||
|
raise TagExists(tagname)
|
||||||
|
|
||||||
|
mastertag = self.get_tag_by_name(mastertagname)
|
||||||
|
if mastertag is None:
|
||||||
|
raise NoSuchTag(mastertagname)
|
||||||
|
mastertagname = mastertag.name
|
||||||
|
|
||||||
|
self.cur.execute('INSERT INTO tag_synonyms VALUES(?, ?)', [tagname, mastertagname])
|
||||||
|
self.sql.commit()
|
||||||
|
|
||||||
|
def new_uid(self, table):
|
||||||
|
'''
|
||||||
|
Create a new UID that is unique to the given table.
|
||||||
|
'''
|
||||||
|
result = None
|
||||||
|
# Well at least we won't get sql injection this way.
|
||||||
|
table = normalize_tagname(table)
|
||||||
|
query = 'SELECT * FROM {table} WHERE id == ?'.format(table=table)
|
||||||
|
while result is None:
|
||||||
|
i = uid()
|
||||||
|
# Just gotta be sure, man.
|
||||||
|
self.cur.execute(query, [i])
|
||||||
|
if self.cur.fetchone() is None:
|
||||||
|
result = i
|
||||||
|
return result
|
||||||
|
|
||||||
|
@not_implemented
|
||||||
|
def remove_photo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@not_implemented
|
||||||
|
def remove_tag(self, tagid=None, tagname=None):
|
||||||
|
'''
|
||||||
|
Delete a tag and its relation to any photos.
|
||||||
|
'''
|
||||||
|
if not is_xor(tagid, tagname):
|
||||||
|
raise XORException('One and only one of `tagid`, `tagname` can be passed.')
|
||||||
|
|
||||||
|
if tagid is not None:
|
||||||
|
self.cur.execute('SELECT * FROM tags WHERE id == ?', [tagid])
|
||||||
|
tag = self.cur.fetchone()
|
||||||
|
|
||||||
|
elif tagname is not None:
|
||||||
|
tagname = normalize_tagname(tagname)
|
||||||
|
self.cur.execute('SELECT * from tags WHERE name == ?', [tagname])
|
||||||
|
tag = self.cur.fetchone()
|
||||||
|
|
||||||
|
if tag is None:
|
||||||
|
raise NoSuchTag(tagid or tagname)
|
||||||
|
|
||||||
|
tag = self.tuple_to_tag(tag)
|
||||||
|
|
||||||
|
self.cur.execute('DELETE FROM tags WHERE id == ?', [tag.id])
|
||||||
|
self.cur.execute('DELETE FROM photo_tag_rel WHERE tagid == ?', [tag.id])
|
||||||
|
self.cur.execute('DELETE FROM tag_synonyms WHERE mastername == ?', [tag.name])
|
||||||
|
self.sql.commit()
|
||||||
|
|
||||||
|
@not_implemented
|
||||||
|
def remove_tag_synonym(self, tagname):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def tuple_to_photo(self, tu):
|
||||||
|
'''
|
||||||
|
Given a tuple like the ones from an sqlite query,
|
||||||
|
create a Photo object.
|
||||||
|
'''
|
||||||
|
photoid = tu[SQL_PHOTO_ID]
|
||||||
|
tags = self.get_tags_by_photo(photoid)
|
||||||
|
|
||||||
|
photo = Photo(
|
||||||
|
photodb = self,
|
||||||
|
photoid = photoid,
|
||||||
|
filepath = tu[SQL_PHOTO_FILEPATH],
|
||||||
|
extension = tu[SQL_PHOTO_EXTENSION],
|
||||||
|
width = tu[SQL_PHOTO_WIDTH],
|
||||||
|
height = tu[SQL_PHOTO_HEIGHT],
|
||||||
|
area = tu[SQL_PHOTO_AREA],
|
||||||
|
created = tu[SQL_PHOTO_CREATED],
|
||||||
|
bytes = tu[SQL_PHOTO_BYTES],
|
||||||
|
tags = tags,
|
||||||
|
)
|
||||||
|
return photo
|
||||||
|
|
||||||
|
def tuple_to_tag(self, tu):
|
||||||
|
'''
|
||||||
|
Given a tuple like the ones from an sqlite query,
|
||||||
|
create a Tag object.
|
||||||
|
'''
|
||||||
|
tag = Tag(
|
||||||
|
photodb = self,
|
||||||
|
tagid = tu[SQL_TAG_ID],
|
||||||
|
name = tu[SQL_TAG_NAME]
|
||||||
|
)
|
||||||
|
return tag
|
||||||
|
|
||||||
|
class Photo:
|
||||||
|
'''
|
||||||
|
This class represents a PhotoDB entry containing information about an image file.
|
||||||
|
Photo objects cannot exist without a corresponding PhotoDB object, because
|
||||||
|
Photos are not the actual files, just the database entry.
|
||||||
|
'''
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
photodb,
|
||||||
|
photoid,
|
||||||
|
filepath,
|
||||||
|
extension,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
area,
|
||||||
|
bytes,
|
||||||
|
created,
|
||||||
|
tags=[],
|
||||||
|
):
|
||||||
|
self.photodb = photodb
|
||||||
|
self.id = photoid
|
||||||
|
self.filepath = filepath
|
||||||
|
self.extension = extension
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.area = area
|
||||||
|
self.bytes = bytes
|
||||||
|
self.created = created
|
||||||
|
self.tags = tags
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, Photo):
|
||||||
|
return False
|
||||||
|
return self.id == other.id
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.id)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
r = ('Photo(photodb={photodb}, ',
|
||||||
|
'photoid={photoid}, ',
|
||||||
|
'filepath={filepath}, ',
|
||||||
|
'extension={extension}, ',
|
||||||
|
'width={width}, ',
|
||||||
|
'height={height}, ',
|
||||||
|
'area={area}, ',
|
||||||
|
'bytes={bytes} ',
|
||||||
|
'created={created})'
|
||||||
|
)
|
||||||
|
r = ''.join(r)
|
||||||
|
r = r.format(
|
||||||
|
photodb = repr(self.photodb),
|
||||||
|
photoid = repr(self.id),
|
||||||
|
filepath = repr(self.filepath),
|
||||||
|
extension = repr(self.extension),
|
||||||
|
width = repr(self.width),
|
||||||
|
height = repr(self.height),
|
||||||
|
bytes = repr(self.bytes),
|
||||||
|
area = repr(self.area),
|
||||||
|
created = repr(self.created),
|
||||||
|
)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'Photo: %s' % self.id
|
||||||
|
|
||||||
|
def add_photo_tag(self, tagname):
|
||||||
|
return self.photodb.add_photo_tag(self.id, tagname, commit=True)
|
||||||
|
|
||||||
|
class Tag:
|
||||||
|
'''
|
||||||
|
This class represents a Tag, which can be applied to Photos for
|
||||||
|
organization.
|
||||||
|
'''
|
||||||
|
def __init__(self, photodb, tagid, name):
|
||||||
|
self.photodb = photodb
|
||||||
|
self.id = tagid
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, Tag):
|
||||||
|
return False
|
||||||
|
return self.id == other.id
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.id)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
r = 'Tag(photodb={photodb}, name={name}, tagid={tagid})'
|
||||||
|
r = r.format(
|
||||||
|
photodb = repr(self.photodb),
|
||||||
|
name = repr(self.name),
|
||||||
|
tagid = repr(self.id),
|
||||||
|
)
|
||||||
|
return r
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'Tag: %s : %s' % (self.id, self.name)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
p = PhotoDB()
|
BIN
Phototagger/phototagger_tests.db
Normal file
45
Phototagger/phototagger_tests.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import os
|
||||||
|
import phototagger
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
DB_NAME = ':memory:'
|
||||||
|
|
||||||
|
#try:
|
||||||
|
# os.remove(DB_NAME)
|
||||||
|
# print('Deleted old database.')
|
||||||
|
#except FileNotFound:
|
||||||
|
# pass
|
||||||
|
|
||||||
|
class PhotoDBTest(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.p = phototagger.PhotoDB(DB_NAME)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_add_and_remove_tag(self):
|
||||||
|
tag = self.p.new_tag('trains')
|
||||||
|
self.assertEqual(tag.name, 'trains')
|
||||||
|
self.assertEqual(len(tag.id), phototagger.UID_CHARACTERS)
|
||||||
|
|
||||||
|
tag2 = self.p.get_tag_by_id(tag.id)
|
||||||
|
self.assertEqual(tag, tag2)
|
||||||
|
|
||||||
|
tag3 = self.p.get_tag_by_name(tag.name)
|
||||||
|
self.assertEqual(tag, tag3)
|
||||||
|
|
||||||
|
self.assertEqual(tag2, tag3)
|
||||||
|
|
||||||
|
self.p.remove_tag(tagid=tag.id)
|
||||||
|
|
||||||
|
tag4 = self.p.get_tag_by_id(tag.id)
|
||||||
|
self.assertIsNone(tag4)
|
||||||
|
|
||||||
|
def test_new_tag_invalid_name(self):
|
||||||
|
print('NOT IMPLEMENTED')
|
||||||
|
|
||||||
|
def test_new_tag_too_long(self):
|
||||||
|
print('NOT IMPLEMENTED')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
BIN
Phototagger/samples/bolts.jpg
Normal file
After Width: | Height: | Size: 467 KiB |
BIN
Phototagger/samples/reddit.png
Normal file
After Width: | Height: | Size: 900 KiB |
BIN
Phototagger/samples/train.jpg
Normal file
After Width: | Height: | Size: 321 KiB |
BIN
RGBLayers/ear.jpg
Normal file
After Width: | Height: | Size: 120 KiB |
BIN
RGBLayers/ear_B.jpg
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
RGBLayers/ear_G.jpg
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
RGBLayers/ear_R.jpg
Normal file
After Width: | Height: | Size: 42 KiB |
52
RGBLayers/rgblayers.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import PIL.Image
|
||||||
|
|
||||||
|
def listget(li, index, fallback=None):
|
||||||
|
try:
|
||||||
|
return li[index]
|
||||||
|
except IndexError:
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
def onechannel(tu, index):
|
||||||
|
ret = [0, 0, 0, 255]
|
||||||
|
ret[index] = listget(tu, index, 0)
|
||||||
|
ret[3] = listget(tu, 3, 255)
|
||||||
|
return tuple(ret)
|
||||||
|
|
||||||
|
def splitchannels(filename):
|
||||||
|
image = PIL.Image.open(filename)
|
||||||
|
(base, extension) = os.path.splitext(filename)
|
||||||
|
image_r = PIL.Image.new(image.mode, image.size)
|
||||||
|
image_g = PIL.Image.new(image.mode, image.size)
|
||||||
|
image_b = PIL.Image.new(image.mode, image.size)
|
||||||
|
width = image.size[0]
|
||||||
|
height = image.size[1]
|
||||||
|
pixels = list(image.getdata())
|
||||||
|
print('Extracting R')
|
||||||
|
image_r.putdata([onechannel(x, 0) for x in pixels])
|
||||||
|
print('Extracting G')
|
||||||
|
image_g.putdata([onechannel(x, 1) for x in pixels])
|
||||||
|
print('Extracting B')
|
||||||
|
image_b.putdata([onechannel(x, 2) for x in pixels])
|
||||||
|
#for (index, pixel) in enumerate(iter(image.getdata())):
|
||||||
|
# x = index % width
|
||||||
|
# y = index // height
|
||||||
|
# co = (x, y)
|
||||||
|
# pixel = image.getpixel(co)
|
||||||
|
# r = listget(pixel, 0, 0)
|
||||||
|
# g = listget(pixel, 1, 0)
|
||||||
|
# b = listget(pixel, 2, 0)
|
||||||
|
# o = listget(pixel, 3, 255)
|
||||||
|
# image_r.putpixel(co, (r, 0, 0, o))
|
||||||
|
# image_g.putpixel(co, (0, g, 0, o))
|
||||||
|
# image_b.putpixel(co, (0, 0, b, 0))
|
||||||
|
# if x == 0:
|
||||||
|
# print(y)
|
||||||
|
image_r.save('%s_R%s' % (base, extension), quality=100)
|
||||||
|
image_g.save('%s_G%s' % (base, extension), quality=100)
|
||||||
|
image_b.save('%s_B%s' % (base, extension), quality=100)
|
||||||
|
|
||||||
|
filename = sys.argv[1]
|
||||||
|
splitchannels(filename)
|
||||||
|
i = PIL.Image.open(filename)
|
|
@ -1,6 +1,13 @@
|
||||||
Steganographic
|
Steganographic
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
2015 01 15:
|
||||||
|
Now supports variable "bitness", the number of bits per color channel to overwrite.
|
||||||
|
Previously, bitness was always 1, to maximize transparency.
|
||||||
|
Now, bitness can be 1-8 to favor transparency or information density.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Let's be honest, this was really just an excuse to make big Terminal headers out of hashmarks.
|
Let's be honest, this was really just an excuse to make big Terminal headers out of hashmarks.
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,11 +29,13 @@ Each Image pixel holds 3 Secret bits, so the Image must have at least `((secretb
|
||||||
An Image can hold `((3 * (pixels - 14)) / 8)` Secret bytes.
|
An Image can hold `((3 * (pixels - 14)) / 8)` Secret bytes.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
> steganographic.py encode imagefilename.png secretfilename.ext
|
> steganographic.py encode imagefilename.png secretfilename.ext bitness
|
||||||
> steganographic.py decode lacedimagename.png
|
> steganographic.py decode lacedimagename.png bitness
|
||||||
|
|
||||||
|
where bitness defaults to 1 in both cases.
|
||||||
|
|
||||||
|
|
||||||
Reference table for files with NO EXTENSION.
|
Reference table for files with NO EXTENSION and bitness of 1.
|
||||||
For each extension character, subtract 1 byte from secret size
|
For each extension character, subtract 1 byte from secret size
|
||||||
|
|
||||||
pixels | example dimensions | Secret file size
|
pixels | example dimensions | Secret file size
|
||||||
|
|
After Width: | Height: | Size: 1.9 MiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 1.8 MiB |
After Width: | Height: | Size: 1.8 MiB |
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:0a9c758425f9185dd207183b283f639cd6499d042ad75b51e3f7407624aeb8aa
|
|
||||||
size 6002882
|
|
Before Width: | Height: | Size: 132 B After Width: | Height: | Size: 1.7 MiB |
|
@ -49,31 +49,99 @@ import math
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# 11 for the content length
|
# 11 pixels for the secret file size
|
||||||
HEADER_SIZE = 11
|
HEADER_SIZE = 11
|
||||||
|
|
||||||
|
FILE_READ_SIZE = 4 * 1024
|
||||||
|
|
||||||
class StegError(Exception):
|
class StegError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class BitsToImage:
|
||||||
|
def __init__(self, image, bitness):
|
||||||
|
self.image = image
|
||||||
|
self.bitness = bitness
|
||||||
|
self.width = image.size[0]
|
||||||
|
self.pixel_index = -1
|
||||||
|
self.channel_index = 0
|
||||||
|
self.bit_index = self.bitness - 1
|
||||||
|
self.active_pixel = None
|
||||||
|
self.x = 0
|
||||||
|
self.y = 0
|
||||||
|
|
||||||
|
def _write(self, bit):
|
||||||
|
if self.active_pixel is None:
|
||||||
|
self.pixel_index += 1
|
||||||
|
self.channel_index = 0
|
||||||
|
self.bit_index = self.bitness - 1
|
||||||
|
(self.x, self.y) = index_to_xy(self.pixel_index, self.width)
|
||||||
|
self.active_pixel = list(self.image.getpixel((self.x, self.y)))
|
||||||
|
|
||||||
|
channel = self.active_pixel[self.channel_index]
|
||||||
|
channel = set_bit(channel, self.bit_index, int(bit))
|
||||||
|
self.active_pixel[self.channel_index] = channel
|
||||||
|
self.bit_index -= 1
|
||||||
|
|
||||||
|
if self.bit_index < 0:
|
||||||
|
# We have exhausted our bitness for this channel.
|
||||||
|
self.bit_index = self.bitness - 1
|
||||||
|
self.channel_index += 1
|
||||||
|
if self.channel_index == 3:
|
||||||
|
# We have exhausted the channels for this pixel.
|
||||||
|
self.image.putpixel((self.x, self.y), tuple(self.active_pixel))
|
||||||
|
self.active_pixel = None
|
||||||
|
|
||||||
|
def write(self, bits):
|
||||||
|
for bit in bits:
|
||||||
|
self._write(bit)
|
||||||
|
|
||||||
|
|
||||||
|
class ImageToBits:
|
||||||
|
def __init__(self, image, bitness):
|
||||||
|
self.image = image
|
||||||
|
self.width = image.size[0]
|
||||||
|
self.pixel_index = -1
|
||||||
|
self.active_byte = []
|
||||||
|
|
||||||
|
def _read(self):
|
||||||
|
if len(self.active_byte) == 0:
|
||||||
|
self.pixel_index += 1
|
||||||
|
(x, y) = index_to_xy(self.pixel_index, self.width)
|
||||||
|
self.active_byte = list(self.image.getpixel((x, y)))
|
||||||
|
self.active_byte = self.active_byte[:3]
|
||||||
|
self.active_byte = [binary(channel) for channel in self.active_byte]
|
||||||
|
self.active_byte = [channel[-bitness:] for channel in self.active_byte]
|
||||||
|
self.active_byte = ''.join(self.active_byte)
|
||||||
|
self.active_byte = list(self.active_byte)
|
||||||
|
|
||||||
|
ret = self.active_byte.pop(0)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def read(self, bits=1):
|
||||||
|
return ''.join(self._read() for x in range(bits))
|
||||||
|
|
||||||
|
|
||||||
def binary(i):
|
def binary(i):
|
||||||
return bin(i)[2:].rjust(8, '0')
|
return bin(i)[2:].rjust(8, '0')
|
||||||
|
|
||||||
def increment_pixel(save=True):
|
def chunk_iterable(iterable, chunk_length, allow_incomplete=True):
|
||||||
'''
|
'''
|
||||||
Increment the active channel, and roll to the next pixel when appropriate.
|
Given an iterable, divide it into chunks of length `chunk_length`.
|
||||||
|
If `allow_incomplete` is True, the final element of the returned list may be shorter
|
||||||
|
than `chunk_length`. If it is False, those items are discarded.
|
||||||
'''
|
'''
|
||||||
global pixel
|
if len(iterable) % chunk_length != 0 and allow_incomplete:
|
||||||
global pixel_index
|
overflow = 1
|
||||||
global channel_index
|
else:
|
||||||
channel_index += 1
|
overflow = 0
|
||||||
if channel_index == 3:
|
|
||||||
channel_index = 0
|
steps = (len(iterable) // chunk_length) + overflow
|
||||||
if save:
|
return [iterable[chunk_length * x : (chunk_length * x) + chunk_length] for x in range(steps)]
|
||||||
image.putpixel((pixel_index % image.size[0], pixel_index // image.size[0]), tuple(pixel))
|
|
||||||
#print('wrote', pixel)
|
def index_to_xy(index, width):
|
||||||
pixel_index += 1
|
x = index % width
|
||||||
pixel = list(image.getpixel( (pixel_index % image.size[0], pixel_index // image.size[0]) ))
|
y = index // width
|
||||||
#print('opend', pixel)
|
return (x, y)
|
||||||
|
|
||||||
def bytes_to_pixels(bytes):
|
def bytes_to_pixels(bytes):
|
||||||
return ((bytes * (8 / 3)) + 14)
|
return ((bytes * (8 / 3)) + 14)
|
||||||
|
@ -81,6 +149,15 @@ def bytes_to_pixels(bytes):
|
||||||
def pixels_to_bytes(pixels):
|
def pixels_to_bytes(pixels):
|
||||||
return ((3 * (pixels - 14)) / 8)
|
return ((3 * (pixels - 14)) / 8)
|
||||||
|
|
||||||
|
def set_bit(number, index, newvalue):
|
||||||
|
# Thanks unwind
|
||||||
|
# http://stackoverflow.com/a/12174051/5430534
|
||||||
|
mask = 1 << index
|
||||||
|
number &= ~mask
|
||||||
|
if newvalue:
|
||||||
|
number |= mask
|
||||||
|
return number
|
||||||
|
|
||||||
############## #### #### ######## ###### ########## ##############
|
############## #### #### ######## ###### ########## ##############
|
||||||
#### ## #### #### #### #### #### #### #### #### #### ##
|
#### ## #### #### #### #### #### #### #### #### #### ##
|
||||||
#### ###### #### #### #### #### #### #### #### ####
|
#### ###### #### #### #### #### #### #### #### ####
|
||||||
|
@ -90,7 +167,7 @@ def pixels_to_bytes(pixels):
|
||||||
#### #### ###### #### #### #### #### #### #### ####
|
#### #### ###### #### #### #### #### #### #### ####
|
||||||
#### ## #### #### #### #### #### #### #### #### #### ##
|
#### ## #### #### #### #### #### #### #### #### #### ##
|
||||||
############## #### #### ######## ###### ########## ##############
|
############## #### #### ######## ###### ########## ##############
|
||||||
def encode(imagefilename, secretfilename):
|
def encode(imagefilename, secretfilename, bitness=1):
|
||||||
global image
|
global image
|
||||||
global pixel
|
global pixel
|
||||||
global pixel_index
|
global pixel_index
|
||||||
|
@ -98,83 +175,88 @@ def encode(imagefilename, secretfilename):
|
||||||
pixel_index = 0
|
pixel_index = 0
|
||||||
channel_index = 0
|
channel_index = 0
|
||||||
|
|
||||||
def modify_pixel(bit):
|
if bitness < 1:
|
||||||
global pixel
|
raise ValueError('Cannot modify less than 1 bit per channel')
|
||||||
global channel_index
|
if bitness > 8:
|
||||||
#print(channel_index, bit)
|
raise ValueError('Cannot modify more than 8 bits per channel')
|
||||||
#print(pixel_index, channel_index, bit)
|
|
||||||
channel = pixel[channel_index]
|
|
||||||
channel = binary(channel)[:7] + bit
|
|
||||||
channel = int(channel, 2)
|
|
||||||
pixel[channel_index] = channel
|
|
||||||
#print(pixel)
|
|
||||||
|
|
||||||
|
|
||||||
print('Hiding "%s" within "%s"' % (secretfilename, imagefilename))
|
print('Hiding "%s" within "%s"' % (secretfilename, imagefilename))
|
||||||
|
secret_size = os.path.getsize(secretfilename)
|
||||||
|
if secret_size == 0:
|
||||||
|
raise StegError('The Secret can\'t be 0 bytes.')
|
||||||
|
|
||||||
image = Image.open(imagefilename)
|
image = Image.open(imagefilename)
|
||||||
|
image_steg = BitsToImage(image, bitness)
|
||||||
|
|
||||||
totalpixels = image.size[0] * image.size[1]
|
totalpixels = image.size[0] * image.size[1]
|
||||||
if totalpixels < HEADER_SIZE:
|
if totalpixels < HEADER_SIZE:
|
||||||
raise StegError('Image cannot have fewer than %d pixels. They are used to store Secret\'s length' % HEADER_SIZE)
|
raise StegError('Image cannot have fewer than %d pixels. They are used to store Secret\'s length' % HEADER_SIZE)
|
||||||
|
|
||||||
secretfile = open(secretfilename, 'rb')
|
|
||||||
secret = secretfile.read()
|
|
||||||
secret = list(secret)
|
|
||||||
|
|
||||||
if secret == []:
|
|
||||||
raise StegError('The Secret can\'t be 0 bytes.')
|
|
||||||
|
|
||||||
secret_extension = os.path.splitext(secretfilename)[1][1:]
|
secret_extension = os.path.splitext(secretfilename)[1][1:]
|
||||||
secret_content_length = (len(secret)) + (len(secret_extension)) + 1
|
secret_content_length = (secret_size) + (len(secret_extension)) + 1
|
||||||
requiredpixels = math.ceil(((secret_content_length * 8) + 32) / 3)
|
requiredpixels = math.ceil(((secret_content_length * 8) + 32) / (3 * bitness))
|
||||||
if totalpixels < requiredpixels:
|
if totalpixels < requiredpixels:
|
||||||
raise StegError('Image does not have enough pixels to store the Secret'
|
raise StegError('Image does not have enough pixels to store the Secret'
|
||||||
'Must have at least %d pixels' % requiredpixels)
|
'Must have at least %d pixels' % requiredpixels)
|
||||||
|
|
||||||
print('%d available pixels, %d required' % (totalpixels, requiredpixels))
|
print('%d pixels available, %d required' % (totalpixels, requiredpixels))
|
||||||
|
|
||||||
# --> YOU ARE HERE <--
|
# --> YOU ARE HERE <--
|
||||||
|
|
||||||
|
# Because bitness may be between 1 and 8, we need to create a writing buffer
|
||||||
|
# called `binary_write_buffer`, so that we're always writing the same amount
|
||||||
|
# of data per color channel.
|
||||||
|
# If we were to write the secret length / extension on the fly, we might end
|
||||||
|
# up using the wrong number of bits for the final channel of some pixel.
|
||||||
|
# Example: 10010101 broken into groups of 3 is [100, 101, 01]
|
||||||
|
# Note that the last group is not the same size as the desired bitness, and
|
||||||
|
# will cause decode errors.
|
||||||
|
|
||||||
pixel = list(image.getpixel((0, 0)))
|
pixel = list(image.getpixel((0, 0)))
|
||||||
|
binary_write_buffer = ''
|
||||||
|
|
||||||
# Write secret length
|
# Write secret length
|
||||||
secret_content_length_b = binary(secret_content_length).rjust(32, '0')
|
secret_content_length_b = binary(secret_content_length).rjust(32, '0')
|
||||||
for x in range(32):
|
print('Content bytes:', secret_content_length)
|
||||||
modify_pixel(secret_content_length_b[x])
|
image_steg.write(secret_content_length_b)
|
||||||
increment_pixel()
|
|
||||||
|
|
||||||
# Write the secret extension
|
# Write the secret extension
|
||||||
for character in (secret_extension + chr(0)):
|
for character in (secret_extension + chr(0)):
|
||||||
character = ord(character)
|
image_steg.write(binary(ord(character)))
|
||||||
character = binary(character)
|
|
||||||
for bit in character:
|
|
||||||
modify_pixel(bit)
|
|
||||||
increment_pixel()
|
|
||||||
|
|
||||||
# Write the secret data
|
# Write the secret data
|
||||||
for (index, byte) in enumerate(secret):
|
bytes_written = 0
|
||||||
if index % 1024 == 0:
|
done = False
|
||||||
percentage = (index + 1) / len(secret)
|
secretfile = open(secretfilename, 'rb')
|
||||||
|
while not done:
|
||||||
|
if bytes_written % 1024 == 0:
|
||||||
|
percentage = (bytes_written + 1) / secret_size
|
||||||
percentage = '%07.3f%%\r' % (100 * percentage)
|
percentage = '%07.3f%%\r' % (100 * percentage)
|
||||||
print(percentage, end='')
|
print(percentage, end='')
|
||||||
# Convert byte integer to a binary string, and loop through characters
|
|
||||||
byte = binary(byte)
|
bytes = secretfile.read(FILE_READ_SIZE)
|
||||||
for (bindex, bit) in enumerate(byte):
|
|
||||||
modify_pixel(bit)
|
done = len(bytes) == 0
|
||||||
if not (index == secret_content_length -1 and bindex == 7):
|
|
||||||
# If your Image dimensions are at the extreme limit of the Secret size,
|
bytes = list(bytes)
|
||||||
# this would otherwise raise IndexError as it tries to grab the next pixel
|
bytes = [binary(byte) for byte in bytes]
|
||||||
# off the canvas.
|
bytes_written += len(bytes)
|
||||||
increment_pixel()
|
bytes = ''.join(bytes)
|
||||||
print('100.000%') # you know it
|
image_steg.write(bytes)
|
||||||
|
|
||||||
|
# haha
|
||||||
|
print('100.000%')
|
||||||
|
|
||||||
if channel_index != 0:
|
if channel_index != 0:
|
||||||
# The Secret data has finished, but we still have an unsaved pixel
|
# The Secret data has finished, but we still have an unsaved pixel
|
||||||
# (because channel_index is set to 0 when we save the active pixel above)
|
# (because channel_index is set to 0 when we save the active pixel above)
|
||||||
image.putpixel((pixel_index % image.size[0], pixel_index // image.size[0]), tuple(pixel))
|
(x, y) = index_to_xy(pixel_index, image.size[0])
|
||||||
|
image.putpixel((x, y), tuple(pixel))
|
||||||
|
|
||||||
newname = os.path.splitext(imagefilename)[0]
|
new_name = os.path.splitext(imagefilename)[0]
|
||||||
newname = '%s (%s).png' % (newname, os.path.basename(secretfilename).replace('.', '_'))
|
original_name = os.path.basename(secretfilename).replace('.', '_')
|
||||||
print(newname)
|
newname = '%s (%s) (%d).png' % (new_name, original_name, bitness)
|
||||||
|
print('Writing:', newname)
|
||||||
image.save(newname)
|
image.save(newname)
|
||||||
|
|
||||||
|
|
||||||
|
@ -188,49 +270,29 @@ def encode(imagefilename, secretfilename):
|
||||||
#### #### #### #### #### #### #### #### #### ####
|
#### #### #### #### #### #### #### #### #### ####
|
||||||
#### #### #### ## #### #### #### #### #### #### #### ##
|
#### #### #### ## #### #### #### #### #### #### #### ##
|
||||||
########## ############## ######## ###### ########## ##############
|
########## ############## ######## ###### ########## ##############
|
||||||
def decode(imagefilename):
|
def decode(imagefilename, bitness=1):
|
||||||
global image
|
|
||||||
global pixel
|
|
||||||
global pixel_index
|
|
||||||
global channel_index
|
|
||||||
pixel_index = 0
|
|
||||||
channel_index = 0
|
|
||||||
|
|
||||||
print('Extracting content from "%s"' % imagefilename)
|
print('Extracting content from "%s"' % imagefilename)
|
||||||
image = Image.open(imagefilename)
|
image = Image.open(imagefilename)
|
||||||
|
image_steg = ImageToBits(image, bitness)
|
||||||
|
|
||||||
# determine the content length
|
# determine the content length
|
||||||
content_length = ''
|
content_length = image_steg.read(32)
|
||||||
for x in range(11):
|
|
||||||
pixel = list(image.getpixel( (pixel_index % image.size[0], pixel_index // image.size[0]) ))
|
|
||||||
pixel = pixel[:3]
|
|
||||||
#print(pixel)
|
|
||||||
content_length += ''.join([bin(i)[-1] for i in pixel])
|
|
||||||
pixel_index += 1
|
|
||||||
content_length = content_length[:-1]
|
|
||||||
content_length = int(content_length, 2)
|
content_length = int(content_length, 2)
|
||||||
print('Content bytes:', content_length)
|
print('Content bytes:', content_length)
|
||||||
|
|
||||||
# Continue from the end of the header
|
|
||||||
# This would have been automatic if I used `increment_pixel`
|
|
||||||
pixel_index = 10
|
|
||||||
channel_index = 2
|
|
||||||
|
|
||||||
# determine secret extension
|
# determine secret extension
|
||||||
extension = ''
|
extension = ''
|
||||||
while extension[-8:] != '00000000' or len(extension) % 8 != 0:
|
while extension[-8:] != '00000000' or len(extension) % 8 != 0:
|
||||||
channel = pixel[channel_index]
|
extension += image_steg.read()
|
||||||
channel = binary(channel)
|
|
||||||
channel = channel[-1]
|
extension = chunk_iterable(extension, 8)
|
||||||
extension += channel
|
extension.remove('00000000')
|
||||||
increment_pixel(save=False)
|
|
||||||
extension = extension[:-8]
|
|
||||||
extension = [extension[8*x: (8*x)+8] for x in range(len(extension)//8)]
|
|
||||||
extension = [chr(int(x, 2)) for x in extension]
|
extension = [chr(int(x, 2)) for x in extension]
|
||||||
extension = ''.join(extension)
|
extension = ''.join(extension)
|
||||||
print('Extension:', extension)
|
print('Extension:', extension)
|
||||||
|
|
||||||
# Remove the extension length
|
# Remove the extension length, and null byte
|
||||||
content_length -= 1
|
content_length -= 1
|
||||||
content_length -= len(extension)
|
content_length -= len(extension)
|
||||||
|
|
||||||
|
@ -242,38 +304,43 @@ def decode(imagefilename):
|
||||||
outfile = open(newname, 'wb')
|
outfile = open(newname, 'wb')
|
||||||
|
|
||||||
# extract data
|
# extract data
|
||||||
for byte in range(content_length):
|
bytes_written = 0
|
||||||
if byte % 1024 == 0:
|
while bytes_written < content_length:
|
||||||
percentage = (byte + 1) / content_length
|
if bytes_written % 1024 == 0:
|
||||||
|
percentage = (bytes_written + 1) / content_length
|
||||||
percentage = '%07.3f%%\r' % (100 * percentage)
|
percentage = '%07.3f%%\r' % (100 * percentage)
|
||||||
print(percentage, end='')
|
print(percentage, end='')
|
||||||
|
|
||||||
activebyte = ''
|
byte = image_steg.read(8)
|
||||||
for bit in range(8):
|
byte = '%02x' % int(byte, 2)
|
||||||
channel = pixel[channel_index]
|
outfile.write(binascii.a2b_hex(byte))
|
||||||
channel = binary(channel)[-1]
|
bytes_written += 1
|
||||||
activebyte += channel
|
|
||||||
if not (byte == content_length - 1 and bit == 7):
|
# I'm on fire
|
||||||
# If your Image dimensions are at the extreme limit of the Secret size,
|
print('100.000%')
|
||||||
# this would otherwise raise IndexError as it tries to grab the next pixel
|
print('Wrote', newname)
|
||||||
# off the canvas.
|
|
||||||
increment_pixel(save=False)
|
|
||||||
activebyte = '%02x' % int(activebyte, 2)
|
|
||||||
outfile.write(binascii.a2b_hex(activebyte))
|
|
||||||
print('100.000%') # I'm on fire
|
|
||||||
print(newname)
|
|
||||||
outfile.close()
|
outfile.close()
|
||||||
|
|
||||||
|
def listget(li, index, fallback=None):
|
||||||
|
try:
|
||||||
|
return li[index]
|
||||||
|
except IndexError:
|
||||||
|
return fallback
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
if (len(sys.argv) == 1) or (sys.argv[1] not in ['encode', 'decode']):
|
command = listget(sys.argv, 1, '').lower()
|
||||||
|
if command not in ['encode', 'decode']:
|
||||||
print('Usage:')
|
print('Usage:')
|
||||||
print('> 3bitspixel.py encode imagefilename.png secretfilename.ext')
|
print('> steganographic.py encode imagefilename.png secretfilename.ext')
|
||||||
print('> 3bitspixel.py decode lacedimagename.png')
|
print('> steganographic.py decode lacedimagename.png')
|
||||||
|
quit()
|
||||||
|
|
||||||
imagefilename = sys.argv[2]
|
imagefilename = sys.argv[2]
|
||||||
|
|
||||||
if sys.argv[1] == 'encode':
|
if command == 'encode':
|
||||||
secretfilename = sys.argv[3]
|
secretfilename = sys.argv[3]
|
||||||
encode(imagefilename, secretfilename)
|
bitness = int(listget(sys.argv, 4, 1))
|
||||||
|
encode(imagefilename, secretfilename, bitness)
|
||||||
else:
|
else:
|
||||||
decode(imagefilename)
|
bitness = int(listget(sys.argv, 3, 1))
|
||||||
|
decode(imagefilename, bitness)
|
|
@ -1,4 +0,0 @@
|
||||||
TKCube
|
|
||||||
=========
|
|
||||||
|
|
||||||
Not done yet.
|
|
117
TKCube/tkcube.py
|
@ -1,117 +0,0 @@
|
||||||
import copy
|
|
||||||
import math
|
|
||||||
import random
|
|
||||||
import tkinter
|
|
||||||
|
|
||||||
class TKCube:
|
|
||||||
def __init__(self):
|
|
||||||
self.t = tkinter.Tk()
|
|
||||||
self.FACES = [
|
|
||||||
[[2, 2, 1], [2, -2, 1], [-2, -2, 1], [-2, 2, 1]],
|
|
||||||
[[2, -2, 1], [-2, -2, -1], [-2, 2, -1], [2, 2, -1]],
|
|
||||||
[[-2, -2, 1], [2, -2, 1], [2, -2, -1], [-2, -2, -1]],
|
|
||||||
[[-2, 2, 1], [2, 2, 1], [2, 2, -1], [-2, 2, -1]],
|
|
||||||
[[-2, -2, -1], [-2, 2, -1], [-2, 2, 1], [-2, -2, -1]],
|
|
||||||
[[2, -2, 1], [2, 2, 1], [2, 2, -1], [2, -2, -1]],
|
|
||||||
]
|
|
||||||
self.INFLATE_SCALE = 8
|
|
||||||
|
|
||||||
self.c = tkinter.Canvas(self.t, width=600, height=600, bg='#444')
|
|
||||||
self.c.pack(fill='both', expand=True)
|
|
||||||
self.t.bind('<Return>', self.render)
|
|
||||||
self.is_mouse_down = False
|
|
||||||
self.prev_mouse_x = None
|
|
||||||
self.prev_mouse_y = None
|
|
||||||
self.t.bind('<ButtonPress-1>', self.mouse_down)
|
|
||||||
self.t.bind('<ButtonRelease-1>', self.mouse_up)
|
|
||||||
self.t.bind('<Motion>', self.mouse_motion)
|
|
||||||
self.t.bind('<Up>', lambda event: self.arbitrarymove(0, -1))
|
|
||||||
self.t.bind('<Down>', lambda event: self.arbitrarymove(0, 1))
|
|
||||||
self.t.bind('<Left>', lambda event: self.arbitrarymove(-1, 0))
|
|
||||||
self.t.bind('<Right>', lambda event: self.arbitrarymove(1, 0))
|
|
||||||
self.render()
|
|
||||||
self.t.mainloop()
|
|
||||||
|
|
||||||
def arbitrarymove(self, deltax, deltay):
|
|
||||||
for face in self.FACES:
|
|
||||||
for point in face:
|
|
||||||
point[0] += deltax
|
|
||||||
point[1] += deltay
|
|
||||||
self.render()
|
|
||||||
|
|
||||||
def mouse_down(self, event):
|
|
||||||
self.is_mouse_down = True
|
|
||||||
|
|
||||||
def mouse_up(self, event):
|
|
||||||
self.is_mouse_down = False
|
|
||||||
|
|
||||||
def mouse_motion(self, event):
|
|
||||||
if not self.is_mouse_down:
|
|
||||||
return
|
|
||||||
if self.prev_mouse_x is None:
|
|
||||||
self.prev_mouse_x = event.x
|
|
||||||
self.prev_mouse_y = event.y
|
|
||||||
distance = math.sqrt( ((event.x - self.prev_mouse_x) ** 2) + ((event.y - self.prev_mouse_y) ** 2) )
|
|
||||||
self.prev_mouse_x = event.x
|
|
||||||
self.prev_mouse_y = event.y
|
|
||||||
print(distance)
|
|
||||||
|
|
||||||
def center_of_square(self, face):
|
|
||||||
x = 0; y = 0; z = 0
|
|
||||||
for point in face:
|
|
||||||
x += point[0]
|
|
||||||
y += point[1]
|
|
||||||
z += point[2]
|
|
||||||
return [x/4, y/4, z/4]
|
|
||||||
|
|
||||||
def plot_point_screen(self, x, y, diameter=4):
|
|
||||||
radius = diameter / 2
|
|
||||||
x1 = x - radius
|
|
||||||
y1 = y - radius
|
|
||||||
x2 = x + radius
|
|
||||||
y2 = y + radius
|
|
||||||
self.c.create_oval(x1, y1, x2, y2, fill='#000')
|
|
||||||
|
|
||||||
def render(self, *event):
|
|
||||||
self.c.delete('all')
|
|
||||||
rendered_faces = copy.deepcopy(self.FACES)
|
|
||||||
|
|
||||||
# Sort by depth from camera
|
|
||||||
# The sort key is the z value of the coordinate
|
|
||||||
# in the center of the face
|
|
||||||
rendered_faces.sort(key=lambda face: self.center_of_square(face)[2])
|
|
||||||
|
|
||||||
canvas_width_half = self.c.winfo_width() / 2
|
|
||||||
canvas_height_half = self.c.winfo_height() / 2
|
|
||||||
highest_z = max([max([point[2] for point in face]) for face in rendered_faces])
|
|
||||||
for face in self.FACES:
|
|
||||||
for point in face:
|
|
||||||
x = point[0]
|
|
||||||
y = point[1]
|
|
||||||
z = point[2]
|
|
||||||
|
|
||||||
# Push everything away from the camera so all z are <= 0
|
|
||||||
z -= highest_z
|
|
||||||
|
|
||||||
# Create vanishing point.
|
|
||||||
distance_camera = math.sqrt((x**2) + (y**2) + (z**2))
|
|
||||||
#if z != 0:
|
|
||||||
# factor = (abs(z) ** 0.2) - 1
|
|
||||||
# print(factor)
|
|
||||||
#else:
|
|
||||||
# factor = 0
|
|
||||||
x += x * factor
|
|
||||||
y += y * factor
|
|
||||||
|
|
||||||
# Inflate for display
|
|
||||||
x *= self.INFLATE_SCALE
|
|
||||||
y *= self.INFLATE_SCALE
|
|
||||||
z *= self.INFLATE_SCALE
|
|
||||||
|
|
||||||
# Shift the coordinates into the screen
|
|
||||||
x += canvas_width_half
|
|
||||||
y += canvas_height_half
|
|
||||||
self.plot_point_screen(x, y)
|
|
||||||
|
|
||||||
#print(rendered_faces)
|
|
||||||
t = TKCube()
|
|
BIN
Toddo/toddo.db
115
Toddo/toddo.py
|
@ -4,6 +4,13 @@ import sqlite3
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
|
try:
|
||||||
|
import colorama
|
||||||
|
colorama.init()
|
||||||
|
HAS_COLORAMA = True
|
||||||
|
except:
|
||||||
|
HAS_COLORAMA = False
|
||||||
|
|
||||||
SQL_ID = 0
|
SQL_ID = 0
|
||||||
SQL_TODOTABLE = 1
|
SQL_TODOTABLE = 1
|
||||||
SQL_CREATED = 2
|
SQL_CREATED = 2
|
||||||
|
@ -28,6 +35,7 @@ Use `toddo all` to see if there are entries for other tables.'''
|
||||||
|
|
||||||
HELP_REMOVE = '''Provide an ID number to remove.'''
|
HELP_REMOVE = '''Provide an ID number to remove.'''
|
||||||
|
|
||||||
|
# The newline at the top of this message is intentional
|
||||||
DISPLAY_INDIVIDUAL = '''
|
DISPLAY_INDIVIDUAL = '''
|
||||||
ID: _id_
|
ID: _id_
|
||||||
Table: _table_
|
Table: _table_
|
||||||
|
@ -58,6 +66,24 @@ class Toddo():
|
||||||
self._cur.execute('CREATE INDEX IF NOT EXISTS todoindex on todos(id)')
|
self._cur.execute('CREATE INDEX IF NOT EXISTS todoindex on todos(id)')
|
||||||
return self._cur
|
return self._cur
|
||||||
|
|
||||||
|
def _install_default_lastid(self):
|
||||||
|
self.cur.execute('SELECT val FROM meta WHERE key == "lastid"')
|
||||||
|
f = cur.fetchone()
|
||||||
|
if f is not None:
|
||||||
|
return int(f[0])
|
||||||
|
self.cur.execute('INSERT INTO meta VALUES("lastid", 1)')
|
||||||
|
self.sql.commit()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def _install_default_todotable(self):
|
||||||
|
self.cur.execute('SELECT val FROM meta WHERE key == "todotable"')
|
||||||
|
f = cur.fetchone()
|
||||||
|
if f is not None:
|
||||||
|
return f[0]
|
||||||
|
self.cur.execute('INSERT INTO meta VALUES("todotable", "default")')
|
||||||
|
self.sql.commit()
|
||||||
|
return 'default'
|
||||||
|
|
||||||
def add_todo(self, message=None):
|
def add_todo(self, message=None):
|
||||||
'''
|
'''
|
||||||
Create new entry in the database on the active todotable.
|
Create new entry in the database on the active todotable.
|
||||||
|
@ -113,7 +139,6 @@ class Toddo():
|
||||||
output = output.replace('_human_', human(todo[SQL_CREATED]))
|
output = output.replace('_human_', human(todo[SQL_CREATED]))
|
||||||
output = output.replace('_message_', message)
|
output = output.replace('_message_', message)
|
||||||
|
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def display_active_todos(self):
|
def display_active_todos(self):
|
||||||
|
@ -154,14 +179,39 @@ class Toddo():
|
||||||
if '\n' in message:
|
if '\n' in message:
|
||||||
message = message.split('\n')[0] + ' ...'
|
message = message.split('\n')[0] + ' ...'
|
||||||
|
|
||||||
total = '%s : %s : %s : %s' % (todoid, todotable, timestamp, message)
|
|
||||||
terminal_width = shutil.get_terminal_size()[0]
|
terminal_width = shutil.get_terminal_size()[0]
|
||||||
|
total = '%s : %s : %s' % (timestamp, todoid, message)
|
||||||
|
space_remaining = terminal_width - len(total)
|
||||||
if len(total) > terminal_width:
|
if len(total) > terminal_width:
|
||||||
total = total[:(terminal_width-(len(total)+4))] + '...'
|
total = total[:(terminal_width-(len(total)+4))] + '...'
|
||||||
display.append(total)
|
display.append(total)
|
||||||
|
|
||||||
return '\n'.join(display)
|
return '\n'.join(display)
|
||||||
|
|
||||||
|
def get_todotable(self):
|
||||||
|
self.cur.execute('SELECT val FROM meta WHERE key="todotable"')
|
||||||
|
todotable = self.cur.fetchone()
|
||||||
|
if todotable is None:
|
||||||
|
self._install_default_todotable()
|
||||||
|
todotable = 'default'
|
||||||
|
else:
|
||||||
|
todotable = todotable[0]
|
||||||
|
return todotable
|
||||||
|
|
||||||
|
def increment_lastid(self, increment=False):
|
||||||
|
'''
|
||||||
|
Increment the lastid in the meta table, THEN return it.
|
||||||
|
'''
|
||||||
|
self.cur.execute('SELECT val FROM meta WHERE key="lastid"')
|
||||||
|
lastid = self.cur.fetchone()
|
||||||
|
if lastid is None:
|
||||||
|
self._install_default_lastid()
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
lastid = int(lastid[0]) + 1
|
||||||
|
self.cur.execute('UPDATE meta SET val=? WHERE key="lastid"', [lastid])
|
||||||
|
return lastid
|
||||||
|
|
||||||
def switch_todotable(self, newtable=None):
|
def switch_todotable(self, newtable=None):
|
||||||
'''
|
'''
|
||||||
Update the meta with `newtable` as the new active todotable.
|
Update the meta with `newtable` as the new active todotable.
|
||||||
|
@ -178,47 +228,24 @@ class Toddo():
|
||||||
self.sql.commit()
|
self.sql.commit()
|
||||||
return newtable
|
return newtable
|
||||||
|
|
||||||
def increment_lastid(self, increment=False):
|
def colorama_print(text):
|
||||||
'''
|
alternator = False
|
||||||
Increment the lastid in the meta table, THEN return it.
|
terminal_size = shutil.get_terminal_size()[0]
|
||||||
'''
|
for line in text.split('\n'):
|
||||||
self.cur.execute('SELECT val FROM meta WHERE key="lastid"')
|
line += ' ' * (terminal_size - (len(line)+1))
|
||||||
lastid = self.cur.fetchone()
|
if HAS_COLORAMA:
|
||||||
if lastid is None:
|
if alternator:
|
||||||
self._install_default_lastid()
|
sys.stdout.write(colorama.Fore.BLACK)
|
||||||
return 1
|
sys.stdout.write(colorama.Back.WHITE)
|
||||||
else:
|
else:
|
||||||
lastid = int(lastid[0]) + 1
|
sys.stdout.write(colorama.Fore.WHITE)
|
||||||
self.cur.execute('UPDATE meta SET val=? WHERE key="lastid"', [lastid])
|
sys.stdout.write(colorama.Back.BLACK)
|
||||||
return lastid
|
alternator = not alternator
|
||||||
|
print(line)
|
||||||
def get_todotable(self):
|
if HAS_COLORAMA:
|
||||||
self.cur.execute('SELECT val FROM meta WHERE key="todotable"')
|
sys.stdout.write(colorama.Back.RESET)
|
||||||
todotable = self.cur.fetchone()
|
sys.stdout.write(colorama.Fore.RESET)
|
||||||
if todotable is None:
|
sys.stdout.flush()
|
||||||
self._install_default_todotable()
|
|
||||||
todotable = 'default'
|
|
||||||
else:
|
|
||||||
todotable = todotable[0]
|
|
||||||
return todotable
|
|
||||||
|
|
||||||
def _install_default_lastid(self):
|
|
||||||
'''
|
|
||||||
This method assumes that "lastid" does not already exist.
|
|
||||||
If it does, it's your fault for calling this.
|
|
||||||
'''
|
|
||||||
self.cur.execute('INSERT INTO meta VALUES("lastid", 1)')
|
|
||||||
self.sql.commit()
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def _install_default_todotable(self):
|
|
||||||
'''
|
|
||||||
This method assumes that "todotable" does not already exist.
|
|
||||||
If it does, it's your fault for calling this.
|
|
||||||
'''
|
|
||||||
self.cur.execute('INSERT INTO meta VALUES("todotable", "default")')
|
|
||||||
self.sql.commit()
|
|
||||||
return 'default'
|
|
||||||
|
|
||||||
def human(timestamp):
|
def human(timestamp):
|
||||||
timestamp = datetime.datetime.utcfromtimestamp(timestamp)
|
timestamp = datetime.datetime.utcfromtimestamp(timestamp)
|
||||||
|
@ -282,14 +309,14 @@ if __name__ == '__main__':
|
||||||
table = toddo.get_todotable()
|
table = toddo.get_todotable()
|
||||||
print(HELP_NOACTIVE % table)
|
print(HELP_NOACTIVE % table)
|
||||||
else:
|
else:
|
||||||
print(message)
|
colorama_print(message)
|
||||||
|
|
||||||
elif sys.argv[1] == 'all':
|
elif sys.argv[1] == 'all':
|
||||||
message = toddo.display_todos_from_table(None)
|
message = toddo.display_todos_from_table(None)
|
||||||
if message is None:
|
if message is None:
|
||||||
print(HELP_NOENTRIES)
|
print(HELP_NOENTRIES)
|
||||||
else:
|
else:
|
||||||
print(message)
|
colorama_print(message)
|
||||||
|
|
||||||
elif sys.argv[1] == 'add':
|
elif sys.argv[1] == 'add':
|
||||||
args = list(filter(None, sys.argv))
|
args = list(filter(None, sys.argv))
|
||||||
|
|
|
@ -4,22 +4,23 @@ name randomly scrambled into 12 digits. The others will increment that number b
|
||||||
1.
|
1.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
print('hi')
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
argv = sys.argv[1:]
|
argv = sys.argv[1:]
|
||||||
print(argv)
|
print(''.join(c for c in argv if c in string.printable))
|
||||||
|
|
||||||
randname = [random.choice(string.digits) for x in range(12)]
|
randname = [random.choice(string.digits) for x in range(12)]
|
||||||
randname = int(''.join(randname))
|
randname = int(''.join(randname))
|
||||||
for originalname in argv:
|
for filepath in argv:
|
||||||
folder = os.path.dirname(originalname)
|
folder = os.path.dirname(filepath)
|
||||||
basename = os.path.basename(originalname)
|
basename = os.path.basename(filepath)
|
||||||
extension = basename.split('.')[-1]
|
extension = os.path.splitext(basename)[1]
|
||||||
newname = randname
|
newname = str(randname).rjust(12, '0')
|
||||||
randname += 1
|
randname += 1
|
||||||
newname = '%s/%d.%s' % (folder, newname, extension)
|
newname = '%s\\%s%s' % (folder, newname, extension)
|
||||||
print('%s -> %s' % (originalname, newname))
|
os.rename(filepath, newname)
|
||||||
os.rename(originalname, newname)
|
print('%s -> %s' % (filepath, newname))
|
|
@ -10,13 +10,13 @@ import string
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
argv = sys.argv[1:]
|
argv = sys.argv[1:]
|
||||||
print(argv)
|
print(''.join(c for c in argv if c in string.printable))
|
||||||
for originalname in argv:
|
for filepath in argv:
|
||||||
folder = os.path.dirname(originalname)
|
folder = os.path.dirname(filepath)
|
||||||
basename = os.path.basename(originalname)
|
basename = os.path.basename(filepath)
|
||||||
extension = basename.split('.')[-1]
|
extension = os.path.splitext(basename)[1]
|
||||||
newname = [random.choice(string.ascii_letters) for x in range(16)]
|
newname = [random.choice(string.ascii_letters) for x in range(16)]
|
||||||
newname = ''.join(newname)
|
newname = ''.join(newname)
|
||||||
newname = '%s/%s.%s' % (folder, newname, extension)
|
newname = '%s\\%s%s' % (folder, newname, extension)
|
||||||
print('%s -> %s' % (originalname, newname))
|
os.rename(filepath, newname)
|
||||||
os.rename(originalname, newname)
|
print('%s -> %s' % (filepath, newname))
|
|
@ -9,13 +9,13 @@ import string
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
argv = sys.argv[1:]
|
argv = sys.argv[1:]
|
||||||
print(argv)
|
print(''.join(c for c in argv if c in string.printable))
|
||||||
for originalname in argv:
|
for filepath in argv:
|
||||||
folder = os.path.dirname(originalname)
|
folder = os.path.dirname(filepath)
|
||||||
basename = os.path.basename(originalname)
|
basename = os.path.basename(filepath)
|
||||||
extension = basename.split('.')[-1]
|
extension = os.path.splitext(basename)[1]
|
||||||
newname = [random.choice(string.digits) for x in range(12)]
|
newname = [random.choice(string.digits) for x in range(12)]
|
||||||
newname = ''.join(newname)
|
newname = ''.join(newname)
|
||||||
newname = '%s/%s.%s' % (folder, newname, extension)
|
newname = '%s\\%s%s' % (folder, newname, extension)
|
||||||
print('%s -> %s' % (originalname, newname))
|
os.rename(filepath, newname)
|
||||||
os.rename(originalname, newname)
|
print('%s -> %s' % (filepath, newname))
|
|
@ -31,7 +31,6 @@ zeropadding = max(2, zeropadding)
|
||||||
zeropadding = str(zeropadding)
|
zeropadding = str(zeropadding)
|
||||||
|
|
||||||
format = '%s%0{pad}d%s'.format(pad=zeropadding)
|
format = '%s%0{pad}d%s'.format(pad=zeropadding)
|
||||||
print(format)
|
|
||||||
|
|
||||||
def natural_sort(l):
|
def natural_sort(l):
|
||||||
'''
|
'''
|
||||||
|
@ -55,5 +54,6 @@ for (fileindex, filename) in enumerate(files):
|
||||||
else:
|
else:
|
||||||
extension = ''
|
extension = ''
|
||||||
newname = format % (prefix, fileindex, extension)
|
newname = format % (prefix, fileindex, extension)
|
||||||
print(''.join([c for c in filename if c in string.printable]), '->', newname)
|
if os.path.basename(filename) != newname:
|
||||||
|
print(''.join([c for c in (filename + ' -> ' + newname) if c in string.printable]))
|
||||||
os.rename(filename, newname)
|
os.rename(filename, newname)
|
25
Toolbox/timestampfilename.pyw
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
'''
|
||||||
|
Drag a file on top of this .py file, and it will
|
||||||
|
be renamed to the current timestamp.
|
||||||
|
'''
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
STRFTIME = '%Y%m%d %H%M%S'
|
||||||
|
UTC = True
|
||||||
|
|
||||||
|
filename = sys.argv[1]
|
||||||
|
folder = os.path.dirname(filename)
|
||||||
|
if folder == '':
|
||||||
|
folder = os.getcwd()
|
||||||
|
basename = os.path.basename(filename)
|
||||||
|
extension = os.path.splitext(basename)[1]
|
||||||
|
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc if UTC else None)
|
||||||
|
newname = now.strftime(STRFTIME)
|
||||||
|
|
||||||
|
newname = '%s\\%s%s' % (folder, newname, extension)
|
||||||
|
print(filename, '-->', newname)
|
||||||
|
os.rename(filename, newname)
|