(One intermediate revision by the same user not shown) | |||
Line 71: | Line 71: | ||
== Fields == | == Fields == | ||
+ | |||
+ | [[File:OFPFieldFormat.png|thumb|right|A typical field]] | ||
'''Fields''' are composed of 4 atoms (that I know of) and are like a super-atom of some sort. | '''Fields''' are composed of 4 atoms (that I know of) and are like a super-atom of some sort. | ||
Line 88: | Line 90: | ||
1 Value Atom => Atom ID=0x34 whose value is the value of the field (told you it was crazy !) | 1 Value Atom => Atom ID=0x34 whose value is the value of the field (told you it was crazy !) | ||
1 Marker Atom => Atom ID=0x7D0 that marks the end of the field | 1 Marker Atom => Atom ID=0x7D0 that marks the end of the field | ||
− | |||
− | |||
=== Field ID === | === Field ID === | ||
Line 123: | Line 123: | ||
= Reading the File = | = Reading the File = | ||
+ | |||
+ | [[File:OFPFileFormat.png|thumb|right|A standard OFP file with only 1 Lens Object]] | ||
So basically, instead of using a standard BinaryReader to read the file, you start by reading the header then all you're going to read are Atoms after that. It's a bit like if the BinaryReader transformed into an ''AtomReader'' instead. | So basically, instead of using a standard BinaryReader to read the file, you start by reading the header then all you're going to read are Atoms after that. It's a bit like if the BinaryReader transformed into an ''AtomReader'' instead. | ||
Here is the general file structure : | Here is the general file structure : | ||
− | |||
− | |||
And this is how you read the file : | And this is how you read the file : | ||
Line 179: | Line 179: | ||
* a '''LENS_OBJECT_SOLO''' atom whose ID=0x04 and value is a boolean telling if the object is displayed alone (true) or displayed with the others (false) | * a '''LENS_OBJECT_SOLO''' atom whose ID=0x04 and value is a boolean telling if the object is displayed alone (true) or displayed with the others (false) | ||
− | All we have to do then is to create an instance of the appropriate LensObject object with its name and Hide/Solo flags, then de-serialize its | + | All we have to do then is to create an instance of the appropriate LensObject object with its name and Hide/Solo flags, then de-serialize its fields. |
We stop reading the block when we encounter an atom whose ID=0x3E8 which is the end marker. | We stop reading the block when we encounter an atom whose ID=0x3E8 which is the end marker. |
Latest revision as of 18:49, 9 May 2011
The Optical Flares Preset file format
It could have been a simple XML format but no ! The guys at Video Copilot knew I would be coming for them, so they devised the most annoying file format on Earth !
I suppose it's one of these companies that imagine they can create some products that are "Future Proof". Moohahaha ! Sorry guys, but nothing's future proof : ask the guys in nuclear waste storage...
Moreover, their products are called Video Copilot.Net so you may think they would take advantage of the smart serialization helpers that can be used with the .Net framework, like XML with XSLT for example. But that would be too easy.
The guys at VC decided to create their own future-proof-tarot-reading binary format with unique IDs and shit.
But enough whining, I owned the bastard format after all.
Structures
Header
The OFP file starts with a header of 4 bytes :
O F P x
The first 3 bytes are the classic signature bytes and the 4th byte x always equals 3 in my files. I assumed it's the file format version and threw an exception if it's not equal to 3.
Atoms
The remaining data in the file are what I called Atoms.
Understand that these guys didn't simply store an INT, a BOOL, a FLOAT, a STRING or anything like it : all these simple types are encapsulated by what I called an Atom class. Basically, all the elements in the remaining file are atoms.
Here is the structure of an Atom :
1 WORD = Atom Type 1 WORD = Atom Run Length L L BYTES = Atom Value
At which point, I was really wondering if the guys at VC weren't a bit crazy in the coconut because, usually, when you create a binary file format and deliberately choose not to use XML, that means you're after size optimization. But here, you have to know that Atoms that host values have a minimum run-length of 32 bytes. Even a simple boolean ! That means that, in order to store a boolean, you must write 2 bytes (Atom Type) + 2 bytes (Run Length = 32) + 32 bytes (the actual boolean value) = 36 bytes total ! (but who am I to judge ? In order to be actually future-proof, we must not eliminate the hypothesis that tomorrow, perhaps, booleans will be stored on 256 bits) (you never really know, that's the beauty with the future)
I have isolated the following Atom types encountered throughout the file :
// Field atoms FIELD_ID = 0x32, FIELD_TYPE = 0x33, FIELD_VALUE = 0x34, // Lens object atoms LENS_OBJECT_NAME = 0x02, LENS_OBJECT_HIDE = 0x03, LENS_OBJECT_SOLO = 0x04, // Block headers GENERAL_PARAMETERS = 0xBB8, LENS_OBJECT_DESCRIPTOR = 0x01, // Block end markers FIELD_END = 0x7D0, GLOBAL_PARAMS_END = 0xFA0, LENS_OBJECT_END = 0x3E8,
I created an Atom class that simply reads Atoms from a standard System.IO.BinaryReader. The run-length encoded Atom Value is stored as a variant type that can be one of the following recognized types :
BOOLEAN INTEGER / ENUM FLOAT COLOR STRING VECTOR2BOOL, 2 floats + a boolean (strange, I know) COLORBOOL, 1 color + a boolean (??) ENUMSTRING, 1 int for enum value + 1 string for enum name
Fields
Fields are composed of 4 atoms (that I know of) and are like a super-atom of some sort.
A Field contains an actual parameter value, these are the parameters you can tweak in the Optical Flares plug-in and that describe the attributes of your lens flares.
There are fields in 2 types of objects :
- General Parameters object, which are parameters global to the lens flare
- Lens Objects, which describe the lens effects existing in the lens flare (a lens object is one of the 12 object types described in the Lens Flare page)
Typically, an object (GeneralParameters or LensObject) starts with a header, a bunch of fields and a footer or end marker.
Here is the structure of a Field (remember we're now dealing with Atoms, not simple types like float or integer anymore) :
1 ID Atom => Atom ID=0x32 whose value as INT is the ID of the field 1 Type Atom => Atom ID=0x33 whose value as INT is the TYPE of the field 1 Value Atom => Atom ID=0x34 whose value is the value of the field (told you it was crazy !) 1 Marker Atom => Atom ID=0x7D0 that marks the end of the field
Field ID
The Field ID is unique and corresponds to the ID of some actual field in your class.
For the purpose of automatically binding the fields from the file and the fields in a C# class, I created an "ID" attribute so when I create a field in my C# class, I also attach it its ID which corresponds to the ID that is read from disk.
For example :
class MyLensObject { [ID( 0x3F )] public float SomeValue; // So we know that when we read a field from the file whose ID is 0x3F, we should de-serialize it here... }
With that system, binding fields is easy : simply read all the fields from the file and all the fields in your class (i.e. System.Reflection.FieldInfo) then build a map that links them together. Deserialize. Add salt. Serve cold.
Field Type
The Field Type is an INT cast into a FIELD_TYPE enum that can take the following values :
FLOAT = 0, a FLOAT field VECTOR2BOOL = 1, a VECTOR2BOOL (2 floats + a boolean) field we saw earlier COLOR = 2, a COLOR field INTEGER = 3, an INTEGER field BOOL = 4, a BOOLEAN field ENUM_STRING = 5, an enum value (INT) + the enum name (STRING) SEPARATOR = 6, a useless separator which corresponds to the groups of fields that you can see in the plug-in ("Common Settings" is a group, "Dynamic Triggering" is another group, each group's fields are separated by a SEPARATOR field)
Field Value
Nothing to say here, this atom merely hosts the (variant) value of the field which we can retrieve and cast to the proper type according to the previously read Field Type described above.
Reading the File
So basically, instead of using a standard BinaryReader to read the file, you start by reading the header then all you're going to read are Atoms after that. It's a bit like if the BinaryReader transformed into an AtomReader instead.
Here is the general file structure :
And this is how you read the file :
Descriptor Atom
The main loop of the file reader reads a single Atom that will either be :
- The GeneralParametersBlock header (Atom ID=0xBB8 as written above), in which case we then read the GeneralParameters
- The LensObjectDescriptor header (Atom ID=0x01 as written above), in which case we then read a LensObject
We exit the loop when we reach the end of the binary reader (i.e. EOF).
Reading the General Parameters Block
Say we encountered an atom with ID=0xBB8, this marks the beginning of a General Parameters object block.
All we have to do is to create an instance of the General Parameters class, then de-serialize its fields.
The field groups are :
- Common Settings
- Matte Box Controls
- Lens Texture
- Chromatic Aberration
- Color Correction
We stop reading the block when we encounter an atom whose ID=0xFA0 which is the end marker.
Reading a Lens Object
Say we encountered an atom with ID=0x01, this marks the beginning of a Lens Object block.
This Atom also gives us (via its INT value) the type of lens object that is described further. Here are the 12 supported types :
LensObjectGlow = 0x1 LensObjectStreak = 0x2 LensObjectMultiIris = 0x4 LensObjectShimmer = 0x5 LensObjectRing = 0x6 LensObjectHoop = 0x7 LensObjectGlint = 0x8 LensObjectSparkle = 0x9 LensObjectSpikeBall = 0xB LensObjectCaustic = 0xC LensObjectLensOrbs = 0xE LensObjectIris = 0xF
This atom is followed by :
- a LENS_OBJECT_NAME atom whose ID=0x02 and value is the name of the lens object (a string)
- a LENS_OBJECT_HIDE atom whose ID=0x03 and value is a boolean telling if the object is hidden (true) or visible (false)
- a LENS_OBJECT_SOLO atom whose ID=0x04 and value is a boolean telling if the object is displayed alone (true) or displayed with the others (false)
All we have to do then is to create an instance of the appropriate LensObject object with its name and Hide/Solo flags, then de-serialize its fields.
We stop reading the block when we encounter an atom whose ID=0x3E8 which is the end marker.
Automatic Deserialization of Fields
As we have summarily described earlier, de-serialization of fields is made quite simple via the Field IDs and the IDAttribute attached to the physical class fields.
The de-serialization of fields exists as a single method that lies in the base class of all de-serializable objects (i.e. GeneralParameters and LensObjects alike).
This methods does the following :
- // List all the fields in the object to de-serialize via reflection
- For each System.Reflection.FieldInfo in our class
- Retrieve its custom ID atribute
- If no such attribute → throw exception (we don't want to have fields without an ID !)
- Add the ID of the field to the map of ID → Field
- If the ID is already registered in the map → throw exception (we don't accept duplicate IDs !)
- For each System.Reflection.FieldInfo in our class
- // Read the fields from the OFP file
- While we haven't encountered an end marker
- Read the next field from file
- Retrieve the actual class field to de-serialize to via the ID → Field map
- If no such ID exists in the map → throw exception (we need a field to de-serialize to !)
- De-serialize the field from file into the class field
- While we haven't encountered an end marker
LensFlare Class
Check the Lens Flare page to get my free LensFlare class in C# that is able to read OFP files.