--- /dev/null
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from mediagoblin.media_types.stl.processing import process_stl, \
+ sniff_handler
+
+
+MEDIA_MANAGER = {
+ "human_readable": "stereo lithographics",
+ "processor": process_stl,
+ "sniff_handler": sniff_handler,
+ "display_template": "mediagoblin/media_displays/stl.html",
+ "default_thumb": "images/media_thumbs/video.jpg",
+ "accepted_extensions": ["obj", "stl"]}
--- /dev/null
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+MIGRATIONS = {}
--- /dev/null
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import struct
+
+
+class ThreeDeeParseError(Exception):
+ pass
+
+
+class ThreeDee():
+ """
+ 3D model parser base class. Derrived classes are used for basic
+ analysis of 3D models, and are not intended to be used for 3D
+ rendering.
+ """
+
+ def __init__(self, fileob):
+ self.verts = []
+ self.average = [0, 0, 0]
+ self.min = [None, None, None]
+ self.max = [None, None, None]
+ self.width = 0 # x axis
+ self.depth = 0 # y axis
+ self.height = 0 # z axis
+
+ self.load(fileob)
+ if not len(self.verts):
+ raise ThreeDeeParseError("Empyt model.")
+
+ for vector in self.verts:
+ for i in range(3):
+ num = vector[i]
+ self.average[i] += num
+ if not self.min[i]:
+ self.min[i] = num
+ self.max[i] = num
+ else:
+ if self.min[i] > num:
+ self.min[i] = num
+ if self.max[i] < num:
+ self.max[i] = num
+
+ for i in range(3):
+ self.average[i]/=len(self.verts)
+
+ self.width = abs(self.min[0] - self.max[0])
+ self.depth = abs(self.min[1] - self.max[1])
+ self.height = abs(self.min[2] - self.max[2])
+
+
+ def load(self, fileob):
+ """Override this method in your subclass."""
+ pass
+
+
+class ObjModel(ThreeDee):
+ """
+ Parser for textureless wavefront obj files. File format
+ reference: http://en.wikipedia.org/wiki/Wavefront_.obj_file
+ """
+
+ def __vector(self, line, expected=3):
+ nums = map(float, line.strip().split(" ")[1:])
+ return tuple(nums[:expected])
+
+ def load(self, fileob):
+ for line in fileob:
+ if line[0] == "v":
+ self.verts.append(self.__vector(line))
+
+
+class BinaryStlModel(ThreeDee):
+ """
+ Parser for ascii-encoded stl files. File format reference:
+ http://en.wikipedia.org/wiki/STL_%28file_format%29#Binary_STL
+ """
+
+ def __num(self, fileob, hint):
+ assert hint == "uint" or hint == "real" or hint == "short"
+ form = None
+ bits = 0
+ if hint == "uint":
+ form = "<I" # little-endian unsigned int
+ bits = 32
+ elif hint == "real":
+ form = "<i" # little-endian signed int
+ bits = 32
+ elif hint == "short":
+ form = "<H" # little-endian unsigned short
+ bits = 16
+ return struct.unpack(form, fileob.read(bits/8))[0]
+
+ def __vector(self, fileob):
+ return tuple([self.__num(fileob, "real") for n in range(3)])
+
+ def load(self, fileob):
+ fileob.seek(80) # skip the header
+ triangle_count = self.__num(fileob, "uint")
+ for i in range(triangle_count):
+ self.__vector(fileob) # skip the normal vector
+ for v in range(3):
+ # - FIXME - traingle_count IS reporting the correct
+ # number, but the vertex information appears to be
+ # total nonsense :(
+ self.verts.append(self.__vector(fileob))
+ self.__num(fileob, "short") # skip the attribute byte count
+
+
+def auto_detect(fileob, hint):
+ """
+ Attempt to divine which parser to use to divine information about
+ the model / verify the file."""
+
+ if hint == "obj" or not hint:
+ try:
+ return ObjModel(fileob)
+ except ThreeDeeParseError:
+ pass
+
+ if hint == "stl" or not hint:
+ try:
+ # HACK Ascii formatted stls are similar enough to obj
+ # files that we can just use the same parser for both.
+ # Isn't that something?
+ return ObjModel(fileob)
+ except ThreeDeeParseError:
+ pass
+ try:
+ # It is pretty important that the binary stl model loader
+ # is tried second, because its possible for it to parse
+ # total garbage from plaintext =)
+ return BinaryStlModel(fileob)
+ except ThreeDeeParseError:
+ pass
+ except MemoryError:
+ pass
+
+ raise ThreeDeeParseError("Could not successfully parse the model :(")
--- /dev/null
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from mediagoblin.db.sql.base import Base
+
+from sqlalchemy import (
+ Column, Integer, Float, ForeignKey)
+from sqlalchemy.orm import relationship, backref
+
+
+class StlData(Base):
+ __tablename__ = "stl__mediadata"
+
+ # The primary key *and* reference to the main media_entry
+ media_entry = Column(Integer, ForeignKey('core__media_entries.id'),
+ primary_key=True)
+ get_media_entry = relationship("MediaEntry",
+ backref=backref("stl__media_data", cascade="all, delete-orphan"))
+
+ center_x = Column(Float)
+ center_y = Column(Float)
+ center_z = Column(Float)
+
+ width = Column(Float)
+ height = Column(Float)
+ depth = Column(Float)
+
+
+DATA_MODEL = StlData
+MODELS = [StlData]
--- /dev/null
+# GNU MediaGoblin -- federated, autonomous media hosting
+# Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import logging
+
+from mediagoblin import mg_globals as mgg
+from mediagoblin.processing import create_pub_filepath, \
+ FilenameBuilder
+
+from mediagoblin.media_types.stl import model_loader
+
+
+_log = logging.getLogger(__name__)
+SUPPORTED_FILETYPES = ['stl', 'obj']
+
+
+def sniff_handler(media_file, **kw):
+ if kw.get('media') is not None:
+ name, ext = os.path.splitext(kw['media'].filename)
+ clean_ext = ext[1:].lower()
+
+ if clean_ext in SUPPORTED_FILETYPES:
+ _log.info('Found file extension in supported filetypes')
+ return True
+ else:
+ _log.debug('Media present, extension not found in {0}'.format(
+ SUPPORTED_FILETYPES))
+ else:
+ _log.warning('Need additional information (keyword argument \'media\')'
+ ' to be able to handle sniffing')
+
+ return False
+
+
+def process_stl(entry):
+ """
+ Code to process an stl or obj model.
+ """
+
+ workbench = mgg.workbench_manager.create_workbench()
+ # Conversions subdirectory to avoid collisions
+ conversions_subdir = os.path.join(
+ workbench.dir, 'conversions')
+ os.mkdir(conversions_subdir)
+ queued_filepath = entry.queued_media_file
+ queued_filename = workbench.localized_file(
+ mgg.queue_store, queued_filepath, 'source')
+ name_builder = FilenameBuilder(queued_filename)
+
+ ext = queued_filename.lower().strip()[-4:]
+ if ext.startswith("."):
+ ext = ext[1:]
+ else:
+ ext = None
+
+ # Attempt to parse the model file and divine some useful
+ # information about it.
+ with open(queued_filename, 'rb') as model_file:
+ model = model_loader.auto_detect(model_file, ext)
+
+ # TODO: generate blender previews
+
+ # Save the public file stuffs
+ model_filepath = create_pub_filepath(
+ entry, name_builder.fill('{basename}{ext}'))
+
+ with mgg.public_store.get_file(model_filepath, 'wb') as model_file:
+ with open(queued_filename, 'rb') as queued_file:
+ model_file.write(queued_file.read())
+
+
+ # Remove queued media file from storage and database
+ mgg.queue_store.delete_file(queued_filepath)
+ entry.queued_media_file = []
+
+ # Insert media file information into database
+ media_files_dict = entry.setdefault('media_files', {})
+ media_files_dict[u'original'] = model_filepath
+ media_files_dict[u'thumb'] = ["mgoblin_static/images/404.png"]
+
+ # Put model dimensions into the database
+ dimensions = {
+ "center_x" : model.average[0],
+ "center_y" : model.average[1],
+ "center_z" : model.average[2],
+ "width" : model.width,
+ "height" : model.height,
+ "depth" : model.depth,
+ }
+ entry.media_data_init(**dimensions)
+
+ # clean up workbench
+ workbench.destroy_self()