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 | ||
d0e9f843 AL |
17 | try: |
18 | from PIL import Image | |
19 | except ImportError: | |
20 | import Image | |
8e5f9746 | 21 | import os |
92f129b5 | 22 | import logging |
93bdab9d | 23 | |
93bdab9d | 24 | from mediagoblin import mg_globals as mgg |
8f88b1f6 | 25 | from mediagoblin.processing import BadMediaFail, FilenameBuilder |
a180ca26 | 26 | from mediagoblin.tools.exif import exif_fix_image_orientation, \ |
0f8221dc B |
27 | extract_exif, clean_exif, get_gps_data, get_useful, \ |
28 | exif_image_needs_rotation | |
93bdab9d | 29 | |
92f129b5 JW |
30 | _log = logging.getLogger(__name__) |
31 | ||
d63cc34e JW |
32 | PIL_FILTERS = { |
33 | 'NEAREST': Image.NEAREST, | |
34 | 'BILINEAR': Image.BILINEAR, | |
35 | 'BICUBIC': Image.BICUBIC, | |
36 | 'ANTIALIAS': Image.ANTIALIAS} | |
37 | ||
58a94757 RE |
38 | MEDIA_TYPE = 'mediagoblin.media_types.image' |
39 | ||
deea3f66 | 40 | |
8f88b1f6 | 41 | def resize_image(proc_state, resized, keyname, target_name, new_size, |
3b359ddd | 42 | exif_tags, workdir): |
c72d661b JW |
43 | """ |
44 | Store a resized version of an image and return its pathname. | |
063670e9 BS |
45 | |
46 | Arguments: | |
c82a8ba5 | 47 | proc_state -- the processing state for the image to resize |
3b359ddd | 48 | resized -- an image from Image.open() of the original image being resized |
8f88b1f6 E |
49 | keyname -- Under what key to save in the db. |
50 | target_name -- public file path for the new resized image | |
063670e9 BS |
51 | exif_tags -- EXIF data for the original image |
52 | workdir -- directory path for storing converted image files | |
53 | new_size -- 2-tuple size for the resized image | |
063670e9 | 54 | """ |
dc1ec36e E |
55 | config = mgg.global_config['media_type:mediagoblin.media_types.image'] |
56 | ||
063670e9 | 57 | resized = exif_fix_image_orientation(resized, exif_tags) # Fix orientation |
7cd7db5a | 58 | |
dc1ec36e | 59 | filter_config = config['resize_filter'] |
7cd7db5a | 60 | try: |
d63cc34e | 61 | resize_filter = PIL_FILTERS[filter_config.upper()] |
7cd7db5a JW |
62 | except KeyError: |
63 | raise Exception('Filter "{0}" not found, choose one of {1}'.format( | |
64 | unicode(filter_config), | |
d63cc34e | 65 | u', '.join(PIL_FILTERS.keys()))) |
7cd7db5a JW |
66 | |
67 | resized.thumbnail(new_size, resize_filter) | |
063670e9 | 68 | |
063670e9 | 69 | # Copy the new file to the conversion subdir, then remotely. |
8f88b1f6 | 70 | tmp_resized_filename = os.path.join(workdir, target_name) |
063670e9 | 71 | with file(tmp_resized_filename, 'w') as resized_file: |
dc1ec36e | 72 | resized.save(resized_file, quality=config['quality']) |
8f88b1f6 | 73 | proc_state.store_public(keyname, tmp_resized_filename, target_name) |
063670e9 | 74 | |
deea3f66 | 75 | |
3e9faf85 | 76 | def resize_tool(proc_state, force, keyname, target_name, |
9a2c66ca | 77 | conversions_subdir, exif_tags, new_size=None): |
45b20dce | 78 | # filename -- the filename of the original image being resized |
3e9faf85 RE |
79 | filename = proc_state.get_orig_filename() |
80 | ||
81 | # Use the default size if new_size was not given | |
9a2c66ca RE |
82 | if not new_size: |
83 | max_width = mgg.global_config['media:' + keyname]['max_width'] | |
84 | max_height = mgg.global_config['media:' + keyname]['max_height'] | |
85 | new_size = (max_width, max_height) | |
3e9faf85 | 86 | |
3b359ddd E |
87 | # If the size of the original file exceeds the specified size for the desized |
88 | # file, a target_name file is created and later associated with the media | |
89 | # entry. | |
90 | # Also created if the file needs rotation, or if forced. | |
91 | try: | |
92 | im = Image.open(filename) | |
93 | except IOError: | |
94 | raise BadMediaFail() | |
95 | if force \ | |
49db7785 RE |
96 | or im.size[0] > new_size[0]\ |
97 | or im.size[1] > new_size[1]\ | |
3b359ddd | 98 | or exif_image_needs_rotation(exif_tags): |
3b359ddd | 99 | resize_image( |
8f88b1f6 | 100 | proc_state, im, unicode(keyname), target_name, |
9a2c66ca | 101 | new_size, |
3b359ddd | 102 | exif_tags, conversions_subdir) |
3b359ddd E |
103 | |
104 | ||
b1a763f6 | 105 | SUPPORTED_FILETYPES = ['png', 'gif', 'jpg', 'jpeg', 'tiff'] |
92f129b5 | 106 | |
c56d4b55 | 107 | |
ec4261a4 | 108 | def sniff_handler(media_file, **kw): |
58a94757 | 109 | _log.info('Sniffing {0}'.format(MEDIA_TYPE)) |
e2caf574 | 110 | if kw.get('media') is not None: # That's a double negative! |
92f129b5 JW |
111 | name, ext = os.path.splitext(kw['media'].filename) |
112 | clean_ext = ext[1:].lower() # Strip the . from ext and make lowercase | |
113 | ||
92f129b5 JW |
114 | if clean_ext in SUPPORTED_FILETYPES: |
115 | _log.info('Found file extension in supported filetypes') | |
58a94757 | 116 | return MEDIA_TYPE |
92f129b5 | 117 | else: |
10085b77 | 118 | _log.debug('Media present, extension not found in {0}'.format( |
92f129b5 JW |
119 | SUPPORTED_FILETYPES)) |
120 | else: | |
121 | _log.warning('Need additional information (keyword argument \'media\')' | |
122 | ' to be able to handle sniffing') | |
123 | ||
58a94757 | 124 | return None |
ec4261a4 | 125 | |
c56d4b55 | 126 | |
9a2c66ca | 127 | def process_image(proc_state, reprocess_info=None): |
45ab3e07 SS |
128 | """Code to process an image. Will be run by celery. |
129 | ||
130 | A Workbench() represents a local tempory dir. It is automatically | |
131 | cleaned up when this function exits. | |
93bdab9d | 132 | """ |
3e9faf85 RE |
133 | def init(self, proc_state): |
134 | self.proc_state = proc_state | |
135 | self.entry = proc_state.entry | |
136 | self.workbench = proc_state.workbench | |
49db7785 | 137 | |
3e9faf85 RE |
138 | # Conversions subdirectory to avoid collisions |
139 | self.conversions_subdir = os.path.join( | |
140 | self.workbench.dir, 'convirsions') | |
93b14fc3 | 141 | |
3e9faf85 RE |
142 | self.orig_filename = proc_state.get_orig_filename() |
143 | self.name_builder = FilenameBuilder(self.orig_filename) | |
9a2c66ca | 144 | |
3e9faf85 RE |
145 | # Exif extraction |
146 | self.exif_tags = extract_exif(self.orig_filename) | |
147 | ||
148 | os.mkdir(self.conversions_subdir) | |
8e5f9746 | 149 | |
3e9faf85 RE |
150 | def initial_processing(self): |
151 | # Is there any GPS data | |
152 | gps_data = get_gps_data(self.exif_tags) | |
e8e444a8 | 153 | |
3e9faf85 RE |
154 | # Always create a small thumbnail |
155 | resize_tool(self.proc_state, True, 'thumb', self.orig_filename, | |
156 | self.name_builder.fill('{basename}.thumbnail{ext}'), | |
157 | self.conversions_subdir, self.exif_tags) | |
9a2c66ca RE |
158 | |
159 | # Possibly create a medium | |
3e9faf85 RE |
160 | resize_tool(self.proc_state, False, 'medium', self.orig_filename, |
161 | self.name_builder.fill('{basename}.medium{ext}'), | |
162 | self.conversions_subdir, self.exif_tags) | |
9a2c66ca RE |
163 | |
164 | # Copy our queued local workbench to its final destination | |
3e9faf85 | 165 | self.proc_state.copy_original(self.name_builder.fill('{basename}{ext}')) |
9a2c66ca RE |
166 | |
167 | # Remove queued media file from storage and database | |
3e9faf85 | 168 | self.proc_state.delete_queue_file() |
3b359ddd | 169 | |
9a2c66ca | 170 | # Insert exif data into database |
3e9faf85 | 171 | exif_all = clean_exif(self.exif_tags) |
93bdab9d | 172 | |
9a2c66ca | 173 | if len(exif_all): |
3e9faf85 | 174 | self.entry.media_data_init(exif_all=exif_all) |
93bdab9d | 175 | |
9a2c66ca RE |
176 | if len(gps_data): |
177 | for key in list(gps_data.keys()): | |
178 | gps_data['gps_' + key] = gps_data.pop(key) | |
3e9faf85 | 179 | self.entry.media_data_init(**gps_data) |
e8e444a8 | 180 | |
3e9faf85 RE |
181 | def reprocess(self, reprocess_info): |
182 | new_size = None | |
763ef5b7 | 183 | |
3e9faf85 RE |
184 | # Did they specify a size? |
185 | if reprocess_info.get('max_width'): | |
186 | max_width = reprocess_info['max_width'] | |
187 | max_height = reprocess_info['max_height'] | |
9a2c66ca | 188 | |
3e9faf85 | 189 | new_size = (max_width, max_height) |
93bdab9d | 190 | |
3e9faf85 RE |
191 | resize_tool(self.proc_state, False, reprocess_info['resize'], |
192 | self.name_builder.fill('{basename}.medium{ext}'), | |
193 | self.conversions_subdir, self.exif_tags, new_size) | |
e8e444a8 | 194 | |
e8e444a8 JW |
195 | if __name__ == '__main__': |
196 | import sys | |
197 | import pprint | |
198 | ||
199 | pp = pprint.PrettyPrinter() | |
200 | ||
201 | result = extract_exif(sys.argv[1]) | |
202 | gps = get_gps_data(result) | |
a180ca26 JW |
203 | clean = clean_exif(result) |
204 | useful = get_useful(clean) | |
e8e444a8 | 205 | |
e8e444a8 | 206 | print pp.pprint( |
a180ca26 | 207 | clean) |