| 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 | |
| 17 | import os |
| 18 | import json |
| 19 | import logging |
| 20 | import subprocess |
| 21 | import pkg_resources |
| 22 | |
| 23 | from mediagoblin import mg_globals as mgg |
| 24 | from mediagoblin.decorators import get_workbench |
| 25 | from mediagoblin.processing import create_pub_filepath, \ |
| 26 | FilenameBuilder |
| 27 | |
| 28 | from mediagoblin.media_types.stl import model_loader |
| 29 | |
| 30 | |
| 31 | _log = logging.getLogger(__name__) |
| 32 | SUPPORTED_FILETYPES = ['stl', 'obj'] |
| 33 | |
| 34 | BLEND_FILE = pkg_resources.resource_filename( |
| 35 | 'mediagoblin.media_types.stl', |
| 36 | os.path.join( |
| 37 | 'assets', |
| 38 | 'blender_render.blend')) |
| 39 | BLEND_SCRIPT = pkg_resources.resource_filename( |
| 40 | 'mediagoblin.media_types.stl', |
| 41 | os.path.join( |
| 42 | 'assets', |
| 43 | 'blender_render.py')) |
| 44 | |
| 45 | |
| 46 | def sniff_handler(media_file, **kw): |
| 47 | if kw.get('media') is not None: |
| 48 | name, ext = os.path.splitext(kw['media'].filename) |
| 49 | clean_ext = ext[1:].lower() |
| 50 | |
| 51 | if clean_ext in SUPPORTED_FILETYPES: |
| 52 | _log.info('Found file extension in supported filetypes') |
| 53 | return True |
| 54 | else: |
| 55 | _log.debug('Media present, extension not found in {0}'.format( |
| 56 | SUPPORTED_FILETYPES)) |
| 57 | else: |
| 58 | _log.warning('Need additional information (keyword argument \'media\')' |
| 59 | ' to be able to handle sniffing') |
| 60 | |
| 61 | return False |
| 62 | |
| 63 | |
| 64 | def blender_render(config): |
| 65 | """ |
| 66 | Called to prerender a model. |
| 67 | """ |
| 68 | arg_string = "blender -b blender_render.blend -F " |
| 69 | arg_string +="JPEG -P blender_render.py" |
| 70 | env = {"RENDER_SETUP" : json.dumps(config), "DISPLAY":":0"} |
| 71 | subprocess.call( |
| 72 | ["blender", |
| 73 | "-b", BLEND_FILE, |
| 74 | "-F", "JPEG", |
| 75 | "-P", BLEND_SCRIPT], |
| 76 | env=env) |
| 77 | |
| 78 | |
| 79 | @get_workbench |
| 80 | def process_stl(entry, workbench=None): |
| 81 | """Code to process an stl or obj model. Will be run by celery. |
| 82 | |
| 83 | A Workbench() represents a local tempory dir. It is automatically |
| 84 | cleaned up when this function exits. |
| 85 | """ |
| 86 | queued_filepath = entry.queued_media_file |
| 87 | queued_filename = workbench.localized_file( |
| 88 | mgg.queue_store, queued_filepath, 'source') |
| 89 | name_builder = FilenameBuilder(queued_filename) |
| 90 | |
| 91 | ext = queued_filename.lower().strip()[-4:] |
| 92 | if ext.startswith("."): |
| 93 | ext = ext[1:] |
| 94 | else: |
| 95 | ext = None |
| 96 | |
| 97 | # Attempt to parse the model file and divine some useful |
| 98 | # information about it. |
| 99 | with open(queued_filename, 'rb') as model_file: |
| 100 | model = model_loader.auto_detect(model_file, ext) |
| 101 | |
| 102 | # generate preview images |
| 103 | greatest = [model.width, model.height, model.depth] |
| 104 | greatest.sort() |
| 105 | greatest = greatest[-1] |
| 106 | |
| 107 | def snap(name, camera, width=640, height=640, project="ORTHO"): |
| 108 | filename = name_builder.fill(name) |
| 109 | workbench_path = workbench.joinpath(filename) |
| 110 | shot = { |
| 111 | "model_path": queued_filename, |
| 112 | "model_ext": ext, |
| 113 | "camera_coord": camera, |
| 114 | "camera_focus": model.average, |
| 115 | "camera_clip": greatest*10, |
| 116 | "greatest": greatest, |
| 117 | "projection": project, |
| 118 | "width": width, |
| 119 | "height": height, |
| 120 | "out_file": workbench_path, |
| 121 | } |
| 122 | blender_render(shot) |
| 123 | |
| 124 | # make sure the image rendered to the workbench path |
| 125 | assert os.path.exists(workbench_path) |
| 126 | |
| 127 | # copy it up! |
| 128 | with open(workbench_path, 'rb') as rendered_file: |
| 129 | public_path = create_pub_filepath(entry, filename) |
| 130 | |
| 131 | with mgg.public_store.get_file(public_path, "wb") as public_file: |
| 132 | public_file.write(rendered_file.read()) |
| 133 | |
| 134 | return public_path |
| 135 | |
| 136 | thumb_path = snap( |
| 137 | "{basename}.thumb.jpg", |
| 138 | [0, greatest*-1.5, greatest], |
| 139 | mgg.global_config['media:thumb']['max_width'], |
| 140 | mgg.global_config['media:thumb']['max_height'], |
| 141 | project="PERSP") |
| 142 | |
| 143 | perspective_path = snap( |
| 144 | "{basename}.perspective.jpg", |
| 145 | [0, greatest*-1.5, greatest], project="PERSP") |
| 146 | |
| 147 | topview_path = snap( |
| 148 | "{basename}.top.jpg", |
| 149 | [model.average[0], model.average[1], greatest*2]) |
| 150 | |
| 151 | frontview_path = snap( |
| 152 | "{basename}.front.jpg", |
| 153 | [model.average[0], greatest*-2, model.average[2]]) |
| 154 | |
| 155 | sideview_path = snap( |
| 156 | "{basename}.side.jpg", |
| 157 | [greatest*-2, model.average[1], model.average[2]]) |
| 158 | |
| 159 | ## Save the public file stuffs |
| 160 | model_filepath = create_pub_filepath( |
| 161 | entry, name_builder.fill('{basename}{ext}')) |
| 162 | |
| 163 | with mgg.public_store.get_file(model_filepath, 'wb') as model_file: |
| 164 | with open(queued_filename, 'rb') as queued_file: |
| 165 | model_file.write(queued_file.read()) |
| 166 | |
| 167 | # Remove queued media file from storage and database |
| 168 | mgg.queue_store.delete_file(queued_filepath) |
| 169 | entry.queued_media_file = [] |
| 170 | |
| 171 | # Insert media file information into database |
| 172 | media_files_dict = entry.setdefault('media_files', {}) |
| 173 | media_files_dict[u'original'] = model_filepath |
| 174 | media_files_dict[u'thumb'] = thumb_path |
| 175 | media_files_dict[u'perspective'] = perspective_path |
| 176 | media_files_dict[u'top'] = topview_path |
| 177 | media_files_dict[u'side'] = sideview_path |
| 178 | media_files_dict[u'front'] = frontview_path |
| 179 | |
| 180 | # Put model dimensions into the database |
| 181 | dimensions = { |
| 182 | "center_x" : model.average[0], |
| 183 | "center_y" : model.average[1], |
| 184 | "center_z" : model.average[2], |
| 185 | "width" : model.width, |
| 186 | "height" : model.height, |
| 187 | "depth" : model.depth, |
| 188 | "file_type" : ext, |
| 189 | } |
| 190 | entry.media_data_init(**dimensions) |