Skip to content

Commit 86c2d8c

Browse files
committed
Init
1 parent adbfbbf commit 86c2d8c

File tree

6 files changed

+383
-0
lines changed

6 files changed

+383
-0
lines changed

setup.py

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import setuptools
2+
3+
with open("README.md", "r") as fh:
4+
long_description = fh.read()
5+
6+
setuptools.setup(
7+
name="ThumbnailFlow", # Replace with your own username
8+
version="0.1.0",
9+
author="Jens Hinghaus",
10+
author_email="[email protected]",
11+
description="Generator for thumbnails",
12+
long_description=long_description,
13+
long_description_content_type="text/markdown",
14+
url=None, #TODO GitHub-Link
15+
keywords="thumbnails",
16+
packages=setuptools.find_packages(exclude=['tests']),
17+
classifiers=[
18+
"Development Status :: 3 - Alpha"
19+
"Programming Language :: Python :: 3",
20+
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
21+
"Operating System :: POSIX :: Linux",
22+
"Topic :: Multimedia :: Graphics",
23+
],
24+
install_requires=['Pillow'],
25+
python_requires='>=3.6',
26+
)

tests/context.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import os
2+
import sys
3+
sys.path.insert(0, os.path.abspath('..'))
4+
import thumbnailflow.thumbnails

tests/test_thumbs.py

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
from context import thumbnailflow
2+
3+
import unittest
4+
import tempfile
5+
import os
6+
import json
7+
from PIL import Image
8+
import time
9+
10+
11+
class ThumbsTC(unittest.TestCase):
12+
13+
def setUp(self):
14+
self.setup_time = time.time()
15+
self.dir = tempfile.TemporaryDirectory()
16+
self.thumbfile_path = os.path.join(
17+
self.dir.name,
18+
thumbnailflow.thumbnails.DEFAULT_FILENAME)
19+
# create some content
20+
self.example_dir = os.path.join(self.dir.name, 'exampledir')
21+
os.mkdir(self.example_dir)
22+
self.example_txt = os.path.join(self.dir.name, 'example.txt')
23+
with open(self.example_txt, 'w') as fp:
24+
fp.write('example')
25+
self.example_png = os.path.join(self.dir.name, 'example.png')
26+
png = Image.new('RGB', (100,100))
27+
png.save(self.example_png, 'PNG')
28+
29+
def tearDown(self):
30+
self.dir.cleanup()
31+
32+
def test_generate_dir(self):
33+
t_object = thumbnailflow.thumbnails.Thumbnail(root=self.dir.name, name='exampledir')
34+
py_dict = t_object.getDict()
35+
self.assertEqual(py_dict['name'], 'exampledir')
36+
self.assertEqual(py_dict['type'], 'dir')
37+
38+
def test_generate_txt(self):
39+
t_object = thumbnailflow.thumbnails.Thumbnail(root=self.dir.name, name='example.txt')
40+
py_dict = t_object.getDict()
41+
self.assertEqual(py_dict['name'], 'example.txt')
42+
self.assertEqual(py_dict['type'], 'txt')
43+
self.assertLess(py_dict['touched'], time.time(), 'Touched in future')
44+
self.assertLess(self.setup_time, time.time(), 'Setup in future')
45+
46+
def test_generate_png(self):
47+
t_object = thumbnailflow.thumbnails.Thumbnail(root=self.dir.name, name='example.png')
48+
py_dict = t_object.getDict()
49+
self.assertEqual(py_dict['name'], 'example.png')
50+
self.assertEqual(py_dict['type'], 'png')
51+
self.assertGreater(len(py_dict.get('data_url', '')), 1)
52+
self.assertNotIn('\n', py_dict['data_url'], 'There is a newline')
53+
54+
def test_get_dirs(self):
55+
names = []
56+
for thumb in thumbnailflow.thumbnails.generateDirThumbs(self.dir.name):
57+
py_dict = json.loads(thumb)
58+
names.append(py_dict['name'])
59+
self.assertEqual(len(names), 1)
60+
self.assertIn('exampledir', names)
61+
62+
def test_get_files_without_preserve(self):
63+
names = []
64+
for thumb in thumbnailflow.thumbnails.generateFileThumbs(self.dir.name):
65+
py_dict = json.loads(thumb)
66+
names.append(py_dict['name'])
67+
self.assertEqual(len(names), 2)
68+
self.assertIn('example.txt', names)
69+
self.assertIn('example.png', names)
70+
if os.path.isfile(self.thumbfile_path):
71+
self.assertTrue(False, 'Preserved thumbs')
72+
73+
def test_get_files_with_preserve(self):
74+
names = []
75+
for thumb in thumbnailflow.thumbnails.generateFileThumbs(self.dir.name, True):
76+
py_dict = json.loads(thumb)
77+
names.append(py_dict['name'])
78+
self.assertEqual(len(names), 2)
79+
self.assertIn('example.txt', names)
80+
self.assertIn('example.png', names)
81+
if not(os.path.isfile(self.thumbfile_path)):
82+
self.assertTrue(False, 'No thumbs file generated')
83+
with open(self.thumbfile_path,'r') as fp:
84+
try:
85+
json.load(fp)
86+
except Exception as err:
87+
lines = ''.join(fp.readlines())
88+
self.assertTrue(False, 'json error {} file is {}'.format(err, lines))
89+
90+
91+
def test_usage_of_perserved(self):
92+
thumbs = [t for t in thumbnailflow.thumbnails.generateFileThumbs(self.dir.name, True)]
93+
first_creation = os.path.getmtime(self.thumbfile_path)
94+
#time.sleep(0.1)
95+
thumbs = [t for t in thumbnailflow.thumbnails.generateFileThumbs(self.dir.name, True)]
96+
if not(os.path.isfile(self.thumbfile_path)):
97+
self.assertTrue(False, 'No thumbs file generated')
98+
thumbs_created = os.path.getmtime(self.thumbfile_path)
99+
self.assertEqual(thumbs_created,
100+
first_creation,
101+
'Created too often')
102+
# let some time pass
103+
time.sleep(0.1)
104+
# set the timestamp
105+
os.utime(self.example_png, None)
106+
thumbs = {}
107+
for thumb in thumbnailflow.thumbnails.generateFileThumbs(self.dir.name, True):
108+
py_dict = json.loads(thumb)
109+
thumbs[py_dict['name']] = py_dict
110+
png_touched = max(os.path.getmtime(self.thumbfile_path),
111+
os.path.getctime(self.thumbfile_path))
112+
self.assertGreater(png_touched,
113+
first_creation,
114+
'Touch did not work')
115+
self.assertGreater(thumbs['example.png']['touched'],
116+
first_creation,
117+
'Not updated')
118+
119+
self.assertEqual(self.count_thumbfiles(), 1, 'Temp file not deleted')
120+
121+
def test_remove_tempfile(self):
122+
thumbs = [t for t in thumbnailflow.thumbnails.generateFileThumbs(self.dir.name, True)]
123+
time.sleep(0.1)
124+
# set the timestamp
125+
os.utime(self.example_png, None)
126+
os.utime(self.example_txt, None)
127+
for t in thumbnailflow.thumbnails.generateFileThumbs(self.dir.name, True):
128+
break
129+
self.assertEqual(self.count_thumbfiles(), 1, 'Temp file not deleted')
130+
131+
def count_thumbfiles(self):
132+
thumb_files_found = 0
133+
for root, dirs, files in os.walk(self.dir.name):
134+
for f in files:
135+
if f.startswith(thumbnailflow.thumbnails.DEFAULT_FILENAME):
136+
thumb_files_found += 1
137+
break
138+
return thumb_files_found

thumbnailflow/__init__.py

Whitespace-only changes.

thumbnailflow/devnull.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/python3
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright (C) 2020 Jens Hinghaus <[email protected]>
5+
#
6+
# This program is free software; you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation; either version 2 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program; if not, write to the Free Software
18+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19+
#
20+
21+
22+
class DevNull(object):
23+
''' Writer dummy not writing anything '''
24+
25+
def write(self, var):
26+
pass
27+
28+
def close(self):
29+
pass
30+
31+
def seek(self, a, b):
32+
pass
33+
34+
def tell(self):
35+
return 4

thumbnailflow/thumbnails.py

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#!/usr/bin/python3
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright (C) 2020 Jens Hinghaus <[email protected]>
5+
#
6+
# This program is free software; you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation; either version 2 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# This program is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program; if not, write to the Free Software
18+
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19+
#
20+
21+
THUMB_SIZE = 64, 64
22+
IMAGE_TYPES = ['jpg','jpeg','bmp','gif','png']
23+
DEFAULT_FILENAME = '.thumbs.json'
24+
25+
import os
26+
import os.path
27+
from PIL import Image
28+
from io import BytesIO
29+
import base64
30+
import json
31+
import uuid
32+
import thumbnailflow.devnull
33+
34+
35+
def generateDirThumbs(folder):
36+
'''
37+
Generator returning thumbsnails for folders in the given folder in json.
38+
No preserve flag implemented so farself.
39+
Preview of contained files could be implemented later.
40+
'''
41+
if not(os.path.isdir(folder)):
42+
return
43+
dir_thumbs = makeDirThumbs(folder=folder)
44+
for thumbnail in dir_thumbs:
45+
thumb_dict = thumbnail.getDict()
46+
thumb_json = json.dumps(thumb_dict)
47+
yield thumb_json
48+
49+
def generateFileThumbs(folder, preserve=False):
50+
'''
51+
Generator returning thumbsnails for files in the given folder in json.
52+
Checks for an existing file with generated thumbnails.
53+
'''
54+
55+
if not(os.path.isdir(folder)):
56+
return
57+
# check for a thumbnail-file
58+
old_thumbs_file = os.path.join(folder,DEFAULT_FILENAME)
59+
60+
file_thumbs = makeFileThumbs(folder=folder)
61+
known_iterator = generateKnownThumbs(old_thumbs_file)
62+
# start the iterator before we open the file to write
63+
current_known = next(known_iterator)
64+
if preserve:
65+
new_thumbs_file = old_thumbs_file + str(uuid.uuid4())
66+
fp = open(new_thumbs_file,'w', encoding='utf-8')
67+
fp.write('[\n')
68+
else:
69+
fp = thumbnailflow.devnull.DevNull()
70+
dirty = False
71+
72+
try:
73+
for thumbnail in file_thumbs:
74+
if ((current_known['name'] == thumbnail.name) and
75+
(current_known['touched'] == thumbnail.touched)):
76+
thumb_dict = current_known
77+
current_known = next(known_iterator)
78+
else:
79+
thumb_dict = thumbnail.getDict()
80+
dirty = True
81+
thumb_json = json.dumps(thumb_dict)
82+
fp.write(thumb_json)
83+
fp.write(',\n')
84+
yield thumb_json
85+
except GeneratorExit:
86+
# the new file can't be complete: Do not write.
87+
dirty = False
88+
if preserve:
89+
if dirty:
90+
fp.seek(fp.tell() - 2, os.SEEK_SET)
91+
fp.write(']')
92+
fp.close()
93+
os.replace(new_thumbs_file, old_thumbs_file)
94+
else:
95+
fp.seek(0)
96+
fp.truncate()
97+
fp.close()
98+
os.remove(new_thumbs_file)
99+
100+
def makeFileThumbs(folder):
101+
''' Returns a list of Thumbnail objects
102+
for the files in the given folder.
103+
The files are sorted by date desc '''
104+
105+
file_thumbs = []
106+
for root, dirs, files in os.walk(folder):
107+
filepaths = []
108+
for filename in files:
109+
if filename == DEFAULT_FILENAME:
110+
continue
111+
f = Thumbnail(root=root, name=filename)
112+
file_thumbs.append(f)
113+
break # only this folder
114+
return sorted(file_thumbs,
115+
key=Thumbnail.keyTouched,
116+
reverse=True)
117+
118+
def makeDirThumbs(folder):
119+
''' Returns a list of Thumbnail objects
120+
for the folders in the given folder.'''
121+
122+
dir_thumbs = []
123+
for root, dirs, files in os.walk(folder):
124+
filepaths = []
125+
for dirname in dirs:
126+
dir_thumbs.append(Thumbnail(root=root, name=dirname))
127+
break # only this folder
128+
return dir_thumbs
129+
130+
def generateKnownThumbs(path):
131+
''' Generate thumbnail dictionaries from the given file
132+
return a dummy when exhausted'''
133+
134+
known_thumbs = []
135+
if os.path.isfile(path):
136+
with open(path,'r') as fp:
137+
for line in fp:
138+
if not(line in ('[\n', ']')):
139+
yield json.loads(line[:-2])
140+
141+
yield {'name':'', 'touched':0}
142+
143+
class Thumbnail(object):
144+
145+
@staticmethod
146+
def keyTouched(tf):
147+
return tf.touched
148+
149+
def __init__(self, root, name):
150+
self.name = name
151+
self.path = os.path.abspath(os.path.join(root, name))
152+
self.touched = max(os.path.getmtime(self.path),
153+
os.path.getctime(self.path))
154+
if os.path.isdir(self.path):
155+
self.type ='dir'
156+
else:
157+
components = os.path.splitext(self.path)
158+
self.type = components[1].lower().lstrip('.')
159+
160+
def generateDataURL(self):
161+
162+
if self.type in IMAGE_TYPES:
163+
# read the image
164+
image = Image.open(self.path)
165+
image.thumbnail(THUMB_SIZE, Image.ANTIALIAS)
166+
# write the thumbnail to a buffer
167+
output = BytesIO()
168+
image.save(output, format='JPEG')
169+
image_data = output.getvalue()
170+
base64_string = base64.b64encode(image_data).decode('utf-8')
171+
return 'data:image/jpg;base64,' + base64_string
172+
else:
173+
return ''
174+
175+
def getDict(self):
176+
177+
return {'name': self.name,
178+
'type': self.type,
179+
'touched': self.touched,
180+
'data_url': self.generateDataURL()}

0 commit comments

Comments
 (0)