This repository has been archived on 2023-03-25. You can view files and clone it, but cannot push or open issues or pull requests.

495 lines
17 KiB
Python

import os
import sys
import struct
from ..basemodel import ModelParser, TextModelParser, Exporter, Vertex, Face, Color, FaceVertex, TexCoord, Material
class UnkownTypeError(Exception):
def __init__(self, message):
self.message = message
def is_ply(filename):
"""Checks that the file is a .ply file
Only checks the extension of the file
:param filename: path to the file
"""
return filename[-4:] == '.ply'
# List won't work with this function
def _ply_type_size(type):
"""Returns the size of a ply property
:param type: a string that is in a ply element
"""
if type == 'char' or type == 'uchar':
return 1
elif type == 'short' or type == 'ushort':
return 2
elif type == 'int' or type == 'uint':
return 4
elif type == 'float':
return 4
elif type == 'double':
return 8
else:
raise UnkownTypeError('Type ' + type + ' is unknown')
def ply_type_size(type):
"""Returns the list containing the sizes of the elements
:param type: a string that is in a ply element
"""
split = type.split()
if len(split) == 1:
return [_ply_type_size(type)]
else:
if split[0] != 'list':
print('You have multiple types but it\'s not a list...', file=sys.stderr)
sys.exit(-1)
else:
return list(map(lambda a: _ply_type_size(a), split[1:]))
def bytes_to_element(type, bytes, byteorder = 'little'):
"""Returns a python object parsed from bytes
:param type: the type of the object to parse
:param bytes: the bytes to read
:param byteorder: little or big endian
"""
if type == 'char':
return ord(struct.unpack('<b', bytes)[0])
if type == 'uchar':
return ord(struct.unpack('<c', bytes)[0])
elif type == 'short':
return struct.unpack('<h', bytes)[0]
elif type == 'ushort':
return struct.unpack('<H', bytes)[0]
elif type == 'int':
return struct.unpack('<i', bytes)[0]
elif type == 'uint':
return struct.unpack('<I', bytes)[0]
elif type == 'float':
return struct.unpack('<f', bytes)[0]
elif type == 'double':
return struct.unpack('<d', bytes)[0]
else:
raise UnkownTypeError('Type ' + type + ' is unknown')
class PLYParser(ModelParser):
"""Parser that parses a .ply file
"""
def __init__(self, up_conversion = None):
super().__init__(up_conversion)
self.counter = 0
self.elements = []
self.inner_parser = PLYHeaderParser(self)
self.beginning_of_line = ''
self.header_finished = False
def parse_bytes(self, bytes, byte_counter):
"""Parses bytes of a .ply file
"""
if self.header_finished:
self.inner_parser.parse_bytes(self.beginning_of_line + bytes, byte_counter - len(self.beginning_of_line))
self.beginning_of_line = b''
return
# Build lines for header and use PLYHeaderParser
current_line = self.beginning_of_line
for (i, c) in enumerate(bytes):
char = chr(c)
if char == '\n':
self.inner_parser.parse_line(current_line)
if current_line == 'end_header':
self.header_finished = True
self.beginning_of_line = bytes[i+1:]
return
current_line = ''
else:
current_line += chr(c)
self.beginning_of_line = current_line
class PLYHeaderParser:
"""Parser that parses the header of a .ply file
"""
def __init__(self, parent):
self.current_element = None
self.parent = parent
self.content_parser = None
def parse_line(self, string):
split = string.split()
if string == 'ply':
return
elif split[0] == 'format':
if split[2] != '1.0':
print('Only format 1.0 is supported', file=sys.stderr)
sys.exit(-1)
if split[1] == 'ascii':
self.content_parser = PLY_ASCII_ContentParser(self.parent)
elif split[1] == 'binary_little_endian':
self.content_parser = PLYLittleEndianContentParser(self.parent)
elif split[1] == 'binary_big_endian':
self.content_parser = PLYBigEndianContentParser(self.parent)
else:
print('Only ascii, binary_little_endian and binary_big_endian are supported', \
file=sys.stderr)
sys.exit(-1)
elif split[0] == 'element':
self.current_element = PLYElement(split[1], int(split[2]))
self.parent.elements.append(self.current_element)
elif split[0] == 'property':
self.current_element.add_property(split[-1], ' '.join(split[1:-1]))
elif split[0] == 'end_header':
self.parent.inner_parser = self.content_parser
elif split[0] == 'comment' and split[1] == 'TextureFile':
material = Material('mat' + str(len(self.parent.materials)))
self.parent.materials.append(material)
material.relative_path_to_texture = split[2]
material.absolute_path_to_texture = os.path.join(os.path.dirname(self.parent.path), split[2])
class PLYElement:
def __init__(self, name, number):
self.name = name
self.number = number
self.properties = []
def add_property(self, name, type):
self.properties.append((name, type))
class PLY_ASCII_ContentParser:
def __init__(self, parent):
self.parent = parent
self.element_index = 0
self.counter = 0
self.current_element = None
self.beginning_of_line = ''
def parse_bytes(self, bytes, byte_counter):
current_line = self.beginning_of_line
for (i, c) in enumerate(bytes):
char = chr(c)
if char == '\n':
self.parse_line(current_line)
current_line = ''
else:
current_line += chr(c)
self.beginning_of_line = current_line
def parse_line(self, string):
if string == '':
return
if self.current_element is None:
self.current_element = self.parent.elements[0]
split = string.split()
color = None
if self.current_element.name == 'vertex':
vertex = Vertex()
red = None
blue = None
green = None
alpha = None
offset = 0
for property in self.current_element.properties:
if property[0] == 'x':
vertex.x = float(split[offset])
elif property[0] == 'y':
vertex.y = float(split[offset])
elif property[0] == 'z':
vertex.z = float(split[offset])
elif property[0] == 'red':
red = float(split[offset]) / 255
elif property[0] == 'green':
green = float(split[offset]) / 255
elif property[0] == 'blue':
blue = float(split[offset]) / 255
elif property[0] == 'alpha':
alpha = float(split[offset]) / 255
offset += 1
self.parent.add_vertex(vertex)
if red is not None:
color = Color(red, blue, green)
self.parent.add_color(color)
elif self.current_element.name == 'face':
faceVertexArray = []
current_material = None
# Analyse element
offset = 0
for property in self.current_element.properties:
if property[0] == 'vertex_indices':
for i in range(int(split[offset])):
faceVertexArray.append(FaceVertex(int(split[i+offset+1])))
offset += int(split[0]) + 1
elif property[0] == 'texcoord':
offset += 1
for i in range(3):
# Create corresponding tex_coords
tex_coord = TexCoord().from_array(split[offset:offset+2])
offset += 2
self.parent.add_tex_coord(tex_coord)
faceVertexArray[i].tex_coord = len(self.parent.tex_coords) - 1
elif property[0] == 'texnumber':
current_material = self.parent.materials[int(split[offset])]
offset += 1
face = Face(*faceVertexArray)
face.material = current_material
self.parent.add_face(face)
self.counter += 1
if self.counter == self.current_element.number:
self.next_element()
def next_element(self):
self.element_index += 1
if self.element_index < len(self.parent.elements):
self.current_element = self.parent.elements[self.element_index]
class PLYLittleEndianContentParser:
def __init__(self, parent):
self.parent = parent
self.previous_bytes = b''
self.element_index = 0
self.counter = 0
self.current_element = None
self.started = False
# Serves for debugging purposes
# self.current_byte = 0
def parse_bytes(self, bytes, byte_counter):
if not self.started:
# self.current_byte = byte_counter
self.started = True
if self.current_element is None:
self.current_element = self.parent.elements[0]
bytes = self.previous_bytes + bytes
current_byte_index = 0
while True:
property_values = []
beginning_byte_index = current_byte_index
for property in self.current_element.properties:
size = ply_type_size(property[1])
if current_byte_index + size[0] > len(bytes):
self.previous_bytes = bytes[beginning_byte_index:]
# self.current_byte -= len(self.previous_bytes)
return
if len(size) == 1:
size = size[0]
current_property_bytes = bytes[current_byte_index:current_byte_index+size]
property_values.append(bytes_to_element(property[1], current_property_bytes))
current_byte_index += size
# self.current_byte += size
elif len(size) == 2:
types = property[1].split()[1:]
current_property_bytes = bytes[current_byte_index:current_byte_index+size[0]]
number_of_elements = bytes_to_element(types[0], current_property_bytes)
current_byte_index += size[0]
# self.current_byte += size[0]
property_values.append([])
# Parse list
for i in range(number_of_elements):
if current_byte_index + size[1] > len(bytes):
self.previous_bytes = bytes[beginning_byte_index:]
# self.current_byte -= len(self.previous_bytes)
return
current_property_bytes = bytes[current_byte_index:current_byte_index+size[1]]
property_values[-1].append(bytes_to_element(types[1], current_property_bytes))
current_byte_index += size[1]
# self.current_byte += size[1]
else:
print('I have not idea what this means', file=sys.stderr)
# Add element
if self.current_element.name == 'vertex':
vertex = Vertex()
red = None
green = None
blue = None
alpha = None
offset = 0
for property in self.current_element.properties:
if property[0] == 'x':
vertex.x = property_values[offset]
elif property[0] == 'y':
vertex.y = property_values[offset]
elif property[0] == 'z':
vertex.z = property_values[offset]
elif property[0] == 'red':
red = property_values[offset] / 255
elif property[0] == 'green':
green = property_values[offset] / 255
elif property[0] == 'blue':
blue = property_values[offset] / 255
elif property[0] == 'alpha':
alpha = property_values[offset] / 255
offset += 1
self.parent.add_vertex(vertex)
if red is not None:
self.parent.add_color(Color(red, blue, green))
elif self.current_element.name == 'face':
vertex_indices = []
tex_coords = []
material = None
for (i, property) in enumerate(self.current_element.properties):
if property[0] == 'vertex_indices':
vertex_indices.append(property_values[i][0])
vertex_indices.append(property_values[i][1])
vertex_indices.append(property_values[i][2])
elif property[0] == 'texcoord':
# Create texture coords
for j in range(0,6,2):
tex_coord = TexCoord(*property_values[i][j:j+2])
tex_coords.append(tex_coord)
elif property[0] == 'texnumber':
material = self.parent.materials[property_values[i]]
for tex_coord in tex_coords:
self.parent.add_tex_coord(tex_coord)
face = Face(*list(map(lambda x: FaceVertex(x), vertex_indices)))
counter = 3
if len(tex_coords) > 0:
for face_vertex in [face.a, face.b, face.c]:
face_vertex.tex_coord = len(self.parent.tex_coords) - counter
counter -= 1
if material is None and len(self.parent.materials) == 1:
material = self.parent.materials[0]
face.material = material
self.parent.add_face(face)
self.counter += 1
if self.counter == self.current_element.number:
self.next_element()
def next_element(self):
self.counter = 0
self.element_index += 1
if self.element_index < len(self.parent.elements):
self.current_element = self.parent.elements[self.element_index]
class PLYBigEndianContentParser(PLYLittleEndianContentParser):
def __init__(self, parent):
super().__init__(self, parent)
def parse_bytes(self, bytes):
# Reverse bytes, and then
super().parse_bytes(self, bytes)
class PLYExporter(Exporter):
def __init__(self, model):
super().__init__(model)
def __str__(self):
faces = sum([part.faces for part in self.model.parts], [])
# Header
string = "ply\nformat ascii 1.0\ncomment Automatically gnerated by model-converter\n"
for material in self.model.materials:
string += "comment TextureFile " + (material.relative_path_to_texture or 'None') + "\n"
# Types : vertices
string += "element vertex " + str(len(self.model.vertices)) +"\n"
string += "property float x\nproperty float y\nproperty float z\n"
# Types : faces
string += "element face " + str(len(faces)) + "\n"
string += "property list uchar int vertex_indices\n"
if len(self.model.tex_coords) > 0:
string += "property list uchar float texcoord\n"
string += "property int texnumber\n"
# End header
string += "end_header\n"
# Content of the model
for vertex in self.model.vertices:
string += str(vertex.x) + " " + str(vertex.y) + " " + str(vertex.z) + "\n"
for face in faces:
string += "3 " + str(face.a.vertex) + " " + str(face.b.vertex) + " " + str(face.c.vertex)
if len(self.model.tex_coords) > 0:
string += " 6 " \
+ str(self.model.tex_coords[face.a.tex_coord].x) + " " \
+ str(self.model.tex_coords[face.a.tex_coord].y) + " " \
+ str(self.model.tex_coords[face.b.tex_coord].x) + " " \
+ str(self.model.tex_coords[face.b.tex_coord].y) + " " \
+ str(self.model.tex_coords[face.c.tex_coord].x) + " " \
+ str(self.model.tex_coords[face.c.tex_coord].y) + " " \
+ str(self.model.get_material_index(face.material))
string += "\n"
return string