3
# Phatch - Photo Batch Processor
4
# Copyright (C) 2009 Nadia Alramli
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 3 of the License, or
9
# (at your option) any later version.
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.
16
# You should have received a copy of the GNU General Public License
17
# along with this program. If not, see http://www.gnu.org/licenses/
19
# Phatch recommends SPE (http://pythonide.stani.be) for editing python files.
9
from subprocess import Popen, PIPE
12
DISABLE_ACTIONS = ['save', 'save tags', 'rename']
15
#gettext.install('test')
17
def get_actions(path):
18
"""Get a dictionary that maps action names to action classes"""
20
os.path.basename(action_file).partition('.')[0]
21
for action_file in glob.glob(os.path.join(path, '*.py'))
24
(name, __import__('actions.%s' % name, name, fromlist=['actions']).Action())
25
for name in action_names
30
def get_actionlist(actions):
31
"""Get actionlist structure from a list of actions"""
32
data = [action.dump() for action in actions]
33
return {'description': '', 'actions': data}
36
def build_library_actionlists(library_path, in_path, out_path):
37
"""Build library actionlists"""
38
if not os.path.exists(in_path):
42
(os.path.basename(actionlist_file).partition('.')[0], eval(open(actionlist_file).read()))
43
for actionlist_file in glob.glob(os.path.join(library_path, '*.phatch'))
45
for name, actionlist in actionlists:
48
for action in actionlist['actions']
49
if action['label'] == 'Save'
33
from collections import defaultdict
38
TEMPLATE ="Errors: %(errors)s\nMissing: %(missing)s\nCorrupted: %(corrupted)s\nMismatch: %(mismatch)s\nNew: %(new)s"
41
gettext.install('test')
42
def system_path(path):
43
"""Convert a path string into the correct form"""
44
return os.path.abspath(os.path.normpath(path))
46
sys.path.insert(0, system_path('..'))
48
class PhatchTest(object):
50
DEFAULT_INPUT = system_path('images/input')
51
DEFAULT_OUTPUT = system_path('images/output')
52
DEFAULT_LOG = system_path('logs')
53
DEFAULT_REPORT = system_path('report')
54
ACTIONS_PATH = system_path('../phatch/actions')
55
LIBRARY_PATH = system_path('../data/actionlists')
56
OUT_ACTIONS_PATH = system_path('test_actionlists')
57
PHATCH_PATH = system_path('../phatch/phatch.py')
59
def __init__(self, input=DEFAULT_INPUT, output=DEFAULT_OUTPUT,
60
verbose=False, log=DEFAULT_LOG, compare=None):
61
"""Build a dictionary of actions"""
68
format='%(asctime)s %(levelname)s:%(message)s'
71
self.compare = compare
72
create(self.OUT_ACTIONS_PATH)
75
file_name(action_file)
76
for action_file in glob.glob(os.path.join(self.ACTIONS_PATH, '*.py'))
51
for save_action in save_actions:
52
save_action['fields']['In'] = '%s/<subfolder>' % out_path
53
save_action['fields']['File Name'] = '<filename>_%s' % name
54
result.append(write_file(os.path.join(
63
def build_actionlists(actions, in_path, out_path):
64
"""Build actionlists"""
66
if not os.path.exists(in_path):
68
save_action = actions['save']
69
save_action.set_field(
71
'%s/<subfolder>' % out_path
73
for name, action in actions.iteritems():
74
if name in DISABLE_ACTIONS:
77
possible_choices(action, choices)
78
for choice in choices:
80
for fname, fvalue in choice.iteritems():
81
action.set_field(fname, fvalue)
82
filename.append('%s=%s' % (fname.lower(), str(fvalue).lower()))
83
filename = '_'.join(filename)
84
save_action.set_field(
79
(name, __import__('phatch.actions.%s' % name, name, fromlist=['phatch.actions']).Action())
80
for name in action_names
81
if name != '__init__' and name not in DISABLE
84
for name, action in self.actions.iteritems():
85
for tag in action.tags:
86
if tag in self.by_tag:
87
self.by_tag[tag][name] = action
89
self.by_tag[tag] = {name: action}
90
self.file_actions = dict(
92
for name, action in self.actions.iteritems()
96
(file_name(actionlist_file), eval(open(actionlist_file).read()))
97
for actionlist_file in glob.glob(os.path.join(self.LIBRARY_PATH, '*.phatch'))
99
self.verbose = verbose
103
if os.path.exists(self.output):
104
shutil.rmtree(self.output)
105
if os.path.exists(self.OUT_ACTIONS_PATH):
106
shutil.rmtree(self.OUT_ACTIONS_PATH)
109
def build_test(self, name, actionlist):
110
file_actions = [action for action in actionlist if action.valid_last]
111
for index, file_action in enumerate(file_actions):
112
file_action.set_field(
116
file_action.set_field(
86
'<filename>_%s' % filename
88
actionlists.append(write_file(
91
'%s.phatch' % filename
93
get_actionlist([action, save_action])
98
def possible_choices(action, choices):
118
'<filename>_%s' % name_index(name, index)
120
return dump(actionlist)
123
def get_output_filename(self, filename, file_action):
125
ftype = file_action.get_field_string('As')
126
if ftype == '<type>':
130
return '%%(filename)s_%s.%s' % (filename, ftype)
133
def generate_actionlists(self, save='save', actions=None):
134
print 'Generating actionlists...'
137
actions = self.actions
138
for name, action in actions.iteritems():
139
if name in self.file_actions:
140
actionlist = [action]
143
file_action = self.actions[save]
144
actionlist = [action, file_action]
145
choices = possible_choices(action)
146
for choice in choices:
147
set_action_fields(action, choice)
148
cname = choice_name(choice)
150
filename = '%s_%s' % (name, cname)
153
filename = shorten(filename)
154
actionlist_data = self.build_test(filename, actionlist)
155
path = os.path.join(self.OUT_ACTIONS_PATH, '%s.phatch' % filename)
156
out_filename = self.get_output_filename(filename, file_action)
157
write_file(path, actionlist_data)
158
actionlists[filename] = {
160
'filename': out_filename
165
def generate_library_actionlists(self):
166
"""Build library actionlists"""
167
print 'Generating library actionlists...'
169
for name, actionlist in self.library.iteritems():
172
for action in actionlist['actions']
173
if action['label'].lower() in self.file_actions
175
for index, file_action in enumerate(file_actions):
176
file_action['fields']['In'] = '%s' % self.output
177
file_action['fields']['File Name'] = '<filename>_%s' % name_index(name, index)
178
ftype = file_action['fields']['As']
179
if ftype == '<type>':
181
path = os.path.join(self.OUT_ACTIONS_PATH, '%s.phatch' % name)
182
out_filename = '%%(filename)s_%s.%s' % (name, ftype)
183
write_file(path, actionlist)
184
actionlists[name] = {
186
'filename': out_filename
191
def generate_custom_actionlists(self, action1, action2):
192
print 'Generating save actionlists...'
194
for choice1 in possible_choices(action1):
195
set_action_fields(action1, choice1)
196
cname1 = choice_name(choice1)
197
for choice2 in possible_choices(action2):
198
set_action_fields(action2, choice2)
199
cname2 = choice_name(choice2)
201
[action1.label.replace(' ', '-'), cname1,
202
action2.label.replace(' ', '-'), cname2]
204
actionlist_data = self.build_test(filename, [action1, action2])
205
path = os.path.join(self.OUT_ACTIONS_PATH, '%s.phatch' % filename)
206
out_filename = self.get_output_filename(filename, action2)
207
write_file(path, actionlist_data)
208
actionlists[filename] = {
210
'filename': out_filename
215
def execute_actionlists(self, actionlists=None):
226
actionlists = phatch.generate_library_actionlists()
227
actionlists.update(phatch.generate_actionlists())
228
total = len(actionlists)
229
for i, name in enumerate(sorted(actionlists)):
231
print name.title().center(50, '-')
234
'\rRunning %s/%s %s' % (
241
if not phatch.execute(actionlists[name]['path']):
242
result['errors'].append(name)
246
print '\nChecking all output images exist...'
247
if os.path.isfile(self.input):
248
images = [tuple(os.path.basename(self.input).rsplit('.', 1))]
250
images = [tuple(image.rsplit('.', 1)) for image in os.listdir(self.input)]
252
for name, actionlist in actionlists.iteritems():
253
for image_name, image_type in images:
254
filename = actionlist['filename'] % {
255
'filename': image_name,
258
path = os.path.join(self.output, filename)
259
if not os.path.exists(path):
260
result['missing'].append(filename)
262
filenames.append(filename)
263
total = len(filenames)
265
print '\nComparing results...'
266
for i, filename in enumerate(filenames):
268
'\rComparing %s/%s %s' % (
271
filename[:50].ljust(50)
276
im1 = Image.open(os.path.join(self.output, filename))
278
result['corrupted'].append(filename)
283
im2 = Image.open(os.path.join(self.compare, filename))
285
result['new'].append(filename)
288
if not compare(im1, im2):
289
result['mismatch'].append(filename)
290
except KeyboardInterrupt:
294
for key, value in result.iteritems():
295
if value and isinstance(value, list):
296
value = '\n\t'.join(value)
299
final_result[key] = value
300
output = TEMPLATE % final_result
301
output += '\n%s' % ', '.join('%s: %s' % (key, len(value)) for key, value in result.iteritems())
302
f = open(self.DEFAULT_REPORT, 'w')
306
print 'The report was saved in: %s' % self.DEFAULT_REPORT
307
print 'Execution took %.2f seconds' % (time.time() - start)
310
def execute(self, actionlist):
311
"""Execute the actionlist in phatch"""
315
stdout = subprocess.PIPE
316
output, error = subprocess.Popen(
317
['python', self.PHATCH_PATH, '-cv', actionlist, self.input],
318
stdout=stdout, stderr=subprocess.PIPE
320
logs_file = open(system_path(os.path.expanduser('~/.cache/phatch/log')))
321
logs = logs_file.read()
326
'Executing actionlist: %s generated the following logs:\n%s\n'
331
'Executing actionlist: %s generated the following error:\n%s\n'
332
% (actionlist, error)
334
return not(error or logs)
338
return x.replace('True', '1').replace('False', '0')\
339
.replace('Background', 'Bg').replace('Rotation', 'Rot')\
340
.replace('Options', 'Op').replace('Position', 'Pos')\
341
.replace('Orientation', 'Or').replace('Vertical', 'Ver')\
342
.replace('Horizontal', 'Hor').replace('Transformation', 'Tr')\
343
.replace('Advanced', 'Adv').replace('Thumbnail', 'Thumb')\
344
.replace('Automatic', 'Auto').replace(' (use exif orientation)', '')\
345
.replace('Rotate', 'Rot').replace('Update', 'Upd')\
346
.replace('-Corner', '')
349
def possible_choices(action):
351
possible_choices_helper(action, choices)
355
def possible_choices_helper(action, choices):
99
356
"""Generate all possible action choices"""
100
357
if hasattr(action, 'get_relevant_field_labels'):
101
358
relevant = action.get_relevant_field_labels()
141
408
f.write(pprint.pformat(data))
146
def execute(input, actionlist):
147
"""Execute the actionlist in phatch"""
148
output, error = Popen(
149
['python', '../phatch/phatch.py', '-c', actionlist, input],
150
stdout=open('logs', 'a'), stderr=open('logs', 'a')
154
'Executing actionlist: %s generated the following output:\n%s'
155
% (actionlist, output)
159
'Executing actionlist: %s generated the following error:\n%s'
160
% (actionlist, error)
413
"""Create a path if it doesn't already exist"""
414
if not os.path.exists(path):
418
"""Get the file name from path without its extension"""
419
return os.path.basename(path).partition('.')[0]
421
def name_index(name, index):
423
return '%s_%s' % (name, index)
426
def choice_name(choice):
429
field_name.strip().replace(' ', '-'),
430
('%s' % field_value).strip().replace(' ', '-')
432
for field_name, field_value in choice.iteritems()
436
def compare(im1, im2):
437
return list(im1.getdata()) == list(im2.getdata())
439
def banner(title, width=50):
442
print '*%s*' % title.center(48, ' ')
164
446
if __name__ == '__main__':
165
# Hack to be able to import phatch
166
sys.path.insert(0, '../phatch')
172
format='%(levelname)s: %(message)s'
176
448
parser = optparse.OptionParser()
177
449
parser.add_option(
179
default='images/source',
451
default=PhatchTest.DEFAULT_INPUT,
180
452
help='Image input folder [default: %default]'
182
454
parser.add_option(
183
455
'-o', '--output',
184
default='images/output',
456
default=PhatchTest.DEFAULT_OUTPUT,
185
457
help='Image output folder [default: %default]'
187
459
parser.add_option(
463
help='Verbose output'
188
466
'-a', '--actionlists',
189
467
action='store_true',
191
469
help='Generate actionlists only'
193
471
parser.add_option(
197
help='Run library actionlists tests only'
473
default=PhatchTest.DEFAULT_LOG,
474
help='Log file path [default: %default]'
486
help='Test custom action'
491
help='Compare results'
199
493
options, args = parser.parse_args()
200
options.output = os.path.abspath(options.output)
204
actions = get_actions('../phatch/actions')
206
# Generating actionlists
208
print 'Generating library actionlists...',
209
actionlists = build_library_actionlists(
210
'../data/actionlists',
216
if not options.library:
217
sys.stdout.write('Generating all possible actionlists...')
220
build_actionlists(actions, 'test_actionlists', '<root>/output')
495
input=system_path(options.input),
496
output=system_path(options.output),
497
verbose=options.verbose,
499
compare=options.compare
502
actionlists = phatch.generate_custom_actionlists(
503
action1=phatch.actions[options.custom[0]],
504
action2=phatch.actions[options.custom[1]],
506
elif options.tag == 'library':
507
actionlists = phatch.generate_library_actionlists()
508
elif options.tag == 'save':
509
actionlists = phatch.generate_custom_actionlists(
510
action1=phatch.actions['convert_mode'],
511
action2=phatch.actions['save'],
515
actions = phatch.by_tag[options.tag]
518
'Tag %s is not available. Select one of:\n%s' % (
520
', '.join(sorted(phatch.by_tag.keys() + ['library', 'save']))
223
if not options.actionlists:
224
total = len(actionlists)
225
for i, name in enumerate(actionlists):
226
sys.stdout.write(' Running %s/%s\r' % (i, total))
228
execute(options.input, name)
229
except KeyboardInterrupt:
231
print 'Execution took %.2f seconds' % (time.time() - start)
523
print 'Testing the following actions:'
524
print ', '.join(actions.keys())
525
actionlists = phatch.generate_actionlists(actions=actions)
527
actionlists = phatch.generate_library_actionlists()
528
actionlists.update(phatch.generate_actionlists())
529
actionlists.update(phatch.generate_custom_actionlists(
530
action1=phatch.actions['convert_mode'],
531
action2=phatch.actions['save'],
533
if not options.actionlists:
534
phatch.execute_actionlists(actionlists)