~ubuntu-branches/debian/wheezy/phatch/wheezy

« back to all changes in this revision

Viewing changes to phatch/core/api.py

  • Committer: Bazaar Package Importer
  • Author(s): Emilio Pozuelo Monfort
  • Date: 2008-02-13 23:48:47 UTC
  • Revision ID: james.westby@ubuntu.com-20080213234847-mp6vc4y88a9rz5qz
Tags: upstream-0.1
ImportĀ upstreamĀ versionĀ 0.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Phatch - Photo Batch Processor
 
2
# Copyright (C) 2007-2008 www.stani.be
 
3
#
 
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.
 
8
#
 
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.
 
13
 
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/
 
16
#
 
17
# Phatch recommends SPE (http://pythonide.stani.be) for editing python files.
 
18
 
 
19
#---import modules
 
20
 
 
21
#standard library
 
22
import cPickle, os, pprint, time, traceback
 
23
 
 
24
#gui-independent
 
25
from core import ct, pil
 
26
from models import Action
 
27
from message import send
 
28
from settings import create_settings
 
29
 
 
30
#---classes
 
31
class PathError(Exception):
 
32
    def __init__(self, filename):
 
33
        """PathError for invalid path."""
 
34
        self.filename   = filename
 
35
        
 
36
    def __str__(self):
 
37
        return '"%s" '%self.filename+_('is not a valid path')+'.'
 
38
 
 
39
class ReadOnlyDict:
 
40
    def __init__(self,data):
 
41
        self.data   = data
 
42
        
 
43
    def __call__(self,key):
 
44
        return self.data[key]
 
45
    
 
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'),
 
49
                                basename)
 
50
        self.update(result,parent_index*self.child_max,newmsg=message)
 
51
        self.sleep()
 
52
        
 
53
    def update_index(self,result,parent_index,child_index):
 
54
        self.update(result,parent_index*self.child_max + child_index + 1)
 
55
        
 
56
    #---overwrite
 
57
    def close(self):
 
58
        pass
 
59
        
 
60
    def update(self,result,value,newmsg=''):
 
61
        pass
 
62
        
 
63
    def sleep(self):
 
64
        pass
 
65
    
 
66
#---init/exit
 
67
def init():
 
68
    verify_app_user_path()
 
69
    import_actions()
 
70
    
 
71
def exit():
 
72
    pass
 
73
        
 
74
 
 
75
def verify_app_user_path():
 
76
    if not os.path.exists(ct.APP_USER_PATH):
 
77
        os.mkdir(ct.APP_USER_PATH)
 
78
 
 
79
#---various
 
80
def title(f):
 
81
    return os.path.splitext(os.path.basename(f))[0].replace('_',' ')\
 
82
        .replace('-',' ').title()
 
83
#---error logs
 
84
def init_error_log_file():
 
85
    global ERROR_LOG_FILE, ERROR_LOG_COUNTER
 
86
    ERROR_LOG_COUNTER   = 0
 
87
    ERROR_LOG_FILE      = open(ct.ERROR_LOG_PATH, 'w')
 
88
 
 
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,
 
94
##    ])
 
95
    details = ''
 
96
    if action:
 
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),
 
101
        details,
 
102
        os.linesep,
 
103
    ]))
 
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
 
108
    return details
 
109
 
 
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]
 
115
       
 
116
def get_image_files_folder(folder,extensions,recursive):
 
117
    if recursive:
 
118
        image_files  = []
 
119
        for folder, dirs, files in os.walk(folder):
 
120
            image_files.extend(filter_image_files(folder, files, extensions))
 
121
        return image_files
 
122
    else:
 
123
        return filter_image_files(folder, os.listdir(folder), extensions)
 
124
    
 
125
 
 
126
#---check
 
127
def check_actionlist(actions,settings):
 
128
    #Check if there is something to do
 
129
    if actions == []:
 
130
        send.frame_show_error('%s %s'%(_('Nothing to do.'),
 
131
            _('The action list is empty.')))
 
132
        return None
 
133
    
 
134
    #Skip disabled actions
 
135
    actions = [action for action in actions if action.is_enabled()]
 
136
    if actions == []:
 
137
        send.frame_show_error('%s %s'%(_('Nothing to do.'),
 
138
            _('There is no action enabled.')))
 
139
        return None
 
140
    
 
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()
 
145
        return None
 
146
    
 
147
    #Check if overwrite is forced
 
148
    settings['overwrite_existing_images_forced'] = \
 
149
        actions[-1].is_overwrite_existing_images_forced()
 
150
 
 
151
    return actions
 
152
 
 
153
def file_only(actions):
 
154
    for action in actions:
 
155
        if not ('file' in action.tags):
 
156
            return False
 
157
    return True
 
158
 
 
159
def check_images(image_files):
 
160
    #show dialog
 
161
    send.frame_show_progress(   title       = _("Checking images"),
 
162
                                parent_max  = len(image_files))
 
163
                                
 
164
    #check files
 
165
    valid = []; invalid = []
 
166
    for index, image_file in enumerate(image_files):
 
167
        pil.report_invalid_image(image_file,valid,invalid)
 
168
        result              = {}
 
169
        send.progress_update_filename(result,index,image_file)
 
170
        if not result['keepgoing']:
 
171
            return
 
172
    send.progress_close()
 
173
    
 
174
    #show invalid messages
 
175
    if invalid:
 
176
        result      = {}
 
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')), 
 
180
            files   = invalid)
 
181
        if result['cancel']:
 
182
            return
 
183
    image_files              = valid
 
184
 
 
185
    #Check if there are files
 
186
    if image_files:
 
187
        return image_files
 
188
    else:
 
189
        send.frame_show_error('%s, %s.'%(_("Sorry"),_("no valid files found")))
 
190
        return
 
191
 
 
192
#---get
 
193
def get_image_files(paths,extensions,recursive):
 
194
    try:
 
195
        result  = []
 
196
        for p in paths:
 
197
            p   = p.strip()
 
198
            if os.path.isfile(p):
 
199
                result.append(p)
 
200
            elif os.path.isdir(p):
 
201
                result.extend(get_image_files_folder(p,extensions,recursive))
 
202
            else:
 
203
                raise PathError(p)
 
204
        result.sort()
 
205
        return result
 
206
    except PathError, error:
 
207
        send.frame_show_error('%s, "%s" %s.'%(_('Sorry'),error.filename,
 
208
                _('is not a valid path')))
 
209
        return []
 
210
 
 
211
def get_paths(paths, settings,drop=False):
 
212
    if drop or (paths is None):
 
213
        result  = {}
 
214
        send.frame_show_execute_dialog(result,settings,paths)
 
215
        if result['cancel']:return
 
216
        paths    = settings['paths']
 
217
        if not paths:
 
218
            send.frame_show_error(_('No files or folder selected.'))
 
219
            return None
 
220
    return paths
 
221
 
 
222
def get_photo(image_file,image_index,result,save_metadata):
 
223
    try:
 
224
        photo   = pil.Photo(image_file,image_index,save_metadata)
 
225
        result['skip'] = False
 
226
        result['abort'] = False
 
227
        return photo, result
 
228
    except Exception, details:
 
229
        photo   = None
 
230
        #log error details
 
231
        message = '%s: %s.'%(_('Unable to open file'),image_file)
 
232
        ignore  = False
 
233
        action  = None
 
234
        return process_error(photo,message,details,image_file,action,
 
235
            result,ignore)
 
236
    
 
237
#---apply
 
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)
 
242
    #show error dialog
 
243
    if result['stop_for_errors']:
 
244
        send.frame_show_progress_error(result,message,details,
 
245
            ignore = ignore)
 
246
        #if result:
 
247
        answer = result['answer']
 
248
        if answer == _('abort'):
 
249
            send.progress_close()
 
250
            result['skip']  = False
 
251
            result['abort'] = True
 
252
            return photo, result
 
253
        result['last_answer'] = answer
 
254
        if answer == _('skip'):
 
255
            result['skip']  = True
 
256
            result['abort'] = False
 
257
            return photo, result
 
258
    elif result['last_answer'] == _('skip'):
 
259
        result['skip']  = True
 
260
        result['abort'] = False
 
261
        return photo, result
 
262
    result['skip']  = False
 
263
    result['abort'] = False
 
264
    return photo, result
 
265
 
 
266
def apply_action(action,photo,setting,cache,image_file,result):
 
267
    try:
 
268
        photo   = action.apply(photo,setting,cache)
 
269
        result['skip']  = False
 
270
        result['abort'] = False
 
271
        return photo, result
 
272
    except Exception, details:
 
273
        folder, image   = os.path.split(image_file)
 
274
        try:
 
275
            reason  = str(details)
 
276
        except:
 
277
            reason  = '?'
 
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},
 
281
            folder,
 
282
            reason,
 
283
        )
 
284
        ignore  = True
 
285
        return process_error(photo,message,details,image_file,action,
 
286
            result,ignore)
 
287
    
 
288
def apply_actions(actions,settings,paths=None,drop=False):
 
289
    """Do all the actions."""
 
290
    
 
291
    #Start log file
 
292
    init_error_log_file()
 
293
    
 
294
    #Check action list
 
295
    actions = check_actionlist(actions,settings)
 
296
    if not actions: return
 
297
    
 
298
    #Get paths (and update settings)
 
299
    paths    = get_paths(paths,settings,drop=drop)
 
300
    if not paths: return
 
301
            
 
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
 
306
    
 
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
 
311
 
 
312
    #Initialize actions
 
313
    for action in actions:
 
314
        action.init()
 
315
        
 
316
    #Retrieve settings
 
317
    skip_existing_images    = not (settings['overwrite_existing_images'] or\
 
318
                                settings['overwrite_existing_images_forced'])
 
319
    save_metadata           = settings['save_metadata']
 
320
    result                  = {
 
321
        'stop_for_errors'   : settings['stop_for_errors'],
 
322
        'last_answer'       : None,
 
323
    }
 
324
    
 
325
    #Execute action list
 
326
    actions_amount          = len(actions) + 1 #open image is extra action
 
327
    cache                   = {} 
 
328
    is_done                 = actions[-1].is_done #checking method for resuming
 
329
    read_only_settings      = ReadOnlyDict(settings)
 
330
    
 
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...'),
 
336
                            )
 
337
    
 
338
    for image_index, image_file in enumerate(image_files):
 
339
        #update image file & progress dialog box
 
340
        progress_result = {}
 
341
        send.progress_update_filename(progress_result,image_index,image_file)
 
342
        if progress_result and not progress_result['keepgoing']:
 
343
            send.progress_close
 
344
            return
 
345
        
 
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
 
350
        
 
351
        #check if already not done
 
352
        if skip_existing_images and is_done(photo):
 
353
            continue
 
354
        
 
355
        #do the actions
 
356
        for action_index, action in enumerate(actions):
 
357
            #update progress
 
358
            progress_result = {}
 
359
            send.progress_update_index(progress_result,image_index,action_index)
 
360
            if progress_result and not progress_result['keepgoing']:
 
361
                send.progress_close()
 
362
                return
 
363
            #apply action
 
364
            photo, result  = apply_action(action,photo,
 
365
                                    read_only_settings, cache,image_file,
 
366
                                    result)
 
367
            if      result['abort']: return
 
368
            elif    result['skip']:
 
369
                break
 
370
        del photo, progress_result, action_index, action
 
371
    send.progress_close()
 
372
    
 
373
#---common
 
374
import glob
 
375
#---classes
 
376
def import_module(folder,module):
 
377
    return getattr(__import__('%s.%s'%(folder,module)),module)
 
378
 
 
379
def import_actions():
 
380
    global ACTIONS, ACTION_LABELS, ACTION_FIELDS
 
381
    #if bitmaps
 
382
    from core.lib.events import send
 
383
    #actions=result
 
384
    ACTIONS = {}
 
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)
 
388
        try:
 
389
            cl  = getattr(module,ct.ACTION)
 
390
        except AttributeError:
 
391
            continue
 
392
        #register action
 
393
        ACTIONS[cl.label]   = cl
 
394
    #ACTION_LABELS
 
395
    ACTION_LABELS                          = ACTIONS.keys()
 
396
    ACTION_LABELS.sort()
 
397
    #ACTION_FIELDS
 
398
    ACTION_FIELDS = {}
 
399
    for label in ACTIONS:
 
400
        ACTION_FIELDS[label]  = ACTIONS[label]()._fields
 
401
 
 
402
def save_actionlist(filename,data):
 
403
    """data = {'actions':...}"""
 
404
    #check filename
 
405
    if os.path.splitext(filename)[1].lower() != ct.EXTENSION:
 
406
        filename    += ct.EXTENSION
 
407
    #prepare data
 
408
    data['actions'] = [action.dump() for action in data['actions']]
 
409
    #backup previous
 
410
    if os.path.isfile(filename):
 
411
        os.rename(filename,filename+'~')
 
412
    #write it
 
413
    f       = open(filename,'wb')
 
414
    f.write(pprint.pformat(data))
 
415
    f.close()
 
416
 
 
417
def open_actionlist(filename):
 
418
    #read source
 
419
    f       = open(filename,'rb')
 
420
    source  = f.read()
 
421
    f.close()
 
422
    #load data
 
423
    data                = eval(source)
 
424
    result              = []
 
425
    invalid_labels      = []
 
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
 
435
    return data