Commit | Line | Data |
---|---|---|
93bdab9d | 1 | # GNU MediaGoblin -- federated, autonomous media hosting |
cf29e8a8 | 2 | # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. |
93bdab9d JW |
3 | # |
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. | |
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 Affero General Public License for more details. | |
13 | # | |
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/>. | |
16 | ||
7f342c72 BP |
17 | from __future__ import print_function |
18 | ||
d0e9f843 AL |
19 | try: |
20 | from PIL import Image | |
21 | except ImportError: | |
22 | import Image | |
8e5f9746 | 23 | import os |
92f129b5 | 24 | import logging |
7ac66a3d | 25 | import argparse |
93bdab9d | 26 | |
93bdab9d | 27 | from mediagoblin import mg_globals as mgg |
85ead8ac CAW |
28 | from mediagoblin.processing import ( |
29 | BadMediaFail, FilenameBuilder, | |
d1e9913b | 30 | MediaProcessor, ProcessingManager, |
1cefccc7 | 31 | request_from_args, get_process_filename, |
5fd239fa | 32 | store_public, copy_original) |
a180ca26 | 33 | from mediagoblin.tools.exif import exif_fix_image_orientation, \ |
0f8221dc B |
34 | extract_exif, clean_exif, get_gps_data, get_useful, \ |
35 | exif_image_needs_rotation | |
93bdab9d | 36 | |
92f129b5 JW |
37 | _log = logging.getLogger(__name__) |
38 | ||
d63cc34e JW |
39 | PIL_FILTERS = { |
40 | 'NEAREST': Image.NEAREST, | |
41 | 'BILINEAR': Image.BILINEAR, | |
42 | 'BICUBIC': Image.BICUBIC, | |
43 | 'ANTIALIAS': Image.ANTIALIAS} | |
44 | ||
58a94757 RE |
45 | MEDIA_TYPE = 'mediagoblin.media_types.image' |
46 | ||
deea3f66 | 47 | |
5fd239fa | 48 | def resize_image(entry, resized, keyname, target_name, new_size, |
a2f50198 | 49 | exif_tags, workdir, quality, filter): |
c72d661b JW |
50 | """ |
51 | Store a resized version of an image and return its pathname. | |
063670e9 BS |
52 | |
53 | Arguments: | |
c82a8ba5 | 54 | proc_state -- the processing state for the image to resize |
3b359ddd | 55 | resized -- an image from Image.open() of the original image being resized |
8f88b1f6 E |
56 | keyname -- Under what key to save in the db. |
57 | target_name -- public file path for the new resized image | |
063670e9 BS |
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 | |
a2f50198 RE |
61 | quality -- level of compression used when resizing images |
62 | filter -- One of BICUBIC, BILINEAR, NEAREST, ANTIALIAS | |
063670e9 | 63 | """ |
063670e9 | 64 | resized = exif_fix_image_orientation(resized, exif_tags) # Fix orientation |
7cd7db5a | 65 | |
7cd7db5a | 66 | try: |
a2f50198 | 67 | resize_filter = PIL_FILTERS[filter.upper()] |
7cd7db5a JW |
68 | except KeyError: |
69 | raise Exception('Filter "{0}" not found, choose one of {1}'.format( | |
a2f50198 | 70 | unicode(filter), |
d63cc34e | 71 | u', '.join(PIL_FILTERS.keys()))) |
7cd7db5a JW |
72 | |
73 | resized.thumbnail(new_size, resize_filter) | |
063670e9 | 74 | |
063670e9 | 75 | # Copy the new file to the conversion subdir, then remotely. |
8f88b1f6 | 76 | tmp_resized_filename = os.path.join(workdir, target_name) |
063670e9 | 77 | with file(tmp_resized_filename, 'w') as resized_file: |
a2f50198 | 78 | resized.save(resized_file, quality=quality) |
5fd239fa | 79 | store_public(entry, keyname, tmp_resized_filename, target_name) |
063670e9 | 80 | |
e2b56345 RE |
81 | # store the thumb/medium info |
82 | image_info = {'width': new_size[0], | |
83 | 'height': new_size[1], | |
84 | 'quality': quality, | |
85 | 'filter': filter} | |
86 | ||
87 | entry.set_file_metadata(keyname, **image_info) | |
88 | ||
deea3f66 | 89 | |
5b546d65 | 90 | def resize_tool(entry, |
af51c423 | 91 | force, keyname, orig_file, target_name, |
a2f50198 | 92 | conversions_subdir, exif_tags, quality, filter, new_size=None): |
3e9faf85 | 93 | # Use the default size if new_size was not given |
9a2c66ca RE |
94 | if not new_size: |
95 | max_width = mgg.global_config['media:' + keyname]['max_width'] | |
96 | max_height = mgg.global_config['media:' + keyname]['max_height'] | |
97 | new_size = (max_width, max_height) | |
3e9faf85 | 98 | |
e2b56345 RE |
99 | # If thumb or medium is already the same quality and size, then don't |
100 | # reprocess | |
101 | if _skip_resizing(entry, keyname, new_size, quality, filter): | |
102 | _log.info('{0} of same size and quality already in use, skipping ' | |
103 | 'resizing of media {1}.'.format(keyname, entry.id)) | |
104 | return | |
105 | ||
3b359ddd E |
106 | # If the size of the original file exceeds the specified size for the desized |
107 | # file, a target_name file is created and later associated with the media | |
108 | # entry. | |
109 | # Also created if the file needs rotation, or if forced. | |
110 | try: | |
af51c423 | 111 | im = Image.open(orig_file) |
3b359ddd E |
112 | except IOError: |
113 | raise BadMediaFail() | |
114 | if force \ | |
49db7785 RE |
115 | or im.size[0] > new_size[0]\ |
116 | or im.size[1] > new_size[1]\ | |
3b359ddd | 117 | or exif_image_needs_rotation(exif_tags): |
3b359ddd | 118 | resize_image( |
5fd239fa | 119 | entry, im, unicode(keyname), target_name, |
931fa43f | 120 | tuple(new_size), |
a2f50198 RE |
121 | exif_tags, conversions_subdir, |
122 | quality, filter) | |
3b359ddd E |
123 | |
124 | ||
e2b56345 RE |
125 | def _skip_resizing(entry, keyname, size, quality, filter): |
126 | """ | |
127 | Determines wither the saved thumb or medium is of the same quality and size | |
128 | """ | |
129 | image_info = entry.get_file_metadata(keyname) | |
130 | ||
131 | if not image_info: | |
132 | return False | |
133 | ||
134 | skip = True | |
135 | ||
136 | if image_info.get('width') != size[0]: | |
137 | skip = False | |
138 | ||
139 | elif image_info.get('height') != size[1]: | |
140 | skip = False | |
141 | ||
142 | elif image_info.get('filter') != filter: | |
143 | skip = False | |
144 | ||
145 | elif image_info.get('quality') != quality: | |
146 | skip = False | |
147 | ||
148 | return skip | |
149 | ||
150 | ||
b1a763f6 | 151 | SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg', 'tiff'] |
92f129b5 | 152 | |
c56d4b55 | 153 | |
301da9ca | 154 | def sniff_handler(media_file, filename): |
58a94757 | 155 | _log.info('Sniffing {0}'.format(MEDIA_TYPE)) |
301da9ca CAW |
156 | name, ext = os.path.splitext(filename) |
157 | clean_ext = ext[1:].lower() # Strip the . from ext and make lowercase | |
158 | ||
159 | if clean_ext in SUPPORTED_FILETYPES: | |
160 | _log.info('Found file extension in supported filetypes') | |
161 | return MEDIA_TYPE | |
92f129b5 | 162 | else: |
301da9ca CAW |
163 | _log.debug('Media present, extension not found in {0}'.format( |
164 | SUPPORTED_FILETYPES)) | |
92f129b5 | 165 | |
58a94757 | 166 | return None |
ec4261a4 | 167 | |
c56d4b55 | 168 | |
85ead8ac CAW |
169 | class CommonImageProcessor(MediaProcessor): |
170 | """ | |
171 | Provides a base for various media processing steps | |
172 | """ | |
1cefccc7 RE |
173 | # list of acceptable file keys in order of prefrence for reprocessing |
174 | acceptable_files = ['original', 'medium'] | |
175 | ||
5fd239fa CAW |
176 | def common_setup(self): |
177 | """ | |
178 | Set up the workbench directory and pull down the original file | |
179 | """ | |
93874d0a RE |
180 | self.image_config = mgg.global_config['plugins'][ |
181 | 'mediagoblin.media_types.image'] | |
a2f50198 | 182 | |
5fd239fa | 183 | ## @@: Should this be two functions? |
eb372949 CAW |
184 | # Conversions subdirectory to avoid collisions |
185 | self.conversions_subdir = os.path.join( | |
0485e9c8 | 186 | self.workbench.dir, 'conversions') |
5fd239fa | 187 | os.mkdir(self.conversions_subdir) |
eb372949 | 188 | |
1cefccc7 RE |
189 | # Pull down and set up the processing file |
190 | self.process_filename = get_process_filename( | |
191 | self.entry, self.workbench, self.acceptable_files) | |
192 | self.name_builder = FilenameBuilder(self.process_filename) | |
2fa7b7f8 | 193 | |
5b546d65 | 194 | # Exif extraction |
1cefccc7 | 195 | self.exif_tags = extract_exif(self.process_filename) |
5b546d65 | 196 | |
a2f50198 RE |
197 | def generate_medium_if_applicable(self, size=None, quality=None, |
198 | filter=None): | |
199 | if not quality: | |
200 | quality = self.image_config['quality'] | |
201 | if not filter: | |
202 | filter = self.image_config['resize_filter'] | |
203 | ||
1cefccc7 | 204 | resize_tool(self.entry, False, 'medium', self.process_filename, |
5fd239fa | 205 | self.name_builder.fill('{basename}.medium{ext}'), |
a2f50198 RE |
206 | self.conversions_subdir, self.exif_tags, quality, |
207 | filter, size) | |
208 | ||
209 | def generate_thumb(self, size=None, quality=None, filter=None): | |
210 | if not quality: | |
211 | quality = self.image_config['quality'] | |
212 | if not filter: | |
63021eb6 | 213 | filter = self.image_config['resize_filter'] |
2fa7b7f8 | 214 | |
1cefccc7 | 215 | resize_tool(self.entry, True, 'thumb', self.process_filename, |
5fd239fa | 216 | self.name_builder.fill('{basename}.thumbnail{ext}'), |
a2f50198 RE |
217 | self.conversions_subdir, self.exif_tags, quality, |
218 | filter, size) | |
5fd239fa CAW |
219 | |
220 | def copy_original(self): | |
221 | copy_original( | |
1cefccc7 | 222 | self.entry, self.process_filename, |
5fd239fa | 223 | self.name_builder.fill('{basename}{ext}')) |
2fa7b7f8 CAW |
224 | |
225 | def extract_metadata(self): | |
5fd239fa | 226 | # Is there any GPS data |
5b546d65 | 227 | gps_data = get_gps_data(self.exif_tags) |
5fd239fa CAW |
228 | |
229 | # Insert exif data into database | |
5b546d65 | 230 | exif_all = clean_exif(self.exif_tags) |
5fd239fa CAW |
231 | |
232 | if len(exif_all): | |
233 | self.entry.media_data_init(exif_all=exif_all) | |
234 | ||
235 | if len(gps_data): | |
236 | for key in list(gps_data.keys()): | |
237 | gps_data['gps_' + key] = gps_data.pop(key) | |
238 | self.entry.media_data_init(**gps_data) | |
2fa7b7f8 | 239 | |
85ead8ac CAW |
240 | |
241 | class InitialProcessor(CommonImageProcessor): | |
242 | """ | |
243 | Initial processing step for new images | |
244 | """ | |
245 | name = "initial" | |
246 | description = "Initial processing" | |
247 | ||
248 | @classmethod | |
7584080b | 249 | def media_is_eligible(cls, entry=None, state=None): |
85ead8ac CAW |
250 | """ |
251 | Determine if this media type is eligible for processing | |
252 | """ | |
7584080b RE |
253 | if not state: |
254 | state = entry.state | |
255 | return state in ( | |
85ead8ac CAW |
256 | "unprocessed", "failed") |
257 | ||
258 | ############################### | |
259 | # Command line interface things | |
260 | ############################### | |
261 | ||
262 | @classmethod | |
55a10fef | 263 | def generate_parser(cls): |
85ead8ac | 264 | parser = argparse.ArgumentParser( |
55a10fef CAW |
265 | description=cls.description, |
266 | prog=cls.name) | |
85ead8ac | 267 | |
5b546d65 CAW |
268 | parser.add_argument( |
269 | '--size', | |
270 | nargs=2, | |
271 | metavar=('max_width', 'max_height'), | |
272 | type=int) | |
273 | ||
274 | parser.add_argument( | |
275 | '--thumb-size', | |
276 | nargs=2, | |
58350141 | 277 | metavar=('max_width', 'max_height'), |
5b546d65 | 278 | type=int) |
85ead8ac | 279 | |
a2f50198 RE |
280 | parser.add_argument( |
281 | '--filter', | |
282 | choices=['BICUBIC', 'BILINEAR', 'NEAREST', 'ANTIALIAS']) | |
283 | ||
284 | parser.add_argument( | |
285 | '--quality', | |
286 | type=int, | |
287 | help='level of compression used when resizing images') | |
288 | ||
85ead8ac CAW |
289 | return parser |
290 | ||
291 | @classmethod | |
55a10fef | 292 | def args_to_request(cls, args): |
d1e9913b | 293 | return request_from_args( |
a2f50198 | 294 | args, ['size', 'thumb_size', 'filter', 'quality']) |
85ead8ac | 295 | |
a2f50198 | 296 | def process(self, size=None, thumb_size=None, quality=None, filter=None): |
5fd239fa | 297 | self.common_setup() |
a2f50198 RE |
298 | self.generate_medium_if_applicable(size=size, filter=filter, |
299 | quality=quality) | |
63021eb6 | 300 | self.generate_thumb(size=thumb_size, filter=filter, quality=quality) |
916db96e | 301 | self.copy_original() |
2fa7b7f8 | 302 | self.extract_metadata() |
58350141 RE |
303 | self.delete_queue_file() |
304 | ||
305 | ||
306 | class Resizer(CommonImageProcessor): | |
307 | """ | |
308 | Resizing process steps for processed media | |
309 | """ | |
310 | name = 'resize' | |
311 | description = 'Resize image' | |
3225008f | 312 | thumb_size = 'size' |
58350141 RE |
313 | |
314 | @classmethod | |
7584080b | 315 | def media_is_eligible(cls, entry=None, state=None): |
58350141 RE |
316 | """ |
317 | Determine if this media type is eligible for processing | |
318 | """ | |
7584080b RE |
319 | if not state: |
320 | state = entry.state | |
321 | return state in 'processed' | |
58350141 RE |
322 | |
323 | ############################### | |
324 | # Command line interface things | |
325 | ############################### | |
326 | ||
327 | @classmethod | |
328 | def generate_parser(cls): | |
329 | parser = argparse.ArgumentParser( | |
330 | description=cls.description, | |
331 | prog=cls.name) | |
332 | ||
333 | parser.add_argument( | |
334 | '--size', | |
335 | nargs=2, | |
336 | metavar=('max_width', 'max_height'), | |
337 | type=int) | |
338 | ||
a2f50198 RE |
339 | parser.add_argument( |
340 | '--filter', | |
341 | choices=['BICUBIC', 'BILINEAR', 'NEAREST', 'ANTIALIAS']) | |
342 | ||
343 | parser.add_argument( | |
344 | '--quality', | |
345 | type=int, | |
346 | help='level of compression used when resizing images') | |
347 | ||
58350141 RE |
348 | parser.add_argument( |
349 | 'file', | |
350 | choices=['medium', 'thumb']) | |
351 | ||
352 | return parser | |
353 | ||
354 | @classmethod | |
355 | def args_to_request(cls, args): | |
356 | return request_from_args( | |
a2f50198 | 357 | args, ['size', 'file', 'quality', 'filter']) |
58350141 | 358 | |
a2f50198 | 359 | def process(self, file, size=None, filter=None, quality=None): |
58350141 RE |
360 | self.common_setup() |
361 | if file == 'medium': | |
a2f50198 RE |
362 | self.generate_medium_if_applicable(size=size, filter=filter, |
363 | quality=quality) | |
58350141 | 364 | elif file == 'thumb': |
a2f50198 | 365 | self.generate_thumb(size=size, filter=filter, quality=quality) |
2fa7b7f8 | 366 | |
85ead8ac CAW |
367 | |
368 | class ImageProcessingManager(ProcessingManager): | |
369 | def __init__(self): | |
370 | super(self.__class__, self).__init__() | |
371 | self.add_processor(InitialProcessor) | |
58350141 | 372 | self.add_processor(Resizer) |
85ead8ac CAW |
373 | |
374 | ||
e8e444a8 JW |
375 | if __name__ == '__main__': |
376 | import sys | |
377 | import pprint | |
378 | ||
379 | pp = pprint.PrettyPrinter() | |
380 | ||
381 | result = extract_exif(sys.argv[1]) | |
382 | gps = get_gps_data(result) | |
a180ca26 JW |
383 | clean = clean_exif(result) |
384 | useful = get_useful(clean) | |
e8e444a8 | 385 | |
7f342c72 | 386 | print(pp.pprint(clean)) |