Added the stl/obj mediatype.
authorAeva Ntsc <aeva.ntsc@gmail.com>
Mon, 15 Oct 2012 06:36:26 +0000 (01:36 -0500)
committerChristopher Allan Webber <cwebber@dustycloud.org>
Mon, 3 Dec 2012 20:40:47 +0000 (14:40 -0600)
mediagoblin/media_types/stl/__init__.py [new file with mode: 0644]
mediagoblin/media_types/stl/migrations.py [new file with mode: 0644]
mediagoblin/media_types/stl/model_loader.py [new file with mode: 0644]
mediagoblin/media_types/stl/models.py [new file with mode: 0644]
mediagoblin/media_types/stl/processing.py [new file with mode: 0644]

diff --git a/mediagoblin/media_types/stl/__init__.py b/mediagoblin/media_types/stl/__init__.py
new file mode 100644 (file)
index 0000000..edffc63
--- /dev/null
@@ -0,0 +1,27 @@
+# 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"]}
diff --git a/mediagoblin/media_types/stl/migrations.py b/mediagoblin/media_types/stl/migrations.py
new file mode 100644 (file)
index 0000000..f54c23e
--- /dev/null
@@ -0,0 +1,17 @@
+# 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 = {}
diff --git a/mediagoblin/media_types/stl/model_loader.py b/mediagoblin/media_types/stl/model_loader.py
new file mode 100644 (file)
index 0000000..12a400e
--- /dev/null
@@ -0,0 +1,153 @@
+# 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 :(")
diff --git a/mediagoblin/media_types/stl/models.py b/mediagoblin/media_types/stl/models.py
new file mode 100644 (file)
index 0000000..d087317
--- /dev/null
@@ -0,0 +1,44 @@
+# 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]
diff --git a/mediagoblin/media_types/stl/processing.py b/mediagoblin/media_types/stl/processing.py
new file mode 100644 (file)
index 0000000..0a5a24c
--- /dev/null
@@ -0,0 +1,107 @@
+# 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()