From ed8278b122442946632068835f32bd4343969740 Mon Sep 17 00:00:00 2001 From: Antonio Ospite Date: Sun, 17 Jun 2012 13:44:14 +0200 Subject: [PATCH 1/1] Initial import --- Makefile | 20 ++++++++++ README.asciidoc | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++ aof2obj.py | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ prw2ppm.py | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 357 insertions(+) create mode 100644 Makefile create mode 100644 README.asciidoc create mode 100755 aof2obj.py create mode 100755 prw2ppm.py diff --git a/Makefile b/Makefile new file mode 100644 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 index 0000000..0b44161 --- /dev/null +++ b/README.asciidoc @@ -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 ( element), the objects it is composed by, +the faces of the objects ( element), the materials and the setup +for example rendering. The XML file also embeds a 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 index 0000000..b3ae0ba --- /dev/null +++ b/aof2obj.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# +# aof2obj - convert Artlantis Object Format files to Wavefront OBJ +# +# Copyright (C) 2012 Antonio Ospite +# +# 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 . + +import os +import sys +from lxml import etree +import binascii + + +def usage(name): + sys.stdout.write("usage: %s \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 index 0000000..a6c6668 --- /dev/null +++ b/prw2ppm.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python +# +# prw2ppm - convert Artlantis Preview files to PPM +# +# Copyright (C) 2012 Antonio Ospite +# +# 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 . + +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 \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) -- 2.1.4