--- /dev/null
+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
--- /dev/null
+= 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.
--- /dev/null
+#!/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)
--- /dev/null
+#!/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)