Commit | Line | Data |
---|---|---|
76918e52 AN |
1 | # GNU MediaGoblin -- federated, autonomous media hosting |
2 | # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS. | |
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 | ||
77daec92 | 17 | import argparse |
76918e52 | 18 | import os |
12a10467 | 19 | import json |
76918e52 | 20 | import logging |
12a10467 | 21 | import subprocess |
39c340f2 | 22 | import pkg_resources |
76918e52 AN |
23 | |
24 | from mediagoblin import mg_globals as mgg | |
77daec92 RE |
25 | from mediagoblin.processing import ( |
26 | FilenameBuilder, MediaProcessor, | |
27 | ProcessingManager, request_from_args, | |
1cefccc7 | 28 | get_process_filename, store_public, |
77daec92 | 29 | copy_original) |
76918e52 AN |
30 | |
31 | from mediagoblin.media_types.stl import model_loader | |
32 | ||
33 | ||
34 | _log = logging.getLogger(__name__) | |
35 | SUPPORTED_FILETYPES = ['stl', 'obj'] | |
239296b0 | 36 | MEDIA_TYPE = 'mediagoblin.media_types.stl' |
76918e52 | 37 | |
39c340f2 CAW |
38 | BLEND_FILE = pkg_resources.resource_filename( |
39 | 'mediagoblin.media_types.stl', | |
40 | os.path.join( | |
41 | 'assets', | |
42 | 'blender_render.blend')) | |
43 | BLEND_SCRIPT = pkg_resources.resource_filename( | |
44 | 'mediagoblin.media_types.stl', | |
45 | os.path.join( | |
46 | 'assets', | |
47 | 'blender_render.py')) | |
48 | ||
76918e52 | 49 | |
301da9ca | 50 | def sniff_handler(media_file, filename): |
239296b0 | 51 | _log.info('Sniffing {0}'.format(MEDIA_TYPE)) |
301da9ca CAW |
52 | |
53 | name, ext = os.path.splitext(filename) | |
54 | clean_ext = ext[1:].lower() | |
55 | ||
56 | if clean_ext in SUPPORTED_FILETYPES: | |
57 | _log.info('Found file extension in supported filetypes') | |
58 | return MEDIA_TYPE | |
76918e52 | 59 | else: |
301da9ca CAW |
60 | _log.debug('Media present, extension not found in {0}'.format( |
61 | SUPPORTED_FILETYPES)) | |
76918e52 | 62 | |
239296b0 | 63 | return None |
76918e52 AN |
64 | |
65 | ||
12a10467 AN |
66 | def blender_render(config): |
67 | """ | |
68 | Called to prerender a model. | |
69 | """ | |
12a10467 | 70 | env = {"RENDER_SETUP" : json.dumps(config), "DISPLAY":":0"} |
39c340f2 CAW |
71 | subprocess.call( |
72 | ["blender", | |
73 | "-b", BLEND_FILE, | |
74 | "-F", "JPEG", | |
75 | "-P", BLEND_SCRIPT], | |
76 | env=env) | |
12a10467 AN |
77 | |
78 | ||
77daec92 RE |
79 | class CommonStlProcessor(MediaProcessor): |
80 | """ | |
81 | Provides a common base for various stl processing steps | |
76918e52 | 82 | """ |
1cefccc7 | 83 | acceptable_files = ['original'] |
12a10467 | 84 | |
77daec92 | 85 | def common_setup(self): |
1cefccc7 RE |
86 | # Pull down and set up the processing file |
87 | self.process_filename = get_process_filename( | |
88 | self.entry, self.workbench, self.acceptable_files) | |
89 | self.name_builder = FilenameBuilder(self.process_filename) | |
77daec92 RE |
90 | |
91 | self._set_ext() | |
92 | self._set_model() | |
93 | self._set_greatest() | |
94 | ||
95 | def _set_ext(self): | |
61b3fc50 | 96 | ext = self.name_builder.ext[1:] |
77daec92 RE |
97 | |
98 | if not ext: | |
99 | ext = None | |
100 | ||
101 | self.ext = ext | |
102 | ||
103 | def _set_model(self): | |
104 | """ | |
105 | Attempt to parse the model file and divine some useful | |
106 | information about it. | |
107 | """ | |
1cefccc7 | 108 | with open(self.process_filename, 'rb') as model_file: |
77daec92 RE |
109 | self.model = model_loader.auto_detect(model_file, self.ext) |
110 | ||
111 | def _set_greatest(self): | |
112 | greatest = [self.model.width, self.model.height, self.model.depth] | |
113 | greatest.sort() | |
114 | self.greatest = greatest[-1] | |
115 | ||
116 | def copy_original(self): | |
117 | copy_original( | |
1cefccc7 | 118 | self.entry, self.process_filename, |
77daec92 RE |
119 | self.name_builder.fill('{basename}{ext}')) |
120 | ||
121 | def _snap(self, keyname, name, camera, size, project="ORTHO"): | |
122 | filename = self.name_builder.fill(name) | |
123 | workbench_path = self.workbench.joinpath(filename) | |
12a10467 | 124 | shot = { |
1cefccc7 | 125 | "model_path": self.process_filename, |
77daec92 | 126 | "model_ext": self.ext, |
39c340f2 | 127 | "camera_coord": camera, |
77daec92 RE |
128 | "camera_focus": self.model.average, |
129 | "camera_clip": self.greatest*10, | |
130 | "greatest": self.greatest, | |
39c340f2 | 131 | "projection": project, |
77daec92 RE |
132 | "width": size[0], |
133 | "height": size[1], | |
e7e43534 | 134 | "out_file": workbench_path, |
12a10467 | 135 | } |
12a10467 | 136 | blender_render(shot) |
e7e43534 CAW |
137 | |
138 | # make sure the image rendered to the workbench path | |
139 | assert os.path.exists(workbench_path) | |
140 | ||
141 | # copy it up! | |
77daec92 RE |
142 | store_public(self.entry, keyname, workbench_path, filename) |
143 | ||
b08d2c36 RE |
144 | def _skip_processing(self, keyname, **kwargs): |
145 | file_metadata = self.entry.get_file_metadata(keyname) | |
146 | ||
147 | if not file_metadata: | |
148 | return False | |
149 | skip = True | |
150 | ||
151 | if keyname == 'thumb': | |
152 | if kwargs.get('thumb_size') != file_metadata.get('thumb_size'): | |
153 | skip = False | |
154 | else: | |
155 | if kwargs.get('size') != file_metadata.get('size'): | |
156 | skip = False | |
157 | ||
158 | return skip | |
159 | ||
77daec92 RE |
160 | def generate_thumb(self, thumb_size=None): |
161 | if not thumb_size: | |
162 | thumb_size = (mgg.global_config['media:thumb']['max_width'], | |
163 | mgg.global_config['media:thumb']['max_height']) | |
164 | ||
b08d2c36 RE |
165 | if self._skip_processing('thumb', thumb_size=thumb_size): |
166 | return | |
167 | ||
77daec92 RE |
168 | self._snap( |
169 | "thumb", | |
170 | "{basename}.thumb.jpg", | |
171 | [0, self.greatest*-1.5, self.greatest], | |
172 | thumb_size, | |
173 | project="PERSP") | |
174 | ||
b08d2c36 RE |
175 | self.entry.set_file_metadata('thumb', thumb_size=thumb_size) |
176 | ||
77daec92 RE |
177 | def generate_perspective(self, size=None): |
178 | if not size: | |
179 | size = (mgg.global_config['media:medium']['max_width'], | |
180 | mgg.global_config['media:medium']['max_height']) | |
181 | ||
b08d2c36 RE |
182 | if self._skip_processing('perspective', size=size): |
183 | return | |
184 | ||
77daec92 RE |
185 | self._snap( |
186 | "perspective", | |
187 | "{basename}.perspective.jpg", | |
188 | [0, self.greatest*-1.5, self.greatest], | |
189 | size, | |
190 | project="PERSP") | |
191 | ||
b08d2c36 RE |
192 | self.entry.set_file_metadata('perspective', size=size) |
193 | ||
77daec92 RE |
194 | def generate_topview(self, size=None): |
195 | if not size: | |
196 | size = (mgg.global_config['media:medium']['max_width'], | |
197 | mgg.global_config['media:medium']['max_height']) | |
198 | ||
b08d2c36 RE |
199 | if self._skip_processing('top', size=size): |
200 | return | |
201 | ||
77daec92 RE |
202 | self._snap( |
203 | "top", | |
204 | "{basename}.top.jpg", | |
205 | [self.model.average[0], self.model.average[1], | |
206 | self.greatest*2], | |
207 | size) | |
208 | ||
b08d2c36 RE |
209 | self.entry.set_file_metadata('top', size=size) |
210 | ||
77daec92 RE |
211 | def generate_frontview(self, size=None): |
212 | if not size: | |
213 | size = (mgg.global_config['media:medium']['max_width'], | |
214 | mgg.global_config['media:medium']['max_height']) | |
215 | ||
b08d2c36 RE |
216 | if self._skip_processing('front', size=size): |
217 | return | |
218 | ||
77daec92 RE |
219 | self._snap( |
220 | "front", | |
221 | "{basename}.front.jpg", | |
222 | [self.model.average[0], self.greatest*-2, | |
223 | self.model.average[2]], | |
224 | size) | |
225 | ||
b08d2c36 RE |
226 | self.entry.set_file_metadata('front', size=size) |
227 | ||
77daec92 RE |
228 | def generate_sideview(self, size=None): |
229 | if not size: | |
230 | size = (mgg.global_config['media:medium']['max_width'], | |
231 | mgg.global_config['media:medium']['max_height']) | |
232 | ||
b08d2c36 RE |
233 | if self._skip_processing('side', size=size): |
234 | return | |
235 | ||
77daec92 RE |
236 | self._snap( |
237 | "side", | |
238 | "{basename}.side.jpg", | |
239 | [self.greatest*-2, self.model.average[1], | |
240 | self.model.average[2]], | |
241 | size) | |
242 | ||
b08d2c36 RE |
243 | self.entry.set_file_metadata('side', size=size) |
244 | ||
77daec92 RE |
245 | def store_dimensions(self): |
246 | """ | |
247 | Put model dimensions into the database | |
248 | """ | |
249 | dimensions = { | |
250 | "center_x": self.model.average[0], | |
251 | "center_y": self.model.average[1], | |
252 | "center_z": self.model.average[2], | |
253 | "width": self.model.width, | |
254 | "height": self.model.height, | |
255 | "depth": self.model.depth, | |
256 | "file_type": self.ext, | |
257 | } | |
258 | self.entry.media_data_init(**dimensions) | |
259 | ||
260 | ||
261 | class InitialProcessor(CommonStlProcessor): | |
262 | """ | |
263 | Initial processing step for new stls | |
264 | """ | |
265 | name = "initial" | |
266 | description = "Initial processing" | |
267 | ||
268 | @classmethod | |
269 | def media_is_eligible(cls, entry=None, state=None): | |
270 | """ | |
271 | Determine if this media type is eligible for processing | |
272 | """ | |
273 | if not state: | |
274 | state = entry.state | |
275 | return state in ( | |
276 | "unprocessed", "failed") | |
277 | ||
278 | @classmethod | |
279 | def generate_parser(cls): | |
280 | parser = argparse.ArgumentParser( | |
281 | description=cls.description, | |
282 | prog=cls.name) | |
283 | ||
284 | parser.add_argument( | |
285 | '--size', | |
286 | nargs=2, | |
287 | metavar=('max_width', 'max_height'), | |
288 | type=int) | |
289 | ||
290 | parser.add_argument( | |
63021eb6 | 291 | '--thumb_size', |
77daec92 RE |
292 | nargs=2, |
293 | metavar=('max_width', 'max_height'), | |
294 | type=int) | |
295 | ||
296 | return parser | |
297 | ||
298 | @classmethod | |
299 | def args_to_request(cls, args): | |
300 | return request_from_args( | |
301 | args, ['size', 'thumb_size']) | |
302 | ||
303 | def process(self, size=None, thumb_size=None): | |
304 | self.common_setup() | |
305 | self.generate_thumb(thumb_size=thumb_size) | |
306 | self.generate_perspective(size=size) | |
307 | self.generate_topview(size=size) | |
308 | self.generate_frontview(size=size) | |
309 | self.generate_sideview(size=size) | |
310 | self.store_dimensions() | |
311 | self.copy_original() | |
312 | self.delete_queue_file() | |
313 | ||
314 | ||
a3cc93c6 RE |
315 | class Resizer(CommonStlProcessor): |
316 | """ | |
317 | Resizing process steps for processed stls | |
318 | """ | |
319 | name = 'resize' | |
320 | description = 'Resize thumbnail and mediums' | |
3225008f | 321 | thumb_size = 'size' |
a3cc93c6 RE |
322 | |
323 | @classmethod | |
324 | def media_is_eligible(cls, entry=None, state=None): | |
325 | """ | |
326 | Determine if this media type is eligible for processing | |
327 | """ | |
328 | if not state: | |
329 | state = entry.state | |
330 | return state in 'processed' | |
331 | ||
332 | @classmethod | |
333 | def generate_parser(cls): | |
334 | parser = argparse.ArgumentParser( | |
335 | description=cls.description, | |
336 | prog=cls.name) | |
337 | ||
338 | parser.add_argument( | |
339 | '--size', | |
340 | nargs=2, | |
341 | metavar=('max_width', 'max_height'), | |
342 | type=int) | |
343 | ||
344 | parser.add_argument( | |
345 | 'file', | |
346 | choices=['medium', 'thumb']) | |
347 | ||
348 | return parser | |
349 | ||
350 | @classmethod | |
351 | def args_to_request(cls, args): | |
352 | return request_from_args( | |
353 | args, ['size', 'file']) | |
354 | ||
355 | def process(self, file, size=None): | |
356 | self.common_setup() | |
357 | if file == 'medium': | |
358 | self.generate_perspective(size=size) | |
359 | self.generate_topview(size=size) | |
360 | self.generate_frontview(size=size) | |
361 | self.generate_sideview(size=size) | |
362 | elif file == 'thumb': | |
63021eb6 | 363 | self.generate_thumb(thumb_size=size) |
a3cc93c6 RE |
364 | |
365 | ||
77daec92 RE |
366 | class StlProcessingManager(ProcessingManager): |
367 | def __init__(self): | |
1a2982d6 | 368 | super(StlProcessingManager, self).__init__() |
77daec92 | 369 | self.add_processor(InitialProcessor) |
a3cc93c6 | 370 | self.add_processor(Resizer) |
c62181f4 | 371 | |
25ecdec9 | 372 | def workflow(self, entry, manager, feed_url, reprocess_action, |
373 | reprocess_info=None): | |
c62181f4 | 374 | ProcessMedia().apply_async( |
375 | [entry.id, feed_url, reprocess_action, reprocess_info], {}, | |
376 | task_id=entry.queued_task_id) |