1 # GNU MediaGoblin -- federated, autonomous media hosting
2 # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU Affero 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 Affero General Public License for more details.
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 from mediagoblin
import mg_globals
as mgg
26 from mediagoblin
.db
.models
import MediaEntry
27 from mediagoblin
.processing
import (
28 BadMediaFail
, FilenameBuilder
,
29 MediaProcessor
, ProcessingManager
,
31 from mediagoblin
.submit
.lib
import run_process_media
32 from mediagoblin
.tools
.exif
import exif_fix_image_orientation
, \
33 extract_exif
, clean_exif
, get_gps_data
, get_useful
, \
34 exif_image_needs_rotation
35 from mediagoblin
.tools
.translate
import lazy_pass_to_ugettext
as _
37 _log
= logging
.getLogger(__name__
)
40 'NEAREST': Image
.NEAREST
,
41 'BILINEAR': Image
.BILINEAR
,
42 'BICUBIC': Image
.BICUBIC
,
43 'ANTIALIAS': Image
.ANTIALIAS
}
45 MEDIA_TYPE
= 'mediagoblin.media_types.image'
48 def resize_image(proc_state
, resized
, keyname
, target_name
, new_size
,
51 Store a resized version of an image and return its pathname.
54 proc_state -- the processing state for the image to resize
55 resized -- an image from Image.open() of the original image being resized
56 keyname -- Under what key to save in the db.
57 target_name -- public file path for the new resized image
58 exif_tags -- EXIF data for the original image
59 workdir -- directory path for storing converted image files
60 new_size -- 2-tuple size for the resized image
62 config
= mgg
.global_config
['media_type:mediagoblin.media_types.image']
64 resized
= exif_fix_image_orientation(resized
, exif_tags
) # Fix orientation
66 filter_config
= config
['resize_filter']
68 resize_filter
= PIL_FILTERS
[filter_config
.upper()]
70 raise Exception('Filter "{0}" not found, choose one of {1}'.format(
71 unicode(filter_config
),
72 u
', '.join(PIL_FILTERS
.keys())))
74 resized
.thumbnail(new_size
, resize_filter
)
76 # Copy the new file to the conversion subdir, then remotely.
77 tmp_resized_filename
= os
.path
.join(workdir
, target_name
)
78 with
file(tmp_resized_filename
, 'w') as resized_file
:
79 resized
.save(resized_file
, quality
=config
['quality'])
80 proc_state
.store_public(keyname
, tmp_resized_filename
, target_name
)
83 def resize_tool(proc_state
, force
, keyname
, target_name
,
84 conversions_subdir
, exif_tags
, new_size
=None):
85 # filename -- the filename of the original image being resized
86 filename
= proc_state
.get_orig_filename()
88 # Use the default size if new_size was not given
90 max_width
= mgg
.global_config
['media:' + keyname
]['max_width']
91 max_height
= mgg
.global_config
['media:' + keyname
]['max_height']
92 new_size
= (max_width
, max_height
)
94 # If the size of the original file exceeds the specified size for the desized
95 # file, a target_name file is created and later associated with the media
97 # Also created if the file needs rotation, or if forced.
99 im
= Image
.open(filename
)
103 or im
.size
[0] > new_size
[0]\
104 or im
.size
[1] > new_size
[1]\
105 or exif_image_needs_rotation(exif_tags
):
107 proc_state
, im
, unicode(keyname
), target_name
,
109 exif_tags
, conversions_subdir
)
112 SUPPORTED_FILETYPES
= ['png', 'gif', 'jpg', 'jpeg', 'tiff']
115 def sniff_handler(media_file
, **kw
):
116 _log
.info('Sniffing {0}'.format(MEDIA_TYPE
))
117 if kw
.get('media') is not None: # That's a double negative!
118 name
, ext
= os
.path
.splitext(kw
['media'].filename
)
119 clean_ext
= ext
[1:].lower() # Strip the . from ext and make lowercase
121 if clean_ext
in SUPPORTED_FILETYPES
:
122 _log
.info('Found file extension in supported filetypes')
125 _log
.debug('Media present, extension not found in {0}'.format(
126 SUPPORTED_FILETYPES
))
128 _log
.warning('Need additional information (keyword argument \'media\')'
129 ' to be able to handle sniffing')
134 class ProcessImage(object):
135 """Code to process an image. Will be run by celery.
137 A Workbench() represents a local tempory dir. It is automatically
138 cleaned up when this function exits.
140 def __init__(self
, proc_state
=None):
142 self
.proc_state
= proc_state
143 self
.entry
= proc_state
.entry
144 self
.workbench
= proc_state
.workbench
146 # Conversions subdirectory to avoid collisions
147 self
.conversions_subdir
= os
.path
.join(
148 self
.workbench
.dir, 'convirsions')
150 self
.orig_filename
= proc_state
.get_orig_filename()
151 self
.name_builder
= FilenameBuilder(self
.orig_filename
)
154 self
.exif_tags
= extract_exif(self
.orig_filename
)
156 os
.mkdir(self
.conversions_subdir
)
158 def reprocess_action(self
, args
):
160 List the available actions for media in a given state
162 if args
[0].state
== 'processed':
163 print _('\n Available reprocessing actions for processed images:'
164 '\n \t --resize: thumb or medium'
166 '\n \t --size: max_width max_height (defaults to'
170 def _parser(self
, args
):
172 Parses the unknown args from the gmg parser
174 parser
= argparse
.ArgumentParser()
177 choices
=['thumb', 'medium'])
181 metavar
=('max_width', 'max_height'),
184 '--initial_processing',
187 return parser
.parse_args(args
[1])
189 def _check_eligible(self
, entry_args
, reprocess_args
):
191 Check to see if we can actually process the given media as requested
194 if entry_args
.state
== 'processed':
195 if reprocess_args
.initial_processing
:
196 raise Exception(_('You can not run --initial_processing on'
197 ' media that has already been processed.'))
199 if entry_args
.state
== 'failed':
200 if reprocess_args
.resize
:
201 raise Exception(_('You can not run --resize on media that has'
202 ' not been processed.'))
203 if reprocess_args
.size
:
204 _log
.warn('With --initial_processing, the --size flag will be'
207 if entry_args
.state
== 'processing':
208 raise Exception(_('We currently do not support reprocessing on'
209 ' media that is in the "processing" state.'))
211 def initial_processing(self
):
212 # Is there any GPS data
213 gps_data
= get_gps_data(self
.exif_tags
)
215 # Always create a small thumbnail
216 resize_tool(self
.proc_state
, True, 'thumb', self
.orig_filename
,
217 self
.name_builder
.fill('{basename}.thumbnail{ext}'),
218 self
.conversions_subdir
, self
.exif_tags
)
220 # Possibly create a medium
221 resize_tool(self
.proc_state
, False, 'medium', self
.orig_filename
,
222 self
.name_builder
.fill('{basename}.medium{ext}'),
223 self
.conversions_subdir
, self
.exif_tags
)
225 # Copy our queued local workbench to its final destination
226 self
.proc_state
.copy_original(self
.name_builder
.fill('{basename}{ext}'))
228 # Remove queued media file from storage and database
229 self
.proc_state
.delete_queue_file()
231 # Insert exif data into database
232 exif_all
= clean_exif(self
.exif_tags
)
235 self
.entry
.media_data_init(exif_all
=exif_all
)
238 for key
in list(gps_data
.keys()):
239 gps_data
['gps_' + key
] = gps_data
.pop(key
)
240 self
.entry
.media_data_init(**gps_data
)
242 def reprocess(self
, reprocess_info
):
244 This function actually does the reprocessing when called by
245 ProcessMedia in gmg/processing/task.py
249 # Did they specify a size? They must specify either both or none, so
250 # we only need to check if one is present
251 if reprocess_info
.get('max_width'):
252 max_width
= reprocess_info
['max_width']
253 max_height
= reprocess_info
['max_height']
255 new_size
= (max_width
, max_height
)
257 resize_tool(self
.proc_state
, False, reprocess_info
['resize'],
258 self
.name_builder
.fill('{basename}.medium{ext}'),
259 self
.conversions_subdir
, self
.exif_tags
, new_size
)
261 def media_reprocess(self
, args
):
263 This function handles the all of the reprocessing logic, before calling
264 gmg/submit/lib/run_process_media
266 reprocess_args
= self
._parser
(args
)
269 # Can we actually process the given media as requested?
270 self
._check
_eligible
(entry_args
, reprocess_args
)
272 # Do we want to re-try initial processing?
273 if reprocess_args
.initial_processing
:
274 for id in entry_args
.media_id
:
275 entry
= MediaEntry
.query
.filter_by(id=id).first()
276 run_process_media(entry
)
278 # Are we wanting to resize the thumbnail or medium?
279 elif reprocess_args
.resize
:
281 # reprocess all given media entries
282 for id in entry_args
.media_id
:
283 entry
= MediaEntry
.query
.filter_by(id=id).first()
285 # For now we can only reprocess with the original file
286 if not entry
.media_files
.get('original'):
287 raise Exception(_('The original file for this media entry'
290 reprocess_info
= self
._get
_reprocess
_info
(reprocess_args
)
291 run_process_media(entry
, reprocess_info
=reprocess_info
)
293 # If we are here, they forgot to tell us how to reprocess
295 _log
.warn('You must set either --resize or --initial_processing'
296 ' flag to reprocess an image.')
298 def _get_reprocess_info(self
, args
):
299 """ Returns a dict with the info needed for reprocessing"""
300 reprocess_info
= {'resize': args
.resize
}
303 reprocess_info
['max_width'] = args
.size
[0]
304 reprocess_info
['max_height'] = args
.size
[1]
306 return reprocess_info
309 class CommonImageProcessor(MediaProcessor
):
311 Provides a base for various media processing steps
313 # Common resizing step
314 def resize_step(self
):
318 def _add_width_height_args(cls
, parser
):
320 "--width", default
=None,
322 "Width of the resized image (if not using defaults)"))
324 "--height", default
=None,
326 "Height of the resized image (if not using defaults)"))
329 class InitialProcessor(CommonImageProcessor
):
331 Initial processing step for new images
334 description
= "Initial processing"
337 def media_is_eligible(cls
, media_entry
):
339 Determine if this media type is eligible for processing
341 return media_entry
.state
in (
342 "unprocessed", "failed")
344 ###############################
345 # Command line interface things
346 ###############################
349 def generate_parser(cls
):
350 parser
= argparse
.ArgumentParser(
351 description
=cls
.description
,
354 cls
._add
_width
_height
_args
(parser
)
359 def args_to_request(cls
, args
):
360 return request_from_args(
361 args
, ['width', 'height'])
365 class ImageProcessingManager(ProcessingManager
):
367 super(self
.__class
__, self
).__init
__()
368 self
.add_processor(InitialProcessor
)
371 if __name__
== '__main__':
375 pp
= pprint
.PrettyPrinter()
377 result
= extract_exif(sys
.argv
[1])
378 gps
= get_gps_data(result
)
379 clean
= clean_exif(result
)
380 useful
= get_useful(clean
)