Add priority to the celery tasks
[mediagoblin.git] / mediagoblin / media_types / stl / processing.py
CommitLineData
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 17import argparse
76918e52 18import os
12a10467 19import json
76918e52 20import logging
12a10467 21import subprocess
39c340f2 22import pkg_resources
76918e52
AN
23
24from mediagoblin import mg_globals as mgg
77daec92
RE
25from 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
31from mediagoblin.media_types.stl import model_loader
32
33
34_log = logging.getLogger(__name__)
35SUPPORTED_FILETYPES = ['stl', 'obj']
239296b0 36MEDIA_TYPE = 'mediagoblin.media_types.stl'
76918e52 37
39c340f2
CAW
38BLEND_FILE = pkg_resources.resource_filename(
39 'mediagoblin.media_types.stl',
40 os.path.join(
41 'assets',
42 'blender_render.blend'))
43BLEND_SCRIPT = pkg_resources.resource_filename(
44 'mediagoblin.media_types.stl',
45 os.path.join(
46 'assets',
47 'blender_render.py'))
48
76918e52 49
301da9ca 50def 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
66def 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
79class 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
261class 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
315class 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
366class 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)