Initial import
authorAntonio Ospite <ospite@studenti.unina.it>
Sun, 17 Jun 2012 11:44:14 +0000 (13:44 +0200)
committerAntonio Ospite <ospite@studenti.unina.it>
Sun, 17 Jun 2012 11:44:14 +0000 (13:44 +0200)
Makefile [new file with mode: 0644]
README.asciidoc [new file with mode: 0644]
aof2obj.py [new file with mode: 0755]
prw2ppm.py [new file with mode: 0755]

diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..feac362
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,20 @@
+all: convert
+
+download:
+       -wget -nv -nc http://www.polantis.com/data/2/2/1093/formats/14/90/IKEA-Expedit_Bookcase-3d.aof
+       -wget -nv -nc http://www.polantis.com/data/2/2/1093/formats/16/95/IKEA-Expedit_Bookcase-3d.obj \
+            -O IKEA-Expedit_Bookcase-3d.obj.orig
+       
+       -wget -nv -nc http://www.polantis.com/data/2/2/1094/formats/14/90/IKEA-Expedit_Bookcase_Black-3d.aof
+       -wget -nv -nc http://www.polantis.com/data/2/2/1094/formats/16/95/IKEA-Expedit_Bookcase_Black-3d.obj \
+            -O IKEA-Expedit_Bookcase_Black-3d.obj.orig
+
+convert: download
+       @for file in *.aof; do ./aof2obj.py "$$file"; done
+       @for file in *.prw; do ./prw2ppm.py "$$file"; done
+
+clean:
+       rm -f *.obj *.prw *.ppm *~
+
+cleanall: clean
+       -rm -i *.aof *.obj.orig
diff --git a/README.asciidoc b/README.asciidoc
new file mode 100644 (file)
index 0000000..0b44161
--- /dev/null
@@ -0,0 +1,106 @@
+= aof2obj
+
+*aof2obj* is a script to convert Artlantis Object Format files to Wavefront
+OBJ; it can be used to have .aof files imported into 3D modeling programs
+such as blender (http://blender.org).
+
+Artlantis is a closed source 3D modeling and rendering software which can be
+found at http://www.artlantis.com/
+
+Artlantis Object Format is one of the formats produced by Artlantis.
+
+Wavefront OBJ is a very common format used to interchange data about 3D
+models, see https://en.wikipedia.org/wiki/Wavefront_.obj_file
+
+*aof2obj* requires the lxml python module.
+
+== Artlantis Object Format
+
+The Artlantis Object Format is based on XML, it provides information about the
+vertices of the model (<Points> element), the objects it is composed by,
+the faces of the objects (<Polygons> element), the materials and the setup
+for example rendering. The XML file also embeds a preview image
+(<Preview.Image> element) of the rendering result.
+
+Currently *aof2obj* supports only a small subset of these features, it was
+written only as a quick script hacked together to see what was inside some AOF
+files that can be found on the web.
+
+Notes that the resulting models may need to be rescaled to become visible in
+the viewport of the destination 3D program which will import the .obj files
+produced by this script.
+
+== Examples of .aof files
+
+Some sample files can be found starting from this page:
+
+ - http://www.polantis.com/ikea/expedit-bookcase
+
+Having the same model in both .aof and .obj makes it a little easier to
+reverse engineer the .aof format, even if its structure is very
+straightforward already.
+
+ - http://www.polantis.com/data/2/2/1093/formats/14/90/IKEA-Expedit_Bookcase-3d.aof
+ - http://www.polantis.com/data/2/2/1093/formats/16/95/IKEA-Expedit_Bookcase-3d.obj
+
+ - http://www.polantis.com/data/2/2/1094/formats/14/90/IKEA-Expedit_Bookcase_Black-3d.aof
+ - http://www.polantis.com/data/2/2/1094/formats/16/95/IKEA-Expedit_Bookcase_Black-3d.obj
+
+== PRW files
+
+*aof2obj* extracts also the Artlantis Preview Files embedded into the Artlantis
+Object Format files, and saves them with the .prw extension.
+
+These files can be converted to ppm with the *prw2ppm* script.
+
+The Artlantis Preview Files are bitmap images compressed using an RLE encoding.
+
+There is a header with this structure:
+
+  TWH
+
+where T, W and H are respectively the file type, the image width and height,
+as 32-bit big-endian integer values.
+
+Immediately after the header there is the image data, which can be seen as divided into
+packets of the format:
+
+  CP+
+
+Where C is the run count as a 32 bit big-endian integer, and P+ is
+a sequence of 1 or more Pixels encoded as 32-bit big-endian integers with the
+color information in the format 00RRGGBB.
+
+[NOTE]
+The rightmost byte was always zero in the analyzed files.
+
+A .prw file looks like:
+
+  TWHCPPPPCPCPCPCPCPPPPPPPPPCP...
+
+The run counter C can refer to two types of packets:
+
+  - run-length packet: here the single P value has to be repeated C times
+
+  - raw packet: here C is followed by C different P pixels values
+
+to decide if a packet is a 'run-length packet' or a 'raw packet' the last
+pixel value of the previous packet has to be inspected: let be P and Q two
+pixel values, and consider the sequence:
+
+  PCQX
+
+We have these rules:
+
+ - if P == Q then C starts a run-length packet and Q is repeated C
+   times and X will be the next run count,
+
+ - if P != Q then C starts a raw packet and Q is the first pixel of a sequence
+   of C different pixel values (X will be the second pixel value).
+
+Some special treatment might be still needed to handle the count in the first
+packet, in the analyzed files the first packet was always a 'run-length
+packet', so this is the assumption *prw2ppm* relies on.
+
+I call this RLE compression method the 'sandwich encoding', because the count
+in run-length packets is between two identical pixel values.
diff --git a/aof2obj.py b/aof2obj.py
new file mode 100755 (executable)
index 0000000..b3ae0ba
--- /dev/null
@@ -0,0 +1,118 @@
+#!/usr/bin/env python
+#
+# aof2obj - convert Artlantis Object Format files to Wavefront OBJ
+#
+# Copyright (C) 2012  Antonio Ospite <ospite@studenti.unina.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+from lxml import etree
+import binascii
+
+
+def usage(name):
+    sys.stdout.write("usage: %s <aof file>\n" % name)
+
+
+def aof2obj(aof_filename, obj_filename, basename):
+
+    f = open(aof_filename, "r")
+
+    # Use recover mode, because the xml format is ill-specified:
+    # there is at least one element with the name starting with a number
+    # <3D.Paths>, in violation to the XML spec.
+    # And there is some elements with the UUID attribute specified multiple
+    # times...
+    parser = etree.XMLParser(recover=True)
+    tree = etree.parse(f, parser)
+
+    # The preview image could be some bitmap format, don't know yet
+    preview = tree.find("Preview.Image")
+    if preview is not None:
+        heximage = preview.text.rstrip("\t\n")
+
+        preview_file = open(basename + ".prw", "w")
+        preview_file.write(binascii.unhexlify(heximage))
+        preview_file.close()
+
+    obj_file = open(obj_filename, "w")
+
+    obj_file.write("# obj file created with aof2obj\n")
+    obj_file.write("# by Antonio Ospite\n\n")
+
+    # Vertices
+    points = tree.find('Points')
+    points_data = points.text.strip("\t\n").split("\n")
+
+    for p in points_data[1:]:
+        obj_file.write("v %s\n" % p.rstrip(' '))
+
+    obj_file.write("# %d vertices\n\n" % int(points_data[0]))
+
+    # Faces
+    polygons = tree.find('Polygons')
+    polygons_data = polygons.text.strip("\t\n").split("\n")
+
+    current_object = -1
+
+    for p in polygons_data[1:]:
+        ptype, pdata = p.split("\t")
+        if ptype != 'o' and ptype != 'p':
+            sys.stderr.write("Unsupported polygon type")
+            sys.exit(1)
+
+        # If there is a M in pdata discard the line
+        # don't know how to handle that
+        if 'M' in pdata:
+            sys.stdout.write("Warning, ignored M element\n")
+            continue
+
+        # Don't know what the 'I' stands for yet, for now just ignore it
+        pdata = pdata.replace(' I ', ' ').strip(' ')
+
+        pdata_list = pdata.split(' ')
+
+        obj_number = int(pdata_list[0])
+        num_verts = int(pdata_list[1])
+
+        # Vert indices start from 1 in .obj files
+        verts_data = " ".join([str(int(i) + 1) for i in pdata_list[2:]])
+
+        if obj_number != current_object:
+            obj_file.write("\n# defining Object_%d\n" % obj_number)
+            obj_file.write("g Object_%d\n" % obj_number)
+            current_object = obj_number
+
+        obj_file.write("f %s\n" % verts_data)
+
+    obj_file.write("# %d faces\n\n" % int(polygons_data[0]))
+
+    obj_file.close()
+
+
+if __name__ == "__main__":
+
+    if len(sys.argv) < 2:
+        usage(sys.argv[0])
+        sys.exit(1)
+
+    aof_filename = sys.argv[1]
+    basename_no_ext = os.path.splitext(aof_filename)[0]
+    obj_filename = basename_no_ext + ".obj"
+
+    aof2obj(aof_filename, obj_filename, basename_no_ext)
+
+    sys.exit(0)
diff --git a/prw2ppm.py b/prw2ppm.py
new file mode 100755 (executable)
index 0000000..a6c6668
--- /dev/null
@@ -0,0 +1,113 @@
+#!/usr/bin/env python
+#
+# prw2ppm - convert Artlantis Preview files to PPM
+#
+# Copyright (C) 2012  Antonio Ospite <ospite@studenti.unina.it>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import struct
+
+
+def get_be32(f):
+    fmt = '>I'
+
+    length = struct.calcsize(fmt)
+    data = f.read(length)
+    value = struct.unpack_from(fmt, data)
+
+    return  value[0]
+
+
+def to_rgb(data):
+    r = (data & 0x00FF0000) >> 16
+    g = (data & 0x0000FF00) >> 8
+    b = (data & 0x000000FF)
+
+    return (r, g, b)
+
+
+def usage(name):
+    sys.stdout.write("usage: %s <prw file>\n" % name)
+
+
+def prw2ppm(prw_filename, ppm_filename):
+    f = open(prw_filename, "rb")
+
+    # file type or frame number,
+    # or maybe the type of the first packet? (run-length or raw)
+    file_type = get_be32(f)
+    if file_type != 1:
+        sys.stdderr.write("Unknown preview file type.\n")
+        sys.exit(1)
+
+    width = get_be32(f)
+    height = get_be32(f)
+
+    outfile = open(ppm_filename, "w")
+
+    outfile.write("P3\n")
+    outfile.write("%d %d\n" % (width, height))
+    outfile.write("%d\n" % 255)
+
+    n = 0
+    n_pixels = width * height
+
+    # Read the first packet here,
+    # AFAIK it is always a run-length packet
+    count = get_be32(f)
+    data = get_be32(f)
+    old_data = data
+
+    while True:
+        if data == old_data:
+            # run-length packet
+            for i in range(0, count):
+                outfile.write("%d %d %d\n" % to_rgb(data))
+        else:
+            # raw packet
+            outfile.write("%d %d %d\n" % to_rgb(data))
+            for i in range(0, count - 1):
+                data = get_be32(f)
+                outfile.write("%d %d %d\n" % to_rgb(data))
+
+        n += count
+        if n == n_pixels:
+            break
+
+        old_data = data
+
+        # read next packet
+        count = get_be32(f)
+        data = get_be32(f)
+
+    outfile.close()
+
+
+if __name__ == "__main__":
+
+    if len(sys.argv) < 2:
+        usage(sys.argv[0])
+        sys.exit(1)
+
+    prw_filename = sys.argv[1]
+
+    basename_no_ext = os.path.splitext(prw_filename)[0]
+    ppm_filename = basename_no_ext + ".ppm"
+
+    prw2ppm(prw_filename, ppm_filename)
+
+    sys.exit(0)