1
# Phatch - Photo Batch Processor
2
# Copyright (C) 2007-2008 www.stani.be
4
# This program is free software: you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program. If not, see http://www.gnu.org/licenses/
17
# Phatch recommends SPE (http://pythonide.stani.be) for editing python files.
22
import cPickle, os, pprint, time, traceback
25
from core import ct, pil
26
from models import Action
27
from message import send
28
from settings import create_settings
31
class PathError(Exception):
32
def __init__(self, filename):
33
"""PathError for invalid path."""
34
self.filename = filename
37
return '"%s" '%self.filename+_('is not a valid path')+'.'
40
def __init__(self,data):
43
def __call__(self,key):
46
def update_filename(self,result,parent_index,filename):
47
dirname, basename = os.path.split(filename)
48
message = "%s: %s\n%s: %s\n"%(_('In'),dirname,_('File'),
50
self.update(result,parent_index*self.child_max,newmsg=message)
53
def update_index(self,result,parent_index,child_index):
54
self.update(result,parent_index*self.child_max + child_index + 1)
60
def update(self,result,value,newmsg=''):
68
verify_app_user_path()
75
def verify_app_user_path():
76
if not os.path.exists(ct.APP_USER_PATH):
77
os.mkdir(ct.APP_USER_PATH)
81
return os.path.splitext(os.path.basename(f))[0].replace('_',' ')\
82
.replace('-',' ').title()
84
def init_error_log_file():
85
global ERROR_LOG_FILE, ERROR_LOG_COUNTER
87
ERROR_LOG_FILE = open(ct.ERROR_LOG_PATH, 'w')
89
def log_error(message,details,filename,action=None):
90
global ERROR_LOG_COUNTER
91
## details = os.linesep.join([
92
## 'File: "%s"'%filename,
93
## 'Details: "%s"'%details,
97
details += os.linesep + 'Action: ' + \
98
pprint.pformat(action.dump())
99
ERROR_LOG_FILE.write(os.linesep.join([
100
'Error %d: %s'%(ERROR_LOG_COUNTER,message),
104
traceback.print_exc(file=ERROR_LOG_FILE)
105
ERROR_LOG_FILE.write('*'+os.linesep)
106
ERROR_LOG_FILE.flush()
107
ERROR_LOG_COUNTER += 1
110
#---collect image files
111
def filter_image_files(folder,files, extensions):
112
files = [os.path.join(folder,file) for file in files]
113
return [file for file in files if os.path.isfile(file) and \
114
os.path.splitext(file)[1].lower() in extensions]
116
def get_image_files_folder(folder,extensions,recursive):
119
for folder, dirs, files in os.walk(folder):
120
image_files.extend(filter_image_files(folder, files, extensions))
123
return filter_image_files(folder, os.listdir(folder), extensions)
127
def check_actionlist(actions,settings):
128
#Check if there is something to do
130
send.frame_show_error('%s %s'%(_('Nothing to do.'),
131
_('The action list is empty.')))
134
#Skip disabled actions
135
actions = [action for action in actions if action.is_enabled()]
137
send.frame_show_error('%s %s'%(_('Nothing to do.'),
138
_('There is no action enabled.')))
141
#Check if there is a save statement
142
last_action = actions[-1]
143
if not (last_action.is_valid_last_action() or file_only(actions)):
144
send.frame_append_save_action()
147
#Check if overwrite is forced
148
settings['overwrite_existing_images_forced'] = \
149
actions[-1].is_overwrite_existing_images_forced()
153
def file_only(actions):
154
for action in actions:
155
if not ('file' in action.tags):
159
def check_images(image_files):
161
send.frame_show_progress( title = _("Checking images"),
162
parent_max = len(image_files))
165
valid = []; invalid = []
166
for index, image_file in enumerate(image_files):
167
pil.report_invalid_image(image_file,valid,invalid)
169
send.progress_update_filename(result,index,image_file)
170
if not result['keepgoing']:
172
send.progress_close()
174
#show invalid messages
177
send.frame_show_files_message(result,
178
message = _('Phatch can not handle %d image(s):')%len(invalid),
179
title = ct.FRAME_TITLE%('',_('Invalid images')),
185
#Check if there are files
189
send.frame_show_error('%s, %s.'%(_("Sorry"),_("no valid files found")))
193
def get_image_files(paths,extensions,recursive):
198
if os.path.isfile(p):
200
elif os.path.isdir(p):
201
result.extend(get_image_files_folder(p,extensions,recursive))
206
except PathError, error:
207
send.frame_show_error('%s, "%s" %s.'%(_('Sorry'),error.filename,
208
_('is not a valid path')))
211
def get_paths(paths, settings,drop=False):
212
if drop or (paths is None):
214
send.frame_show_execute_dialog(result,settings,paths)
215
if result['cancel']:return
216
paths = settings['paths']
218
send.frame_show_error(_('No files or folder selected.'))
222
def get_photo(image_file,image_index,result,save_metadata):
224
photo = pil.Photo(image_file,image_index,save_metadata)
225
result['skip'] = False
226
result['abort'] = False
228
except Exception, details:
231
message = '%s: %s.'%(_('Unable to open file'),image_file)
234
return process_error(photo,message,details,image_file,action,
238
def process_error(photo,message,details,image_file,action,result,ignore):
239
"""Logs error to file and show dialog box allowing the user to
240
skip,abort or ignore."""
241
details = log_error(message,details,image_file,action)
243
if result['stop_for_errors']:
244
send.frame_show_progress_error(result,message,details,
247
answer = result['answer']
248
if answer == _('abort'):
249
send.progress_close()
250
result['skip'] = False
251
result['abort'] = True
253
result['last_answer'] = answer
254
if answer == _('skip'):
255
result['skip'] = True
256
result['abort'] = False
258
elif result['last_answer'] == _('skip'):
259
result['skip'] = True
260
result['abort'] = False
262
result['skip'] = False
263
result['abort'] = False
266
def apply_action(action,photo,setting,cache,image_file,result):
268
photo = action.apply(photo,setting,cache)
269
result['skip'] = False
270
result['abort'] = False
272
except Exception, details:
273
folder, image = os.path.split(image_file)
275
reason = str(details)
278
message = '%s\n%s\n\n%s'%(
279
_("Can not apply action %(a)s on image '%(i)s' in folder:")%\
280
{'a':_(action.label),'i':image},
285
return process_error(photo,message,details,image_file,action,
288
def apply_actions(actions,settings,paths=None,drop=False):
289
"""Do all the actions."""
292
init_error_log_file()
295
actions = check_actionlist(actions,settings)
296
if not actions: return
298
#Get paths (and update settings)
299
paths = get_paths(paths,settings,drop=drop)
302
#Check if all files exist
303
extensions = ['.'+x for x in settings['extensions']]
304
image_files = get_image_files(paths,extensions,settings['recursive'])
305
if not image_files: return
307
#Check if all the images are valid
308
if settings['check_images_first']:
309
image_files = check_images(image_files)
310
if not image_files: return
313
for action in actions:
317
skip_existing_images = not (settings['overwrite_existing_images'] or\
318
settings['overwrite_existing_images_forced'])
319
save_metadata = settings['save_metadata']
321
'stop_for_errors' : settings['stop_for_errors'],
322
'last_answer' : None,
326
actions_amount = len(actions) + 1 #open image is extra action
328
is_done = actions[-1].is_done #checking method for resuming
329
read_only_settings = ReadOnlyDict(settings)
331
#Start progress dialog
332
send.frame_show_progress( title = _("Executing action list"),
333
parent_max = len(image_files),
334
child_max = actions_amount,
335
message = _('Starting...'),
338
for image_index, image_file in enumerate(image_files):
339
#update image file & progress dialog box
341
send.progress_update_filename(progress_result,image_index,image_file)
342
if progress_result and not progress_result['keepgoing']:
346
#open image and check for errors
347
photo, result = get_photo(image_file,image_index, result,save_metadata)
348
if result['abort']: return
349
elif result['skip']: break
351
#check if already not done
352
if skip_existing_images and is_done(photo):
356
for action_index, action in enumerate(actions):
359
send.progress_update_index(progress_result,image_index,action_index)
360
if progress_result and not progress_result['keepgoing']:
361
send.progress_close()
364
photo, result = apply_action(action,photo,
365
read_only_settings, cache,image_file,
367
if result['abort']: return
370
del photo, progress_result, action_index, action
371
send.progress_close()
376
def import_module(folder,module):
377
return getattr(__import__('%s.%s'%(folder,module)),module)
379
def import_actions():
380
global ACTIONS, ACTION_LABELS, ACTION_FIELDS
382
from core.lib.events import send
385
for filename in glob.glob(os.path.join(ct.ACTIONS_PATH,'*.py')):
386
basename= os.path.basename(os.path.splitext(filename)[0])
387
module = import_module('actions',basename)
389
cl = getattr(module,ct.ACTION)
390
except AttributeError:
393
ACTIONS[cl.label] = cl
395
ACTION_LABELS = ACTIONS.keys()
399
for label in ACTIONS:
400
ACTION_FIELDS[label] = ACTIONS[label]()._fields
402
def save_actionlist(filename,data):
403
"""data = {'actions':...}"""
405
if os.path.splitext(filename)[1].lower() != ct.EXTENSION:
406
filename += ct.EXTENSION
408
data['actions'] = [action.dump() for action in data['actions']]
410
if os.path.isfile(filename):
411
os.rename(filename,filename+'~')
413
f = open(filename,'wb')
414
f.write(pprint.pformat(data))
417
def open_actionlist(filename):
419
f = open(filename,'rb')
426
for action in data['actions']:
427
actionLabel = action['label']
428
actionFields = action['fields']
429
newAction = ACTIONS[actionLabel]()
430
invalid_labels.extend(['- %s (%s)'%(label,actionLabel)
431
for label in newAction.load(actionFields)])
432
result.append(newAction)
433
data['actions'] = result
434
data['invalid labels'] = invalid_labels