The Visage Format is a graphic format that can store multiple images and palettes. It is used in The Lost Files of Sherlock Holmes: The Case of the Serrated Scalpel and The Lost Files of Sherlock Holmes: The Case of the Rose Tattoo to store various backgrounds and sprites. Each block of data can be uncompressed or use RLE. Visage files use the *.vgs extension, though some use *.lbv.
Although the format can contain multiple blocks, there is no header to tell how many data blocks exist. You just need to check for End of File to know when you're done reading.
|UINT8||Unknown||0 usually, disasssembly shows this can control scaling somehow.|
|UINT8||Compressed||0 - Uncompressed, 1 - RLE.|
|BYTE||xOffset|| Added to drawing position to offset an image. Works as an anchor to allow sprite
frames with varying dimensions to 'line up'.
|BYTE||yOffset||See X Offset.|
|BYTE[Width×height]||Data||Palette or graphic data.|
With the header loaded, you know to read width×height bytes of data which can either be palette or graphic data.
Palette data can be identified because it begins with "VGS palette" followed by a standard 256 index, 6-bit color depth VGA Palette. For some reason the palette data is given a height of 1 (2 rows), so you have to concatenate the data to read the values properly.
Graphic data is implied if the data block isn't a palette. The graphic data is 8-bit VGA data, but, depending on the compression flag, can be either linear VGA data or use a type of RLE as described below.
The compressed data in both The Lost Files of Sherlock Holmes: The Case of the Serrated Scalpel and the sequel The Lost Files of Sherlock Holmes: The Case of the Rose Tattoo uses the following header:
|UINT16LE||size||Size of the compressed data.|
|BYTE||repeatMarker||Signals a repeated byte. Only used in the first game.|
In the first game, decompress as follows:
- Read a BYTE as testByte.
- Loop until out of data.
The second game's compression scheme divides each row into alternating chunks of transparent and opaque pixels. The decompression of each row follows this logic:
For each row:
- Read a UINT8 as skipSize.
- Add skipSize × 0xff to output. 0xff is the transparent colour index.
- Read a UINT8 as chunkSize.
- Copy chunkSize bytes from input to output.
- Loop until the end of the row is reached.
The following FreeBASIC code will load through each block of a Visage file and display the palette or graphic data. It doesn't yet properly decode the RLE compressed graphics.
' Visage File Viewer. ' Visage files contain 1 or more blocks of data that can include either palette or graphic data. ' Each block begins with a header of four 16-bit integers: width, height, RLE compression, and unknown. ' Blocks containing palette data begin with "VGA palette", all other blocks are graphic data. ' Palette blocks are typical 8-bit VGA palettes, 256 indexes, each with three 6-bit color values. ' Graphic blocks are raw 8-bit indexed VGA pixel data. ' The file path to the palette file you want to open. Dim As String VGSFile = "H:\Programs\LIB Extractor\vgs\bigmap.vgs" ' Open the Visage file. If Open(VGSFile For Binary Access Read As #1) <> 0 Then Print "File: " + VGSFile + " not found!" Sleep End End If Dim Y As Integer, X As Integer Dim Index As UByte, Length As UByte Dim XSize As UShort, YSize As UShort Dim RLEFlag As UShort, Unknown As UShort Dim BlockSize As UInteger Dim As String PaletteCheck = "" Dim ColorIndex As UShort, ColorOffset As UShort Dim Red As UByte, Green As UByte, Blue As UByte Dim As UByte Block = 0 Dim Offset As UInteger, Delta As UShort, I As UShort, FullLength As UInteger Screen 13 Do CLS ' Load the block header. Get #1, , XSize Get #1, , YSize Get #1, , RLEFlag Get #1, , Unknown 'If XSize = 389 And YSize = 1 Then ' ' Trap for palette which doesn't store the correct size to load. ' XSize = 780 'End If BlockSize = (XSize + 1) * (YSize + 1) Print " Block #: " + Str(Block) Print "X, Y (Size): " + Str(XSize) + ", " + Str(YSize) + " (" + Str(BlockSize) + " bytes)" Print " Compressed: " + Str(RLEFlag) Print " Unknown: " + Str(Unknown) ' Create an array for the block data. ReDim VGSData(0 To Blocksize) As UByte ' Load this block's data into the array. For Y = 0 To YSize For X = 0 To XSize Get #1, , VGSData(Y * XSize + X) Next X Next Y ' Check for palette. PaletteCheck = "" For X = 0 To 11 PaletteCheck = PaletteCheck + Chr(VGSData(X)) Next X If PaletteCheck = "VGA palette" + Chr(26) Then Print " Type: Palette Data" Sleep Cls ' Loop through all of the colors. ColorOffset = 12 ColorIndex = 0 Do ' Read the color attributes from the file. Red = VGSData(ColorOffset) Green = VGSData(ColorOffset + 1) Blue = VGSData(ColorOffset + 2) Red = Red * 4 Green = Green * 4 Blue = Blue * 4 ' Change the palette. Palette ColorIndex, Red, Green, Blue Line(ColorIndex, 0)-(ColorIndex, 199), ColorIndex ColorIndex = ColorIndex + 1 ColorOffset = ColorOffset + 3 Loop Until ColorOffset >= BlockSize Else Print " Type: Graphic Data" Sleep Cls ' If it's not a palette, it's graphic data. If RLEFlag = 0 Then ' Uncompressed image. For Y = 0 To YSize For X = 0 To XSize PSet(X, Y), VGSData(Y * XSize + X) Next X Next Y Else ' Compressed image. Y = 0 X = 0 FullLength = VGSData(1) * 256 + VGSData(0) ' Data Stream Length ' Unknown: Always FE FE. Offset = 4 ' To Do: Decode RLE. End If End If Sleep Block = Block + 1 Loop While Not EOF(1) Close #1
This file format was reverse engineered by TheAlmightyGuru. The RLE compression was reverse engineered by Ceidwad. If you find this information helpful in a project you're working on, please give credit where credit is due. (A link back to this wiki would be nice too!)