diff --git a/plugins/impex/sai/3rdparty/libsai/CMakeLists.txt b/plugins/impex/sai/3rdparty/libsai/CMakeLists.txt index 23fb3a61a9..3dbbf4e2dc 100644 --- a/plugins/impex/sai/3rdparty/libsai/CMakeLists.txt +++ b/plugins/impex/sai/3rdparty/libsai/CMakeLists.txt @@ -1,54 +1,54 @@ cmake_minimum_required( VERSION 3.8.0 ) project( sai ) -set( CMAKE_CXX_STANDARD 14 ) +set( CMAKE_CXX_STANDARD 11 ) set( CMAKE_CXX_STANDARD_REQUIRED ON ) set( CMAKE_CXX_EXTENSIONS OFF ) set( CMAKE_COLOR_MAKEFILE ON ) set( CMAKE_VERBOSE_MAKEFILE ON ) set( CMAKE_EXPORT_COMPILE_COMMANDS ON ) -if( MSVC ) - add_compile_options( /W3 ) -elseif( CMAKE_COMPILER_IS_GNUCXX ) - add_compile_options( -march=native ) - add_compile_options( -Wall ) - add_compile_options( -Wextra ) -endif() - ### libsai add_library( sai source/sai.cpp ) target_include_directories( sai PUBLIC include ) +if( MSVC ) + target_compile_options( sai PUBLIC /W3 ) +elseif( CMAKE_COMPILER_IS_GNUCXX ) + target_compile_options( sai PUBLIC -Wall ) + target_compile_options( sai PUBLIC -Wextra ) +endif() + + ### Decryption sample add_executable( Decrypt samples/Decrypt.cpp ) target_link_libraries( Decrypt PRIVATE sai ) ### VFS tree sample add_executable( Tree samples/Tree.cpp ) target_link_libraries( Tree PRIVATE sai ) diff --git a/plugins/impex/sai/3rdparty/libsai/README.md b/plugins/impex/sai/3rdparty/libsai/README.md index b62c7a9cf4..b0ecc75b52 100644 --- a/plugins/impex/sai/3rdparty/libsai/README.md +++ b/plugins/impex/sai/3rdparty/libsai/README.md @@ -1,1041 +1,1059 @@ - [Cracking PaintTool Sai documents](#cracking-painttool-sai-documents) - [Decryption](#decryption) - [Caching](#caching) - [File System](#file-system) - [Folder structure](#folder-structure) - [Serialization Streams](#serialization-streams) - [Files](#files) - [".XXXXXXXXXXXXXXXX"](#xxxxxxxxxxxxxxxx) - ["canvas"](#canvas) - ["laytbl" "subtbl"](#laytbl-subtbl) - ["/layers" "/sublayers"](#layers-sublayers) - [Raster Layers](#raster-layers) - [Linework Layers](#linework-layers) - [Decryption Keys](#decryption-keys) - [UserKey](#userkey) - [NotRemoveMe](#notremoveme) - [LocalState](#localstate) - [sai.ssd](#saissd) # Cracking PaintTool Sai documents This document represents about a year and a half of off-and-on hobby-research on reverse engineering the digitizing raster/vector art program PaintTool Sai. This write-up in particular is focused on the technical specifications of the user-created `.sai` file format used to archive a user's artwork and the layers of abstraction implemented by SYSTEMAX for extracting this data outside of the context of the original software. This document is more directed at anyone that wants to implement their own library to read or interface with `.sai` files or just to get a comprehensive understanding of the decisions that SYSTEMAX has chosen to make for their file format. If you find anything in this document to be misleading, incomplete, or flat-out incorrect feel free to shoot me an email at `Wunkolo (at) gmail.com`. Previous work includes my now-abandoned run-time exploitation framework [SaiPal](https://github.com/Wunkolo/SaiPal/releases) and the more recent Windows explorer thumbnail extension [SaiThumbs](https://github.com/Wunkolo/SaiThumbs). This document assumes you have some knowledge of the C and C++ syntax as the data structures and algorithms here will be presented in the form of C and C++ structures and subroutines. > PaintTool SAI Ver.1 > > ![](https://www.systemax.jp/image/sai_logo.jpg) > > `PaintTool SAI is high quality and lightweight painting software, fully digitizer support, amazing anti-aliased paintings, provide easy and stable operation, this software make digital art more enjoyable and comfortable.` > > SYSTEMAX Software Development > > Details: > - Fully digitizer support with pressure. > - Amazing anti-aliased drawings. > - Highly accurate composition with 16bit ARGB channels. > - Simple but powerful user interface, easy to learn. > - Fully support Intel MMX Technology. > - Data protection function to avoid abnormal termination such as bugs. > > Copyright 1996-2016 SYSTEMAX Software Development # Decryption Sai uses the file type `.sai` as its document format for storing both raster and vector layers as well as other canvas related meta-data. The `.sai` file among with other files such as thumbnails, the `sai.ssd` file and others is but an archive containing a _file-system-like_ structure once decrypted. Each layer, mask, and related meta data is stored in an individual pseudo-file which also has a layer of block-level encryption. The file itself is encrypted in [ECB](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_Codebook_.28ECB.29) blocks in which any randomly accessed block can be decrypted by also decrypting the appropriate `Table-Block` and accessing its 32-bit key found within. It's been found that some preliminary files such as thumbnails and the archive responsible for swatches/palettes use a different decryption key, block size, and `Table-Block` location. This document will mostly cover the method used for sai's user created `.sai` documents and very partially show related information for the other files. An individual block in a `.sai` file is `4096` bytes of data. Every block index that is a multiple of `512`(`0, 512, 1024, etc`) is a `Table-Block` containing meta-data about the block itself and the `511` blocks after it. Every other block that is not a `Table-Block` is a `Data-Block`: ```cpp // Gets the Table-Block index appropriate for the current block index std::size_t NearestTable(std::size_t BlockIndex) { return BlockIndex & ~(0x1FF); } // Demonstrating how to quickly determine if a block Index is a data-block or a table-block bool IsTableBlock(std::size_t BlockIndex) { return (BlockIndex & 0x1FF) ? false:true; } bool IsDataBlock(std::size_t BlockIndex) { return (BlockIndex & 0x1FF) ? true:false; } ``` All blocks are encrypted and decrypted symmetrically using a simple exclusive-or-based encryption which refers to a static atlas of 256 32-bit integers which can be found at the end of this text. Different files related to Sai use different static keys. The keyvault used for the `.sai` file will be referred to as the `UserKey` since this is the only symmetrical key used to decrypt and encrypt files generated by the end-ser. `Table-Blocks` and `Data-Blocks` are encrypted differently using the same `UserKey`. `Table-Blocks` can be decrypted by random access using only their multiple-of-512 block index and the the `UserKey`. The first block of a `.sai` file (block index 0) will be a `Table-Block` storing related data for the `511` blocks after it. When decrypting a `Table-Block`, four of the 256 keys within `UserKey` are indexed by the four bytes of the 32-bit block-index and then summed together. This sum is exclusive-ored with the current 4-byte cipher-word and the block-index followed by a 16-bit left rotation of the result. When decrypting a `Data-Block`, an initial decryption vector is given which selects the appropriate integers from `UserKey` using the individual bytes of the 32-bit vector integer and xors with the vector integer itself, and subtracts this value from the cipher to get the plaintext before passing on the vector to the next round using the cipher integer. The input `Vector` is the checksum integer found in the `Table-Block`. ```cpp // Ensure BlockIndex is a valid Table-Block index void DecryptTable(std::uint32_t BlockIndex, std::uint32_t* Data) { // see "IsTableBlock" above on making sure BlockIndex // is a table or use: // BlockNumber &= (~0x1FF); for( std::size_t i = 0; i < 1024; i++ ) { std::uint32_t CurCipher = Data[i]; std::uint32_t X = BlockIndex ^ CurCipher ^ ( UserKey[(BlockIndex >> 24) & 0xFF] + UserKey[(BlockIndex >> 16) & 0xFF] + UserKey[(BlockIndex >> 8) & 0xFF] + UserKey[BlockIndex & 0xFF]); Data[i] = static_cast((X << 16) | (X >> 16)); BlockIndex = CurCipher; }; } void DecryptData(std::uint32_t Vector, std::uint32_t* Data) { for( std::size_t i = 0; i < 1024; i++ ) { std::uint32_t CurCipher = Data[i]; Data[i] = CurCipher - (Vector ^ ( UserKey[Vector & 0xFF] + UserKey[(Vector >> 8) & 0xFF] + UserKey[(Vector >> 16) & 0xFF] + UserKey[(Vector >> 24) & 0xFF])); Vector = CurCipher; } } ``` `Table-Blocks` contain 512 8-byte structures containing a a 32-bit checksum and a 32-bit integer used to store preliminary data for the block that I have yet to fully decipher. Each index of table-entries corresponds to the appropriate block index after the table index. The first checksum entry found within the `Table-Block` is a checksum of the table itself, excluding the first 32-bit integer. Setting the first checksum to 0 and calculating the checksum of the entire table produces the same results as if the first entry was skipped. A table entry with a checksum of `0` is considered to be an unallocated/unused block. ```cpp struct TableEntry { std::uint32_t Checksum; std::uint32_t Flags; // Yet to be fully deciphered } TableEntries[512]; ``` ``` ~ ~ Table-Block | | +----------+----------+ <---+---------+ 0 |0xChecksum|0xPrelimin| |XXXX|XXXX| Block 512 Checksum used to +-->1 |0xChecksum|0xPrelimin| |XXXX|XXXX| 0x200200 decrypt block 513 2 |0xChecksum|0xPrelimin| |XXXX|XXXX| 3 |0xChecksum|0xPrelimin| +---------+ 4 |0xChecksum|0xPrelimin| /| | Block 513 512 entries 5 |0xChecksum|0xPrelimin| / | | 0x200400 6 |0xChecksum|0xPrelimi.| / | | 7 |0xChecksum|0xPrelim..| / +---------+ 8 |0xChecksum|0xPreli...|< | | Block 514 8 |0xChecksum|0xPrel....| | | 0x200600 9 |0xChecksu.| | | | ~ ~ ~ +---------+ | | ~ ~ ``` The checksum for `Data-Blocks` and `Table-Blocks` is a simple exclusive-or and bit-rotate which interprets all 4096 bytes of the block as 1024 32-bit integers, with the exception that the checksum for `Table-Blocks` does not include the first four bytes(the checksum integer of the block itself). All 1024 integers are exclusive-ored with an initial checksum of zero, which is rotated left 1 bit before the exclusive-or operation. Finally the lowest bit is set, making all checksums an odd number. ```cpp // If your block number is a multiple of 512, set `Table` to true. std::uint32_t Checksum(bool Table, std::uint32_t* Data) { std::uint32_t Sum = 0; for( std::size_t i = (Table ? 1 : 0); i < 1024; i++ ) { Sum = ( ( Sum << 1 ) | (Sum >> 31)) ^ Data[i]; } return Sum | 1; } // Generic version for both Table-Blocks and Data-Blocks // Works on tables if you set the first 32-bit integer to 0 before running. std::uint32_t Checksum(std::uint32_t* Data) { std::uint32_t Sum = 0; for( std::size_t i = 0; i < 1024; i++ ) { Sum = ( ( Sum << 1 ) | (Sum >> 31)) ^ Data[i]; } return Sum | 1; } ``` A block-level corruption can be detected by a checksum mismatch. If the `Data-Block`'s generated checksum does not match the checksum found at the appropriate table entry within the `Table-Block` then the `Data-Block` is considered corrupted. ## Caching Sai internally uses a Direct Mapped cache table to speed up the random access and decryption of a file by caching both `Table-Blocks` and `Data-Blocks`. An arbitrary block number will have its appropriate cache entry looked up by first shifting the `BlockNumber` integer right by 14 bits and comparing both the upper 18 bits of the block ID to the lower 31 bits of the cache entry found within the internally mounted file object. Should these two numbers match then a cache-hit has occurred. Otherwise the block is to fully loaded and decrypted into the cache. The the mounted file context object(I've called it `VFSObject` in IDA Pro, has exactly 32 cache lines for `Table-Blocks`. The highest bit of the cache table line is the `dirty` bit which notes if the block is due for a write-back before a new block is to overwrite the entry. Cache size seems to generally be the block-size divided by 8 and will be a different size depending on the file being handled. This cache mechanism is Sai's mechanism to minimize the need for constant file IO stalls at run-time and for efficient file-writing and flushing. Changes are fully "flushed" simply by writing any remaining cache lines to the file with the upper `dirty` bit set(and adjusting appropriate checksums within appropriate `Table-Blocks` if needed). If you plan to implement a library that reads from `.sai` files, you should probably follow the same cache routine to speed up your file access as Sai. `Table-Blocks` should at the very least be cached as almost every random access of a `.sai` file will require you to read the appropriate `Table-Block` before being able to decrypt the `Data-Block`. # File System Now that the cipher can be fully randomly accessed and decrypted, the virtual file system actually implemented can be deciphered. The file system found after decrypting will be described as a `Virtual File system` or `VFS`(Internally sai refers to them as a `VFS` along with terminology such as "mounting" within its error messages). Individual files are described by a `File Allocation Table` that describe the name, timestamp, starting block index, and the size(in bytes) of the data. A `Data-Block` can contain a max of `64` `FATEntries`. Folders are described by having their `Type` variable set to `Folder` and the starting `Block` variable instead points to another `Data-Block` of 64 `FATEntries` depicting the contents of the folder. ```cpp enum class EntryType : std::uint8_t { Folder = 0x10, File = 0x80 }; struct FATEntry { std::uint32_t Flags; char Name[32]; std::uint8_t Pad1; std::uint8_t Pad2; std::uint8_t Type; // EntryType enum std::uint8_t Pad4; std::uint32_t Block; std::uint32_t Size; // Windows FILETIME structure // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724284(v=vs.85).aspx std::uint64_t TimeStamp; std::uint64_t UnknownB; }; struct FATBlock { FATEntry Entries[64]; } ``` >**Note:** When reading file-data of an FATEntry, files are **not** stored continuously. > >`TableBlocks` may intercept the file stream and must be skipped. So when reading filedata you must abstract away table blocks. >This means when reading a file, you must skip all table blocks as if they did not exist and skip over them to simulate continuous files > >So offsets such as: > >[0,4096],[2097152,2101248],[4194304,4198400],...,**[TableIndex * 4096,TableIndex * 4096 + 4096]** > >must be skipped over Some info on `TimeStamp`: To convert this 64 bit integer to the more standardized `time_t` variable simply divide the 64-bit integer by `10000000UL` and subtract by `11644473600ULL`. `FILETIME` is the number of 100-nanosecond intervals since January 1, 1601 while `time_t` is the number of 1-second intervals since January 1, 1970. If you're writing a multi-platform library it's best to use the more standardized `time_t` format when available as most functions converting timestamps into strings use the `time_t` format. ```cpp time_t filetime_to_time_t(std::uint64_t Time) { return Time / 10000000ULL - 11644473600ULL; } ``` The `root` directory of the `VFS` will always start at block index `2`. This will always be the position of the first `FATBlock` containing 64 `FatEntries` of the `root` folder. If the `Flags` variable of the `FATEntry` structure is `0` the entry is considered to be unused. The full hierarchy of files can be traversed simply by iterating through all 64 entries of the `FatBlock` within block index `2` and stopping at the entry whose `Flags` variable is set to `0`. If any of the 64 `FATEntries` is a folder, then recursively iterate at the 64 `FatEntries` at the `Block` variable. If the entry is a file then simply go to the starting block index and read `Size` amount of bytes continuously, decrypting appropriate `Data-Blocks` along the way should `Size` be larger than 1 block(`0x1000` bytes). Padded bytes within a block will always be `0`. From this point on it is assumed you are capable of decrypting the file for random access and can interpret the internal file system format. Now we will look at the actual files and the strucutre in which they are placed within this virtual file system. # Folder structure The actual file/folder structure found within `.sai` files describes information on the canvas, layers, a thumbnail image, and other meta-data. Here is a sample file structure of a `.sai` file created in October. ``` /.a1541b366925e034 | 32 bytes | 2016/10/12 03:53:53 /canvas | 56 bytes | 2016/10/12 03:53:53 /laytbl | 60 bytes | 2016/10/12 03:53:53 /layers/ | --- | 2016/10/12 03:53:53 /0000000a | 464007 bytes | 2016/10/12 03:53:53 /00000010 | 452 bytes | 2016/10/12 03:53:53 /0000000e | 361 bytes | 2016/10/12 03:53:53 /00000011 | 373 bytes | 2016/10/12 03:53:53 /00000012 | 373 bytes | 2016/10/12 03:53:53 /0000000f | 538 bytes | 2016/10/12 03:53:53 /0000000b | 82454 bytes | 2016/10/12 03:53:53 /subtbl | 12 bytes | 2016/10/12 03:53:53 /sublayers/ | --- | 2016/10/12 03:53:53 /0000000d | 87213 bytes | 2016/10/12 03:53:53 /thumbnail | 90012 bytes | 2016/10/12 03:53:53 ``` the first entry `.a1541b366925e034` will vary in name but will always be the first entry. See [.xxxxxxxxxxxxxxxx](#xxxxxxxxxxxxxxxx) for more info on this file. ## Serialization Streams Before going into the file formats a specific format of serialization needs to be explained that is found across the internal files. Sai.exe internally uses a specially formatted array of 32 bit integers that describe how serialized data is to be read and written to a file. A size of `0` delimits the end of the table. Format of the Serial-Table found within Sai.exe for the `reso` identifier. ``` Serialization Table for `reso` identifier 0-0x00000004 Serial Entry+-----+------------------------+ 0x0000014C <-----------+ | | 1-0x00000002 Serial Entry \ | Size in Bytes | 0x00000150 <-----------+ \ +------------------------+ 2-0x00000002 Serial Entry \ | | 0x00000152 <-----------+ \ | Runtime Offset | 0x00000000 End +------------------------+ ``` `Runtime Offset` is the offset within the runtime object where `Size` amount of data gets written to in memory after reading from the file. In C++ code this would be the `offsetof` and `sizeof` macro of specific fields of an object being stored in an array. One could trace what an unknown serial entry does by finding what runtime object gets written to and finding out when that specific field gets used again. SYSTEMAX Source code, probably ```cpp struct ResData { ... std::uint32_t DPI;//0x14C bytes within some class/struct/etc std::uint16_t Unknown150; std::uint16_t Unknown152; ... }; std::uint32_t ResDataStream[] = { sizeof(ResData.DPI), offsetof(ResData, DPI), sizeof(ResData.Unknown150), offsetof(ResData, Unknown152) }; ``` Output written by the Serial-Table for some arbitrary runtime ResData object ``` 6F 73 65 72 08 00 00 00 00 00 48 00 00 00 00 00 ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ +---------+ +---------+ +---------+ +---+ +---+ `oser` Size Serial Ser. Ser. Data Data Data 0 1 2 ``` `oser` is the little endian storage of `reso`. In code the identifier `oser` is actually defined as something along the lines of: ```cpp const std::uint32_t ResDataMagic = `reso`; ``` `Size` is simply the sum of all `Size` integers for each `Serial Entry`. This integer gets written so that entire streams of unneeded data may be skipped. If two streams `reso` and `lyid` were next to each other, one could skip to the `lyid` stream by reading 32-bit identifier `reso` to see that it does not match up with `lyid` and use the next 32-bit `Size` integer to know the amount of bytes to skip to get to the next stream. A tag identifier of `0` delimits the end of a `Serial Stream`. Sample code for reading a serial stream. ```cpp std::uint32_t CurTag; std::uint32_t CurTagSize; while( File.Read(CurTag) && CurTag ) { File.Read(CurTagSize); switch( CurTag ) { case 'reso': { //Handle 'reso' data File.Read(...); File.Read(...); File.Read(...); break; } case 'lyid': { //... break; } case 'layr': { //... break; } default: { // for any streams that we do not handle, // we just skip forward in the stream File.Seek(File.Tell() + CurTagSize); break; } } } ``` Serial streams from here on out will be depicted as an enumeration of the four-byte identifier and the formatted data that it contains. # Files ## ".XXXXXXXXXXXXXXXX" This file name is procedurally generated based on the system that wrote the file. It is a 64 bit hash integer generated from a string involving the information of the motherboard formatted into a `%s/%s/%s` string. Three strings are queried from Windows Management Instrumentation(WMI) first with the query ``` SELECT * FROM Win32_BaseBoard ``` and then taking the `Manufacturer`, `Product`, and `SerialNumber` table entries (making sure to convert the UTF16 into UTF8) and formatting them together into a string identifying the user's chipset(formatted `%s/%s/%s`). An example chipset: ``` ASUSTeK COMPUTER INC./Z87-DELUXE/130410781704124 ``` The machine-identifying hash is then calculated with this from this string. Within the hash function this null-terminated string is repeated continuously until it fits a 256 byte span. ``` ASUSTeK COMPUTER INC./Z87-DELUXE /130410781704124\ASUSTeK COMPUTE R INC./Z87-DELUXE/13041078170412 4\0ASUSTeK COMPUTER INC./Z87-DELU XE/130410781704124\0ASUSTeK COMPU TER INC./Z87-DELUXE/130410781704 124\0ASUSTeK COMPUTER INC./Z87-DE LUXE/130410781704124\0ASUSTeK COM ``` This 256 byte array of characters is then interpreted as 64 32-bit integers for a chained rotate-and-xor hashing function, generating a 64 bit hash. ```cpp std::uint64_t MachineHash(const char* MachineIdentifier) { std::uint32_t StringBlock[64]; const char* ReadPoint = MachineIdentifier; for(std::size_t i = 0; i < 256; i++) { reinterpret_cast(StringBlock)[i] = *ReadPoint; ReadPoint = *ReadPoint ? ++ReadPoint : MachineIdentifier; } std::uint32_t UpperHash = 0; std::uint32_t LowerHash = 0; std::uint32_t Temp1 = 0; for(std::size_t i = 0; i < 64; i++) { std::uint32_t CurUpper = UpperHash + StringBlock[i % 64]; std::uint32_t CurLower = LowerHash + StringBlock[(i + 1) % 64]; for( std::size_t j = 0; j < 4; j++ ) { CurUpper = CurLower + ((CurUpper << CurLower) | (CurUpper >> (32 - CurLower))); CurLower = CurUpper + ((CurLower << CurUpper) | (CurLower >> (32 - CurUpper))); } LowerHash = CurLower ^ Temp1; UpperHash ^= CurUpper; Temp1 ^= CurLower; } return (static_cast(UpperHash) << 32) | LowerHash; } ``` The resulting hash for the above formatted string is `a1541b366925e034` which would make the filename `.a1541b366925e034` using the internal format `/%s.%016I64x`. The first string seems to always be null leaving the hash to simply have a period character prepended to it. The file itself is only 32 bytes long. ```cpp struct AuthorSystemInfo { std::uint32_t BitFlag; // always 0x08000000 std::uint32_t Unknown4; std::uint64_t DateCreated; // Date Created std::uint64_t DateModified; // Date Modified std::uint64_t MachineHash; // Calculated using the above routine } ``` Timestamps are 64 bit integer counts of seconds since `January 1, 1601`. This value is calculated using [GetSystemTimeAsFileTime](https://msdn.microsoft.com/en-us/library/windows/desktop/ms724397%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396) and then dividing the 64-bit result by `10000000` to convert from 100-nanosecond-intervals into seconds. ## "canvas" This file contains metadata involving the dimensions of the canvas. The first three integers are a static structure: ```cpp struct CanvasInfo { std::uint32_t Unknown0; // Always 0x10(16), possibly bpc or alignment std::uint32_t Width; std::uint32_t Height }; ``` After this, a [Serial Stream](#serialization-streams): - `reso` ```cpp // 16.16 fixed point integer std::uint32_t DotsPerInch; // 0 = pixels, 1 = inch, 2 = cm, 3 = mm std::uint16_t SizeUnits; // 0 = pixel/inch, 1 = pixel/cm std::uint16_t ResolutionUnits; ``` - `wsrc` +Layer marked as the selection source ```cpp -std::uint32_t Unknown0; +std::uint32_t SelectionSourceID; ``` -- `lyid` +- `layr` ```cpp -std::uint32_t Unknown0; +std::uint32_t SelectedLayerID; ``` -- `layr` +- `lyid` +Seems to be a duplication of `layr` ```cpp std::uint32_t SelectedLayerID; ``` ## "laytbl" "subtbl" These files contains a description of all layers that make up an image stored from "lowest" layer to "highest". `subtbl` contains preliminary layers such as masks. Both `laytbl` and `subtbl` have the same format and describe the contents within their respective `layers` and `sublayers` folder. The first integer of either file is a is a 32bit integer for the number of layers followed by an equivalent amount of `LayerTableEntries`. Layers are identified by 32 bit integers with their appropriate filename found in the `layers` and `sublayers` folder using an 8 digit lowercase hexidecimal file name. The full path for any given layer or sublayer identifier can be generated given the identifying integer and the [printf](http://en.cppreference.com/w/cpp/io/c/fprintf) format `/layers/%08x` or `/sublayers/%08x`. ```cpp enum class LayerType : std::uint16_t { Null = 0x00, Layer = 0x03, // Regular Layer Unknown4 = 0x4, // Unknown Linework = 0x05, // Vector Linework Layer Mask = 0x06, // Masks applied to any layer object Unknown7 = 0x07, //Unknown Set = 0x08//Layer Folder }; struct LayerTableEntry { std::uint32_t Identifier; std::uint16_t Type; // LayerType enum std::uint16_t Unknown6; // Gets sent as windows message 0x80CA for some reason }; ``` Sample routine: ```cpp // First integer is number of layer entires std::uint32_t LayerCount = File.Read(); while( LayerCount-- ) // Read each layer entry { // Read current layer entry into above structure LayerTableEntry CurrentLayer = File.Read(); // Do something with this layer //... } ``` --- ## "/layers" "/sublayers" The individual layer files within these folders match the numerical hexidecimal identifiers found in `laytbl` or `subtbl`. These files contain the actual raster or vector data(or none) of the specified layer entry. The header of the file is a static struture identifying the layer's opacity, size, blending mode, etc. ```cpp enum BlendingModes : std::uint32_t { PassThrough = 'pass', Normal = 'norm', Multiply = 'mul\0', Screen = 'scrn', Overlay = 'over', Luminosity = 'add\0', Shade = 'sub\0', LumiShade = 'adsb', Binary = 'cbin' }; // Rectangular bounds // Can be off-canvas or larger than canvas if the user moves // The layer outside of the "canvas window" without cropping // similar to photoshop // 0,0 is top-left corner of image struct LayerBounds { // Can be negative, rounded to nearest multiple of 32 std::int32_t X; std::int32_t Y; std::uint32_t Width; std::uint32_t Height; }; struct LayerHeader { std::uint32_t Type; // LayerType enum std::uint32_t Identifier; LayerBounds Bounds; std::uint32_t Unknown18; std::uint8_t Opacity; std::uint8_t Visible; std::uint8_t PreserveOpacity; std::uint8_t Clipping; std::uint8_t Unknown1C; std::uint32_t Blending; // BlendingModes enum }; ``` Immediately after the `LayerHeader` is a [Serial Stream](#serialization-streams). >**Note:** >Not all streams might be present depending on the type of layer the file is referencing. >Streams such as `texp` and `peff` may not exist if the layer is a lineart layer or folder - `lorg` ```cpp std::uint32_t Unknown0; std::uint32_t Unknown4; ``` - `name` Zero terminated string of the layer's name. ```cpp std::uint8_t LayerName[256]; ``` - `pfid` -Parent Set id. If this layer is a child of a folder this will be a layer identifier of the parent container layer. +Parent Set ID. If this layer is a child of a folder this will be a layer identifier of the parent container layer. +```cpp +std::uint32_t ParentSetID; +``` + +- `plid` + +Parent Layer ID. If this layer is a child of another layer(ex, a mask-layer) this will be a layer identifier of the parent container layer. ```cpp -std::uint32_t ParentLayer; +std::uint32_t ParentLayerID; ``` + - `lmfl` ```cpp -std::uint32_t Unknown0; // Bitflag +std::uint32_t Unknown0; // Bitflag ``` - `fopn` Present only in a layer that is a Set/Folder. A single `bool` variable for if the folder is expanded within the layers panel or not ```cpp std::uint8_t Open; ``` - `texn` + +Name of the overlay-texture assigned to a layer. Ex: `Watercolor A` ```cpp -std::uint8_t Unknown0[64]; // UTF16 string +std::uint8_t TextureName[64]; // UTF16 string ``` - `texp` + + ```cpp std::uint16_t Unknown0; std::uint8_t Unknown2; ``` - `peff` + +Options related to the overlay-texture assigned to a layer ```cpp +// Has an overlay: 0, no overlay: 1 +// 32-bit bool? std::uint8_t Unknown0; -std::uint8_t Unknown1; -std::uint8_t Unknown2; +std::uint8_t Unknown1; // 100 +std::uint8_t Unknown2; // 1 ``` - `vmrk` ```cpp std::uint8_t Unknown0; ``` Immediately after the stream may be the contents of the layer. If the layer is a folder or set, there is no additional data. If the layer is a raster layer of pixels then specially formatted `raster` data follows. If the layer is a linework layer, specifically formatted `linework` data follows. Sample layer file reading procedure ```cpp // Read header LayerHeader CurHeader = LayerFile.Read(LayerHead); // Read Serial Stream std::uint32_t CurTag, CurTagSize; CurTag = CurTagSize = 0; char Name[256]; while( LayerFile.Read(CurTag) && CurTag ) { LayerFile.Read(CurTagSize); switch( CurTag ) { case 'name': { LayerFile.Read(Name); break; } // any other cases you care for case 'pfid': // Parent folder ID { // ... break; } default: { LayerFile.Seek(LayerFile.Tell() + CurTagSize); break; } } } if( CurHeader.Type == LayerType::Layer ) { // Read Raster data } else if( CurHeader.Type == LayerType::Linework ) { // Read Linework data } ``` ## Raster Layers -Raster data is stored in a tiled format immediately after the header structure above. There is an array of `(LayerWidth / 32) * (LayerHeight / 32)` 8-bit boolean integer values stored before the compressed channel pixel data. Each boolean value within this `BlockMap` determines if the appropriately positioned `32x32` tile of bitmap data contains pixel data that varies from pure black transparency. If a tile is active(1), its pixel data is stored as four or more streams of Run-Length-Encoding compressed data for each color channel for that `32x32` tile. If a tile is not active(0), the tile is to be filled with a `32x32` fully transparent block of pixels(`0x00000000` for all pixels). If more than four streams exist, the extra streams may be safely ignored and skipped. Note that the RLE routine is the very same algorithm that Photoshop uses when compressing layer data and the same as the [PackBits](https://en.wikipedia.org/wiki/PackBits) algorithm that apple uses. +Raster data is stored in a tiled format immediately after the header structure above. There is an array of `(LayerWidth / 32) * (LayerHeight / 32)` 8-bit boolean integer values stored before the compressed channel pixel data. Each boolean value within this `BlockMap` determines if the appropriately positioned `32x32` tile of bitmap data contains pixel data that varies from pure black transparency. If a tile is active(1), its pixel data is stored as four or more streams of Run-Length-Encoding compressed data for each color channel for that `32x32` tile. If a tile is not active(0), the tile is to be filled with a `32x32` fully transparent block of pixels(`0x00000000` for all pixels). If more than four streams exist, the extra streams may be safely ignored and skipped(I am not sure what the extra streams are, possible masking data?). Note that the RLE routine is the very same algorithm that Photoshop uses when compressing layer data and the same as the [PackBits](https://en.wikipedia.org/wiki/PackBits) algorithm that apple uses. RLE streams are prefixed with a 16-bit size integer for the amount of RLE stream bytes that follow. Compressed channel data will be at max `0x800` bytes. Decompressed data will be at most `0x1000` bytes. Use these as your buffer sizes when reading and decompressing in-place. Color data is stored with `premultiplied alpha` and should be converted to `straight` as soon as relavently needed. It is highly recommended to use SIMD intrinsics featured in C headers such as `emmintrin.h` and `tmmintrin.h` to speed up conversions and arithmetic upon pixel data. Internally Sai uses `MMX` for all of its SIMD speedups so many structures already lend themselves to more modern SIMD speedups(SSE,AVX,etc). Pixel data is stored in BGRA order 1. First, load in the array of `(LayerWidth / 32) * (LayerHeight / 32)` bytes immediately following the layer's Serial Stream as `BlockMap` 2. Iterate both Y and X dimensions by `LayerHeight / 32` and `LayerWidth / 32` times respectively - **Be sure to iterate the Y dimension first, then the X to ensure a row-by-row iteration.** - Access the the boolean at index `(LayerWidth/32) * Y + X` from `BlockMap` - If the boolean is true(1) - Read a 16 bit integer - If nonzero, read this amount of data, decompress it, and put this data into the correct `B`, `G`, `R`, or `A` channel in order for however you're formatting your pixel data. Read another 16-bit integer and test for non-zero again in step one to get the next channel. - If there are more than 4 streams(channels) you can safely skip the extra RLE streams by this 16 bit integer amount in bytes by iterating again at step 2. - I have yet to find out what the extra channels are but it is possibly "mip-map-like" data for different zoom levels to speed up certain calculations - If zero, no more streams to read. Move on to the next tile by iterating at step 2. Here is a sample scratch-implementation I made using SIMD to shuffle channels into `RGBA` format and convert from `premultiplied alpha` to `straight alpha` as well as Routine for decompressing an RLE stream and placing resulting data into the appropriate interleaved 32bpp 8bpc channel index. ```cpp void RLEDecompress32(void* Destination, const std::uint8_t *Source, std::size_t SourceSize, std::size_t IntCount, std::size_t Channel) { std::uint8_t *Write = reinterpret_cast(Destination) + Channel; std::size_t WriteCount = 0; while( WriteCount < IntCount ) { std::uint8_t Length = *Source++; if( Length == 128 ) // No-op { } else if( Length < 128 ) // Copy { // Copy the next Length+1 bytes Length++; WriteCount += Length; while( Length ) { *Write = *Source++; Write += 4; Length--; } } else if( Length > 128 ) // Repeating byte { // Repeat next byte exactly "-Length + 1" times Length ^= 0xFF; Length += 2; WriteCount += Length; std::uint8_t Value = *Source++; while( Length ) { *Write = Value; Write += 4; Length--; } } } } ``` ```cpp // Read BlockMap // Do not use a vector as this is commonly implemented as a specialized vector type that does not implement individual bool values as bytes but rather as packed bits within a word std::vector BlockMap; TileData.resize((LayerHead.Bounds.Width / 32) * (LayerHead.Bounds.Height / 32)); // Read Block Map LayerFile.Read(BlockMap.data(), (LayerHead.Bounds.Width / 32) * (LayerHead.Bounds.Height / 32)); // the resulting raster image data for this layer, RGBA 32bpp interleaved // Use a vector to ensure that tiles with no data are still initialized // to #00000000 // Also note that the claim that SystemMax has made involving 16bit color depth // may actually only be true at run-time. All raster data found in files are stored at // 8bpc while only some run-time color arithmetic converts to 16-bit std::vector LayerImage; LayerImage.resize(LayerHead.Bounds.Width * LayerHead.Bounds.Height * 4); // iterate 32x32 tile chunks row by row for( std::size_t y = 0; y < (LayerHead.Bounds.Height / 32); y++ ) { for( std::size_t x = 0; x < (LayerHead.Bounds.Width / 32); x++ ) { if( BlockMap[(LayerHead.Bounds.Width / 32) * y + x] ) // if tile is active { // Decompress Tile std::array CompressedTile; // Aligned memory for simd alignas(sizeof(__m128i)) std::array DecompressedTile; std::uint8_t Channel = 0; std::uint16_t Size = 0; while( LayerFile.Read(Size) ) // Get Current RLE stream size { LayerFile.Read(CompressedTile.data(), Size); // decompress and place into the appropriate interleaved channel RLEDecompress32( DecompressedTile.data(), CompressedTile.data(), Size, 1024, Channel ); Channel++; // Move on to next channel if( Channel >= 4 ) // skip all other channels besides the RGBA ones we care about { for( std::size_t i = 0; i < 4; i++ ) { std::uint16_t Size = LayerFile.Read(); LayerFile.Seek(LayerFile.Tell() + Size); } break; } } // Current 32x32 tile within final image std::uint32_t *ImageBlock = reinterpret_cast(LayerImage.data()) + (x * 32) + ((y * LayerHead.Bounds.Width) * 32); for( std::size_t i = 0; i < (32 * 32) / 4; i++ ) // Process 4 pixels at a time { __m128i QuadPixel = _mm_load_si128( reinterpret_cast<__m128i*>(DecompressedTile.data()) + i ); // ABGR to ARGB, if you want. // Do your swizzling here QuadPixel = _mm_shuffle_epi8( QuadPixel, _mm_set_epi8( 15, 12, 13, 14, 11, 8, 9, 10, 7, 4, 5, 6, 3, 0, 1, 2) ); /// Alpha is pre-multiplied, convert to straight // Get Alpha into [0.0,1.0] range __m128 Scale = _mm_div_ps( _mm_cvtepi32_ps( _mm_shuffle_epi8( QuadPixel, _mm_set_epi8( -1, -1, -1, 15, -1, -1, -1, 11, -1, -1, -1, 7, -1, -1, -1, 3 ) ) ), _mm_set1_ps(255.0f)); // Normalize each channel into straight color for( std::uint8_t i = 0; i < 3; i++ ) { __m128i CurChannel = _mm_srli_epi32(QuadPixel, i * 8); CurChannel = _mm_and_si128(CurChannel, _mm_set1_epi32(0xFF)); __m128 ChannelFloat = _mm_cvtepi32_ps(CurChannel); ChannelFloat = _mm_div_ps(ChannelFloat, _mm_set1_ps(255.0));// [0,255] to [0,1] ChannelFloat = _mm_div_ps(ChannelFloat, Scale); ChannelFloat = _mm_mul_ps(ChannelFloat, _mm_set1_ps(255.0));// [0,1] to [0,255] CurChannel = _mm_cvtps_epi32(ChannelFloat); CurChannel = _mm_and_si128(CurChannel, _mm_set1_epi32(0xff)); CurChannel = _mm_slli_epi32(CurChannel, i * 8); QuadPixel = _mm_andnot_si128(_mm_set1_epi32(0xFF << (i * 8)), QuadPixel); QuadPixel = _mm_or_si128(QuadPixel, CurChannel); } // Write directly to final image _mm_store_si128( reinterpret_cast<__m128i*>(ImageBlock) + (i % 8) + ((i / 8) * (LayerHead.Bounds.Width / 4)), QuadPixel ); } } } } ``` --- ## Linework Layers Todo --- # Decryption Keys ## UserKey This is the key that we care for. Used to encrypt/decrypt all user-created files. Decrypts `.sai` files. ```cpp const std::uint32_t UserKey[256] = { 0x9913D29E,0x83F58D3D,0xD0BE1526,0x86442EB7,0x7EC69BFB,0x89D75F64,0xFB51B239,0xFF097C56, 0xA206EF1E,0x973D668D,0xC383770D,0x1CB4CCEB,0x36F7108B,0x40336BCD,0x84D123BD,0xAFEF5DF3, 0x90326747,0xCBFFA8DD,0x25B94703,0xD7C5A4BA,0xE40A17A0,0xEADAE6F2,0x6B738250,0x76ECF24A, 0x6F2746CC,0x9BF95E24,0x1ECA68C5,0xE71C5929,0x7817E56C,0x2F99C471,0x395A32B9,0x61438343, 0x5E3E4F88,0x80A9332C,0x1879C69F,0x7A03D354,0x12E89720,0xF980448E,0x03643576,0x963C1D7B, 0xBBED01D6,0xC512A6B1,0x51CB492B,0x44BADEC9,0xB2D54BC1,0x4E7C2893,0x1531C9A3,0x43A32CA5, 0x55B25A87,0x70D9FA79,0xEF5B4AE3,0x8AE7F495,0x923A8505,0x1D92650C,0xC94A9A5C,0x27D4BB14, 0x1372A9F7,0x0C19A7FE,0x64FA1A53,0xF1A2EB6D,0x9FEB910F,0x4CE10C4E,0x20825601,0x7DFC98C4, 0xA046C808,0x8E90E7BE,0x601DE357,0xF360F37C,0x00CD6F77,0xCC6AB9D4,0x24CC4E78,0xAB1E0BFC, 0x6A8BC585,0xFD70ABF0,0xD4A75261,0x1ABF5834,0x45DCFE17,0x5F67E136,0x948FD915,0x65AD9EF5, 0x81AB20E9,0xD36EAF42,0x0F7F45C7,0x1BAE72D9,0xBE116AC6,0xDF58B4D5,0x3F0B960E,0xC2613F98, 0xB065F8B0,0x6259F975,0xC49AEE84,0x29718963,0x0B6D991D,0x09CF7A37,0x692A6DF8,0x67B68B02, 0x2E10DBC2,0x6C34E93C,0xA84B50A1,0xAC6FC0BB,0x5CA6184C,0x34E46183,0x42B379A9,0x79883AB6, 0x08750921,0x35AF2B19,0xF7AA886A,0x49F281D3,0xA1768059,0x14568CFD,0x8B3625F6,0x3E1B2D9D, 0xF60E14CE,0x1157270A,0xDB5C7EB3,0x738A0AFA,0x19C248E5,0x590CBD62,0x7B37C312,0xFC00B148, 0xD808CF07,0xD6BD1C82,0xBD50F1D8,0x91DEA3B8,0xFA86B340,0xF5DF2A80,0x9A7BEA6E,0x1720B8F1, 0xED94A56B,0xBF02BE28,0x0D419FA8,0x073B4DBC,0x829E3144,0x029F43E1,0x71E6D51F,0xA9381F09, 0x583075E0,0xE398D789,0xF0E31106,0x75073EB5,0x5704863E,0x6EF1043B,0xBC407F33,0x8DBCFB25, 0x886C8F22,0x5AF4DD7A,0x2CEACA35,0x8FC969DC,0x9DB8D6B4,0xC65EDC2F,0xE60F9316,0x0A84519A, 0x3A294011,0xDCF3063F,0x41621623,0x228CB75B,0x28E9D166,0xAE631B7F,0x06D8C267,0xDA693C94, 0x54A5E860,0x7C2170F4,0xF2E294CB,0x5B77A0F9,0xB91522A6,0xEC549500,0x10DD78A7,0x3823E458, 0x77D3635A,0x018E3069,0xE039D055,0xD5C341BF,0x9C2400EA,0x85C0A1D1,0x66059C86,0x0416FF1A, 0xE27E05C8,0xB19C4C2D,0xFE4DF58F,0xD2F0CE2A,0x32E013C0,0xEED637D7,0xE9FEC1E8,0xA4890DCA, 0xF4180313,0x7291738C,0xE1B053A2,0x9801267E,0x2DA15BDB,0xADC4DA4F,0xCF95D474,0xC0265781, 0x1F226CED,0xA7472952,0x3C5F0273,0xC152BA68,0xDD66F09B,0x93C7EDCF,0x4F147404,0x3193425D, 0x26B5768A,0x0E683B2E,0x952FDF30,0x2A6BAE46,0xA3559270,0xB781D897,0xEB4ECB51,0xDE49394D, 0x483F629C,0x2153845E,0xB40D64E2,0x47DB0ED0,0x302D8E4B,0x4BF8125F,0x2BD2B0AC,0x3DC836EC, 0xC7871965,0xB64C5CDE,0x9EA8BC27,0xD1853490,0x3B42EC6F,0x63A4FD91,0xAA289D18,0x4D2B1E49, 0xB8A060AD,0xB5F6C799,0x6D1F7D1C,0xBA8DAAE6,0xE51A0FC3,0xD94890E7,0x167DF6D2,0x879BCD41, 0x5096AC1B,0x05ACB5DA,0x375D24EE,0x7F2EB6AA,0xA535F738,0xCAD0AD10,0xF8456E3A,0x23FD5492, 0xB3745532,0x53C1A272,0x469DFCDF,0xE897BF7D,0xA6BBE2AE,0x68CE38AF,0x5D783D0B,0x524F21E4, 0x4A257B31,0xCE7A07B2,0x562CE045,0x33B708A4,0x8CEE8AEF,0xC8FB71FF,0x74E52FAB,0xCDB18796 }; ``` ## NotRemoveMe Seems to only be used for the `Notremoveme.ssd` file located in `"C:\ProgramData\SYSTEMAX Software Development\SAI"` Appears to contain log data similar to `sai.ssd` ```cpp const std::uint32_t NotRemoveMeKey[256] = { 0xA0C62B54,0x0374CB94,0xB3A53F76,0x5B772C6B,0xF2B92931,0x80F923A9,0x7A22EF7A,0x216C7582, 0xEDFF8B71,0x8B0C6642,0xAF81AD2F,0x8E095A62,0x02926C0C,0xDD2F56B9,0xA3614155,0xF9AED6E4, 0x079C3E5E,0xE6D9E1FD,0x256F165C,0x77280767,0x5D2037A1,0x3019B3CE,0xFC13CC15,0xF457C85F, 0x728DF4E9,0x4405AA18,0x2AE0B950,0xE847316F,0xD69FA172,0x62F658E2,0xB0F21F89,0x8AFB852E, 0x1A3E924A,0xDBAD0B48,0x88ECBD5A,0xC53FC908,0x81251757,0x57D53685,0x73F463A3,0x048F4B58, 0xC36A46AC,0x9A8B6FBD,0x35DC9DC1,0xF76EABF5,0x9280D935,0xBFCC93FB,0x4B2BCA7D,0x60861DFC, 0x7C548877,0x2EA46821,0x7136998F,0x5AD45EDF,0x019BA6EF,0x6FC598C7,0x1DF383EC,0x39BAC06D, 0x5C3A5B1F,0x7827FB39,0x27FCA953,0x8601E843,0x6C429623,0xBA5DC127,0xCE659075,0x48291378, 0x5EDA6B5B,0xE355AC99,0xCF8C704D,0x965E6A29,0xF5035103,0x20582702,0x1B7909DB,0xCA974452, 0x7DB20E30,0x2807326C,0x2DF56D0E,0x084E9C41,0xA42DE39C,0x9170A5C3,0x9DB4F95D,0x53CA2068, 0x3488FC6E,0xD1BB7AE8,0xC61F81C5,0x310857E5,0xEF1694EE,0xF63067B1,0x3E621B8B,0x22523BFF, 0x0D37A4BA,0xCB83BECA,0x9BE78691,0xB7D84E2C,0x45A676DD,0x1F31F636,0x7FAB97C6,0x3CA15F33, 0xFA6DB6FE,0x67DD72DC,0x6B8948FA,0x9849FF4B,0xBE452E79,0x38AF6E7F,0x8FE211A7,0x941728B4, 0x63217749,0x70EF1280,0x13A9F201,0xACDB14A2,0x1184E73A,0x337E87B5,0xB6008EB7,0xC868C43C, 0x85F7DC83,0xD35AD519,0xF87310ED,0xA7C0D29B,0x361D2DCF,0xC1D27C3F,0x9C78DFE0,0x2C4FD8C4, 0x05357D9D,0x2B398964,0x182AC610,0xFD4A3873,0xE71E6416,0x842C4A05,0x5946F70F,0xB95FA366, 0x1C0B71CB,0x50CEFA06,0xAB9DC211,0x659ABCAE,0xD2E17FE7,0x581A0365,0xA61BE0B0,0xD460B084, 0xE21C5CF9,0x87B1D460,0x4DF8CF04,0x4C1573EA,0xCD967432,0xD58EBA12,0x5F2E9A3B,0x6A9955EB, 0x55A391AF,0xEBC1EED5,0xB59E8C7C,0x1E825946,0xAA18A04E,0x6891EDF3,0x663C542D,0xC459D37E, 0xC06453BC,0x460D223E,0x1690F8DE,0xC97580F7,0xA1F08D4F,0x56DE4381,0xEE06B5E3,0xC2FA05D1, 0x3794B488,0xEACD428E,0x7B2362C2,0xE97FDE9F,0xBB4C60D2,0xE4B3E2AB,0x74C93909,0x76AA2FDA, 0x9F049B7B,0x93BCDA8A,0x51BEC790,0x0FD6E4CC,0x8972E6AD,0xBCA70F40,0x405C2469,0x10673486, 0xBD104C97,0x49381E0D,0x063B456A,0x23D02634,0x43ACEC9E,0xE50E49F8,0x197DBF1B,0x8DF1BB9A, 0xB46B1CA6,0xD7E895A5,0xCC51A217,0xE1C2F196,0xDEB533C9,0x24FDC58D,0x32850822,0x12DF4DA8, 0x90BD3500,0x97C7F320,0xDA3450F4,0x2F534059,0xDC7B3D63,0x95B6CD98,0x09BF19D6,0xA5D15DBF, 0x42E47851,0xF07A021E,0x9ECB2A3D,0xE0C39F38,0x99714F95,0x3A5BEA4C,0xB2C4DD25,0xB13D47C0, 0xAD418A0B,0x6DEAB81C,0x83EE25F2,0x3B26AE47,0xA8B018D3,0xFF76E5F1,0xA2ED0461,0x26119ED8, 0x61EB0A74,0x15A2B187,0x4A93CE2A,0x7943A707,0x29E5B744,0x4E14F02B,0x0A698424,0xD9A03AE6, 0xEC87D7C8,0xA94021B8,0x3D95D1CD,0x6E2415BE,0x52E3F592,0x64A83CD9,0x8263C31D,0x41B87EB6, 0x8C50FD1A,0x47C80CD7,0xD844008C,0xB812E9AA,0x0B983013,0xFB7C520A,0x4F66FEBB,0x17E982D0, 0x00FE6914,0xFE0FD028,0x0C328F93,0x75021AF6,0x3FE6AFB2,0x7E330DE1,0xDF8ADB45,0x14D37B37, 0xD04D06A4,0x694B0156,0x0ECF6170,0xC756EBF0,0xF1B76526,0xF348A8B3,0xAE0A79A0,0x54D7B2D4 }; ``` ## LocalState Used for thumbnail files located in `"C:\ProgramData\SYSTEMAX Software Development\SAI\thumbnail"` Thumbnail filenames use [printf](http://en.cppreference.com/w/cpp/io/c/fprintf) pattern `"%08x.ssd"`. Named `LocalState` as it describes an active user context. ```cpp const std::uint32_t LocalStateKey[256] = { 0x021CF107,0xE9253648,0x8AFBA619,0x8CF31842,0xBF40F860,0xA672F03E,0xFA2756AC,0x927B2E7E, 0x1E37D3C4,0x7C3A0524,0x4F284D1B,0xD8A31E9D,0xBA73B6E6,0xF399710D,0xBD8B1937,0x70FFE130, 0x056DAA4A,0xDC509CA1,0x07358DFF,0xDF30A2DC,0x67E7349F,0x49532C31,0x2393EBAA,0xE54DF202, 0x3A2C7EC9,0x98AB13EF,0x7FA52975,0x83E4792E,0x7485DA08,0x4A1823A8,0x77812011,0x8710BB89, 0x9B4E0C68,0x64125D8E,0x5F174A0E,0x33EA50E7,0xA5E168B0,0x1BD9B944,0x6D7D8FE0,0xEE66B84C, 0xF0DB530C,0xF8B06B72,0x97ED7DF8,0x126E0122,0x364BED23,0xA103B75C,0x3BC844FA,0xD0946501, 0x4E2F70F1,0x79A6F413,0x60B9E977,0xC1582F10,0x759B286A,0xE723EEF5,0x8BAC4B39,0xB074B188, 0xCC528E64,0x698700EE,0x44F9E5BB,0x7E336153,0xE2413AFD,0x91DCE2BE,0xFDCE9EC1,0xCAB2DE4F, 0x46C5A486,0xA0D630DB,0x1FCD5FCA,0xEA110891,0x3F20C6F9,0xE8F1B25D,0x6EFD10C8,0x889027AF, 0xF284AF3F,0x89EE9A61,0x58AF1421,0xE41B9269,0x260C6D71,0x5079D96E,0xD959E465,0x519CD72C, 0x73B64F5A,0x40BE5535,0x78386CBC,0x0A1A02CF,0xDBC126B6,0xAD02BC8D,0x22A85BC5,0xA28ABEC3, 0x5C643952,0xE35BC9AD,0xCBDACA63,0x4CA076A4,0x4B6121CB,0x9500BF7D,0x6F8E32BF,0xC06587E5, 0x21FAEF46,0x9C2AD2F6,0x7691D4A2,0xB13E4687,0xC7460AD6,0xDDFE54D5,0x81F516F3,0xC60D7438, 0xB9CB3BC7,0xC4770D94,0xF4571240,0x06862A50,0x30D343D3,0x5ACF52B2,0xACF4E68A,0x0FC2A59B, 0xB70AEACD,0x53AA5E80,0xCF624E8F,0xF1214CEB,0x936072DF,0x62193F18,0xF5491CDA,0x5D476958, 0xDA7A852D,0x5B053E12,0xC5A9F6D0,0xABD4A7D1,0xD25E6E82,0xA4D17314,0x2E148C4E,0x6B9F6399, 0xBC26DB47,0x8296DDCE,0x3E71D616,0x350E4083,0x2063F503,0x167833F2,0x115CDC5E,0x4208E715, 0x03A49B66,0x43A724BA,0xA3B71B8C,0x107584AE,0xC24AE0C6,0xB3FC6273,0x280F3795,0x1392C5D4, 0xD5BAC762,0xB46B5A3B,0xC9480B8B,0xC39783FC,0x17F2935B,0x9DB482F4,0xA7E9CC09,0x553F4734, 0x8DB5C3A3,0x7195EC7A,0xA8518A9A,0x0CE6CB2A,0x14D50976,0x99C077A5,0x012E1733,0x94EC3D7C, 0x3D825805,0x0E80A920,0x1D39D1AB,0xFCD85126,0x3C7F3C79,0x7A43780B,0xB26815D9,0xAF1F7F1C, 0xBB8D7C81,0xAAE5250F,0x34BC670A,0x1929C8D2,0xD6AE9FC0,0x1AE07506,0x416F3155,0x9EB38698, 0x8F22CF29,0x04E8065F,0xE07CFBDE,0x2AEF90E8,0x6CAD049C,0x4DC3A8CC,0x597E3596,0x08562B92, 0x52A21D6F,0xB6C9881D,0xFBD75784,0xF613FC32,0x54C6F757,0x66E2D57B,0xCD69FE9E,0x478CA13D, 0x2F5F6428,0x8E55913C,0xF9091185,0x0089E8B3,0x1C6A48BD,0x3844946D,0x24CC8B6B,0x6524AC2B, 0xD1F6A0F0,0x32980E51,0x8634CE17,0xED67417F,0x250BAEB9,0x84D2FD1A,0xEC6C4593,0x29D0C0B1, 0xEBDF42A9,0x0D3DCD45,0x72BF963A,0x27F0B590,0x159D5978,0x3104ABD7,0x903B1F27,0x9F886A56, 0x80540FA6,0x18F8AD1F,0xEF5A9870,0x85016FC2,0xC8362D41,0x6376C497,0xE1A15C67,0x6ABD806C, 0x569AC1E2,0xFE5D1AF7,0x61CADF59,0xCE063874,0xD4F722DD,0x37DEC2EC,0xAE70BDEA,0x0B2D99B4, 0x39B895FE,0x091E9DFB,0xA9150754,0x7D1D7A36,0x9A07B41E,0x5E8FE3B5,0xD34503A0,0xBE2BFAB7, 0x5742D0A7,0x48DDBA25,0x7BE3604D,0x2D4C66E9,0xB831FFB8,0xF7BBA343,0x451697E4,0x2C4FD84B, 0x96B17B00,0xB5C789E3,0xFFEBF9ED,0xD7C4B349,0xDE3281D8,0x689E4904,0xE683F32F,0x2B3CB0E1 }; ``` ## sai.ssd Used only for `sai.ssd` Handled the same as user-files but with a different block size of `1024` and `Table-blocks` indexes at every multiple of `128`. `sai.ssd` seems to have multiple log files stored with symbolic headers: - "++FSIF logfile++" - Seems to be related to file-security and encryption - "++VFS logfile++" - Everything related to the virtual file system - "++SCDF logfile++" - Unknown ```cpp const std::uint32_t SystemKey[256] = { 0x724FB987,0x4A3E70BE,0xCA549C50,0x34E263E1,0x2D5ED2FF,0x127F0E11,0x58A42B78,0x5F6D14AE, 0x7E2F745D,0xC3450384,0xCFBB15DE,0xDF0A6D8A,0xEF2545F3,0x6D8919DB,0xBC413C94,0xCCB0A198, 0xE42DBBD2,0x361C0B8C,0x8359731F,0x13D61E9F,0x7505F7CE,0x271D7957,0x429C0699,0xD84EC85F, 0x953391DD,0xB25DE567,0xC1BA2F97,0x2309B605,0x69A134D1,0x14A092F2,0x681500EF,0xB90148A7, 0x01AF398B,0x16FD5168,0x9E572161,0x0F7405E3,0x56AC576D,0xF275A349,0x1E8120C0,0x4BF64E3A, 0x5A90E85E,0xD27BC4F1,0x3BD2FFB1,0xD6B40FDC,0x26EC61CF,0xF744AD3F,0xCDE7C548,0x8AFFE60A, 0xE382CA47,0x87DA3E1B,0x8FA3DB36,0x5737C7E0,0xACD8CC17,0xD0CC3B66,0xD93D776B,0x37E5BE2B, 0xD38A1129,0x037E81D0,0x15B15072,0xA6493052,0x35BCD4B9,0xC4538D32,0xEC66C1D5,0xA20DF513, 0x5524EB75,0x92C10488,0xDA03D9FD,0x65168F4B,0x1902BA24,0x7439FA7D,0x1D8CB46F,0xFBC39389, 0xC5DF6A58,0x89E8FB00,0x50DBE0A1,0xAAE98AF8,0x6A7C6C9C,0x7712D6EC,0x4030D0CD,0x6052B585, 0x6132AA77,0xEB4A38C3,0x673AB1E6,0x1C3C07C6,0x91EA2C76,0x7A4C7EA0,0x10B3DCFC,0xBE7DF402, 0x2817D87A,0x25632264,0xBD8D02B0,0xF6D0F8A8,0xB1ED3AF0,0xE6C4F1CA,0x99E028B5,0xE5D48674, 0x09CF47B8,0x9D6EAF0E,0x0A721AFE,0xB6109E54,0x8D642344,0x9FEFC27C,0xF0CA520F,0x2C6BDA7E, 0x2E9DB06A,0x97DEFC2E,0x53C5F0EE,0xAD4B8C60,0xE9F36696,0xA8C68907,0x70B70A20,0x3D9F82AA, 0x7604A595,0x441A563B,0x39193D4A,0x33BF1DC7,0x31B283FB,0xA399F25B,0x642CE39E,0xF9E3B204, 0x79A87534,0x5DBE2943,0x9813E93E,0x47864AD6,0xD420D1BF,0x24A6C986,0xFE386EF7,0xD1B65AB7, 0x3A96BF2F,0x006FE1AB,0x22938E90,0x78FE7A40,0x5CE1319B,0x46F5EEF5,0xBB064BE4,0xB7271C22, 0xC0225D21,0xFA145B10,0x7C58BC33,0xF84654C2,0xEEF4691E,0x021BEC16,0xE16C1737,0x1BCB2603, 0x48A2954D,0xDD56A8FA,0xB8C8A48D,0x5277590B,0x1194E7A9,0x590F42B4,0x7B97C0D8,0x7142B714, 0xAEDD6BC8,0xBA116212,0x6B0E642C,0xF42ABDC5,0x6E76AC81,0xBF348819,0xCB790C59,0xDC6718AD, 0x80471230,0x84DC985C,0x2AEE32C1,0x4D35964F,0x0C6894AC,0x3EF2CDE5,0xB59B37A5,0x9BC9729D, 0x186A41AF,0xEA98A970,0x21F8A291,0x5487E2C9,0xE05F3F42,0xA523B86E,0x8C1E4062,0xA962F6CB, 0x0D4816E8,0x9A4DF92D,0x20439DCC,0xA0713645,0x43506FE9,0xC2EB4651,0xB4780D6C,0xAFC29B28, 0x1FCE5FD4,0x9C7385D3,0xCE00E463,0x38CD997F,0x452933DA,0xC9F7DEBA,0x0840A093,0xDB287B41, 0x90E48479,0x66FC6709,0x6C884C65,0x3FB56082,0xF5B87123,0xED367D1D,0x6F0C44F9,0x8270DD38, 0x0E314F83,0x1AE69F35,0xD5A51FB3,0xA761A671,0x850B4DED,0x06AE0892,0x5EAA2A06,0xC7FA80F6, 0xB0692D4E,0x81657F8F,0x948B0980,0xB3D97C01,0xFC80C3EA,0xFF9E53A4,0x30BD784C,0xF3AD970C, 0xA12E9A31,0x04D37646,0x072655A3,0xE8D5F353,0x4CA98BDF,0x7391FE56,0x7D5BEDA6,0x2BD7650D, 0x862B5C73,0x8B60A726,0x7F8ECB3C,0x517A49B6,0xD7B9CF5A,0x6308D5BC,0x0B3F68D7,0x62A7EA15, 0xC65AFD3D,0xAB8525B2,0xA451B308,0xE7C7AB18,0x88F91369,0x1783279A,0x4F95DF2A,0x41F158BD, 0xC8D1CEBB,0x325CD3E2,0xF1928739,0x9355AE8E,0x2FC05EC4,0x4E0735E7,0xDE3B10D9,0x8E18C61A, 0xE29AEF25,0x4984D7A2,0x051F247B,0x29AB9055,0xFD2101F4,0x96FB2E1C,0x5BF04327,0x3C8F1BEB }; ``` \ No newline at end of file diff --git a/plugins/impex/sai/3rdparty/libsai/include/sai.hpp b/plugins/impex/sai/3rdparty/libsai/include/sai.hpp index 37609beb94..6d1920dd15 100644 --- a/plugins/impex/sai/3rdparty/libsai/include/sai.hpp +++ b/plugins/impex/sai/3rdparty/libsai/include/sai.hpp @@ -1,423 +1,448 @@ /* LibSai - Library for interfacing with SystemMax PaintTool Sai files LICENSE MIT License Copyright (c) 2017 Wunkolo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #pragma once #include #include #include #include #include #include #include namespace sai { #pragma pack(push, 1) struct FATEntry { enum class EntryType : std::uint8_t { Folder = 0x10, File = 0x80 }; std::uint32_t Flags; char Name[32]; std::uint8_t Pad1; std::uint8_t Pad2; EntryType Type; std::uint8_t Pad4; std::uint32_t PageIndex; std::uint32_t Size; std::uint64_t TimeStamp; // Windows FILETIME std::uint64_t UnknownB; }; /// Internal Structures struct ThumbnailHeader { std::uint32_t Width; std::uint32_t Height; std::uint32_t Magic; // BM32 }; enum BlendingMode : std::uint32_t { PassThrough = 'pass', Normal = 'norm', Multiply = 'mul ', Screen = 'scrn', Overlay = 'over', Luminosity = 'add ', Shade = 'sub ', LumiShade = 'adsb', Binary = 'cbin' }; enum class LayerClass : std::uint32_t { RootLayer = 0x00, // Parent Canvas layer object Layer = 0x03, Unknown4 = 0x4, Linework = 0x05, Mask = 0x06, Unknown7 = 0x07, Set = 0x08 }; struct LayerReference { std::uint32_t Identifier; std::uint16_t LayerClass; // These all get added and sent as a windows message 0x80CA for some reason std::uint16_t Unknown; }; struct LayerBounds { std::int32_t X; // (X / 32) * 32 std::int32_t Y; // (Y / 32) * 32 std::uint32_t Width; // Width - 31 std::uint32_t Height; // Height - 31 }; struct LayerHeader { std::uint32_t LayerClass; std::uint32_t Identifier; LayerBounds Bounds; std::uint32_t Unknown; std::uint8_t Opacity; std::uint8_t Visible; std::uint8_t PreserveOpacity; std::uint8_t Clipping; std::uint8_t Unknown4; std::uint32_t Blending; }; union VirtualPage { static constexpr std::size_t PageSize = 0x1000; static constexpr std::size_t TableSpan = PageSize / 8; // Data std::uint8_t u8[PageSize]; std::int8_t i8[PageSize]; std::uint32_t u32[PageSize / sizeof(std::uint32_t)]; std::int32_t i32[PageSize / sizeof(std::int32_t)]; // Page Table entries struct PageEntry { std::uint32_t Checksum; std::uint32_t Flags; } PageEntries[PageSize / sizeof(PageEntry)]; void DecryptTable(std::uint32_t PageIndex); void DecryptData(std::uint32_t PageChecksum); // FAT Table Entries FATEntry FATEntries[64]; /* To checksum a table be sure to do "u32[0] = 0" first */ std::uint32_t Checksum(); }; #pragma pack(pop) /* Symmetric keys for decrupting and encrypting the virtual file system */ namespace Keys { extern const std::uint32_t User[256]; extern const std::uint32_t NotRemoveMe[256]; extern const std::uint32_t LocalState[256]; extern const std::uint32_t System[256]; } // Streambuf to read from an encrypted file class ifstreambuf : public std::streambuf { public: explicit ifstreambuf( const std::uint32_t* Key = Keys::User ); // No copy ifstreambuf(const ifstreambuf&) = delete; ifstreambuf& operator=(const ifstreambuf&) = delete; // Adhere similarly to std::basic_filebuf ifstreambuf* open( const char* Name ); ifstreambuf* open( const wchar_t* Name ); ifstreambuf* close(); bool is_open() const; // std::streambuf overrides virtual std::streambuf::int_type underflow() override; virtual std::streambuf::pos_type seekoff( std::streambuf::off_type Offset, std::ios_base::seekdir Direction, std::ios_base::openmode Mode = std::ios_base::in ) override; virtual std::streambuf::pos_type seekpos( std::streambuf::pos_type Position, std::ios_base::openmode Mode = std::ios_base::in ) override; private: std::ifstream FileIn; VirtualPage Buffer; // Decryption Key const std::uint32_t* Key; std::uint32_t CurrentPage; // Caching bool FetchPage(std::uint32_t PageIndex, VirtualPage* Dest); std::unique_ptr PageCache; std::uint32_t PageCacheIndex; std::unique_ptr TableCache; std::uint32_t TableCacheIndex; std::uint32_t PageCount; }; class ifstream : public std::istream { public: explicit ifstream( const std::string& Path ); explicit ifstream( const char* Path ); explicit ifstream( const std::wstring& Path ); explicit ifstream( const wchar_t* Path ); // Similar to ifstream member functions void open(const char* FilePath) const; void open(const std::string& FilePath) const; void open(const wchar_t* FilePath) const; void open(const std::wstring& FilePath) const; bool is_open() const; virtual ~ifstream(); private: }; // Forward declarations class VirtualFileEntry; class VirtualFileSystem; // Visitors class VirtualFileVisitor { public: virtual ~VirtualFileVisitor(); // Return false to stop iteration virtual bool VisitFolderBegin(VirtualFileEntry&); virtual bool VisitFolderEnd(VirtualFileEntry&); virtual bool VisitFile(VirtualFileEntry&); }; class VirtualFileSystem { public: explicit VirtualFileSystem(const char* FileName); explicit VirtualFileSystem(const wchar_t* FileName); ~VirtualFileSystem(); // No Copy VirtualFileSystem(const VirtualFileSystem&) = delete; VirtualFileSystem& operator=(const VirtualFileSystem&) = delete; bool IsOpen() const; bool Exists(const char* Path); std::unique_ptr GetEntry(const char* Path); std::size_t Read( std::size_t Offset, void* Destination, std::size_t Size) const; template< typename T > inline std::size_t Read(std::size_t Offset, T& Destination) { return Read( Offset, &Destination, sizeof(T) ); } void IterateFileSystem(VirtualFileVisitor& Visitor); private: void IterateFATBlock( std::size_t Index, VirtualFileVisitor& Visitor ); std::shared_ptr SaiStream; }; class VirtualFileEntry { public: ~VirtualFileEntry(); // No Copy VirtualFileEntry(const VirtualFileEntry&) = delete; VirtualFileEntry& operator=(const VirtualFileEntry&) = delete; const char* GetName() const; FATEntry::EntryType GetType() const; std::time_t GetTimeStamp() const; std::size_t GetSize() const; std::size_t GetPageIndex() const; std::size_t Tell() const; void Seek(std::size_t Offset); std::size_t Read(void* Destination, std::size_t Size); template< typename T > inline std::size_t Read(T& Destination) { return Read(&Destination, sizeof(T)); } template< typename T > inline T Read() { T temp; Read(&temp, sizeof(T)); return temp; } private: friend VirtualFileSystem; VirtualFileEntry(); std::weak_ptr FileSystem; std::size_t ReadPoint; FATEntry FATData; }; class Layer { public: Layer(VirtualFileEntry& entry); ~Layer(); LayerClass LayerType(); std::uint32_t Identifier(); std::tuple< std::int32_t, std::int32_t > Position(); std::tuple< std::uint32_t, std::uint32_t > Size(); int Opacity(); bool IsVisible(); bool IsPreserveOpacity(); bool IsClipping(); BlendingMode Blending(); + //Layer name. char* LayerName(); + //ID of the parent layer or folder. std::uint32_t ParentID(); + + //String representing the name of the texture used. + //If empty, no texture effect. + char* TextureName(); + //Texture scaling in percentages going from 50 to 500%. + int TextureScale(); + //Opacity of the texture effect. + int TextureOpacity(); + + //Boolean toggling whether the water color fringe is enabled. + //May have been intended to be an integer going over several effects. + int LayerEffect(); + //Integer going from 0 to 100 representing the opacity of the effect. + int LayerEffectOpacity(); + //Integer going from 0 to 15 presenting the width of the effect. + int LayerEffectWidth(); + private: LayerHeader header; char layerName[256]; std::uint32_t ParentLayer; + char TexName[64]; + std::uint16_t TexScale; + std::uint8_t TexOpacity; + std::uint8_t Effect; + std::uint8_t EffectOpacity; + std::uint8_t EffectWidth; }; class Document : public VirtualFileSystem { public: explicit Document(const char* FileName); explicit Document(const wchar_t* FileName); ~Document(); // No Copy Document(const Document&) = delete; Document& operator=(const Document&) = delete; // Returns (Width, Height) // Returns (0,0) if an error has occured std::tuple< std::uint32_t, std::uint32_t > GetCanvasSize(); // Returns (RGBA Pixel Data, Width, Height). // Returns (null,0,0) if an error has occured. std::tuple< std::unique_ptr, std::uint32_t, std::uint32_t > GetThumbnail(); private: }; } diff --git a/plugins/impex/sai/3rdparty/libsai/samples/Tree.cpp b/plugins/impex/sai/3rdparty/libsai/samples/Tree.cpp index 5e59ccb2de..1d3c891301 100644 --- a/plugins/impex/sai/3rdparty/libsai/samples/Tree.cpp +++ b/plugins/impex/sai/3rdparty/libsai/samples/Tree.cpp @@ -1,210 +1,211 @@ #include #include #include #include #include #include #include #include "Benchmark.hpp" class SaiTreeView : public sai::VirtualFileVisitor { public: SaiTreeView() : FolderDepth(0) { } ~SaiTreeView() { } bool VisitFolderBegin(sai::VirtualFileEntry& Entry) override { PrintVirtualFileEntry(Entry); ++FolderDepth; return true; } bool VisitFolderEnd(sai::VirtualFileEntry& /*Entry*/) override { --FolderDepth; return true; } bool VisitFile(sai::VirtualFileEntry& Entry) override { PrintVirtualFileEntry(Entry); if (FolderDepth>0) { sai::Layer layerData = sai::Layer(Entry); PrintNestedFolder(); PrintNestedFolder(); std::cout << " " <<"LayerType: "<< LayerType(layerData.LayerType()) << ", ParentLayer: "<< layerData.ParentID() << ", LayerID: "<< layerData.Identifier() << ", LayerName: "<< layerData.LayerName() << "\n"; PrintNestedFolder(); PrintNestedFolder(); std::cout <<" Visibility:" << layerData.IsVisible() <<", Opacity:" << layerData.Opacity() << ", AlphaLock:" << layerData.IsPreserveOpacity() << ", Clipping:" << layerData.IsClipping() << "\n"; PrintNestedFolder(); PrintNestedFolder(); std::cout << " Position: [" << int(std::get<0>(layerData.Position())) << ", " << int(std::get<1>(layerData.Position())) << "], Size: [" << int(std::get<0>(layerData.Size())) <<"," << int(std::get<1>(layerData.Size())) << "] Blending Mode: " << BlendingMode(layerData.Blending()) << "\n"; } return true; } private: void PrintVirtualFileEntry(const sai::VirtualFileEntry& Entry) const { const std::time_t TimeStamp = Entry.GetTimeStamp(); char TimeString[32]; std::strftime( TimeString, 32, "%D %R", std::localtime(&TimeStamp) ); PrintNestedFolder(); std::printf( "\u251C\u2500\u2500 [%12zu %s] %s\n", Entry.GetSize(), TimeString, Entry.GetName() ); } void PrintNestedFolder() const { for( std::size_t i = 0; i < FolderDepth; ++i ) { std::fputs( "\u2502 ", stdout ); } } std::string LayerType(sai::LayerClass type) { std::string s = ""; switch(type) { case sai::LayerClass::Layer: s = "RasterLayer"; break; case sai::LayerClass::Linework: s = "LineworkLayer"; break; case sai::LayerClass::Set: s = "Folder"; break; case sai::LayerClass::Mask: s = "Mask"; break; case sai::LayerClass::RootLayer: s = "RootLayer"; break; default: s = "unknown"; break; } return s; } std::string BlendingMode(sai::BlendingMode mode) { std::string s = ""; switch (mode) { case sai::BlendingMode::Shade: s = "Shade"; break; case sai::BlendingMode::Binary: s = "Binary"; break; case sai::BlendingMode::Normal: s = "Normal"; break; case sai::BlendingMode::Screen: s = "Screen"; break; case sai::BlendingMode::Multiply: s = "Multiply"; break; case sai::BlendingMode::LumiShade: s = "LumiShade"; break; case sai::BlendingMode::Luminosity: s = "Luminosity"; break; case sai::BlendingMode::PassThrough: s = "Passthrough"; break; case sai::BlendingMode::Overlay: s = "Overlay"; break; default: s = "unknown blending mode"; std::cout << " unknown blending mode " << mode; printf("|%c%c%c%c|\n", reinterpret_cast(&mode)[3], reinterpret_cast(&mode)[2], reinterpret_cast(&mode)[1], reinterpret_cast(&mode)[0] ); break; } return s; } std::uint32_t FolderDepth; }; const char* const Help = "Show virtual file system tree of a user-created .sai files:\n" "\tDecrypt.exe (filename)\n" "\tWunkolo - Wunkolo@gmail.com"; int main(int argc, char* argv[]) { if( argc < 2 ) { std::puts(Help); return EXIT_FAILURE; } sai::Document CurDocument(argv[1]); if( !CurDocument.IsOpen() ) { std::cout << "Error opening file for reading: " << argv[1] << std::endl; return EXIT_FAILURE; } const auto Bench = Benchmark::Run( [&CurDocument]() -> void { SaiTreeView TreeVisitor; + std::tuple size = CurDocument.GetCanvasSize(); CurDocument.IterateFileSystem(TreeVisitor); } ); std::printf( "Iterated VFS of %s in %zu ns\n", argv[1], Bench.count() ); return EXIT_SUCCESS; } diff --git a/plugins/impex/sai/3rdparty/libsai/source/sai.cpp b/plugins/impex/sai/3rdparty/libsai/source/sai.cpp index 59f9412491..36ce6aa0cb 100644 --- a/plugins/impex/sai/3rdparty/libsai/source/sai.cpp +++ b/plugins/impex/sai/3rdparty/libsai/source/sai.cpp @@ -1,1134 +1,1262 @@ #include #include #include #include #include #include #include #include #include namespace sai { /// VirtualPage #if defined(__AVX2__) inline __m256i KeySum8( __m256i Vector8, const std::uint32_t Key[256] ) { __m256i Sum = _mm256_i32gather_epi32( (const std::int32_t*)Key, _mm256_and_si256(Vector8, _mm256_set1_epi32(0xFF)), sizeof(std::uint32_t) ); Sum = _mm256_add_epi32( Sum, _mm256_i32gather_epi32( (const std::int32_t*)Key, _mm256_and_si256( _mm256_srli_epi32(Vector8, 8), _mm256_set1_epi32(0xFF) ), sizeof(std::uint32_t) ) ); Sum = _mm256_add_epi32( Sum, _mm256_i32gather_epi32( (const std::int32_t*)Key, _mm256_and_si256( _mm256_srli_epi32(Vector8, 16), _mm256_set1_epi32(0xFF) ), sizeof(std::uint32_t) ) ); Sum = _mm256_add_epi32( Sum, _mm256_i32gather_epi32( (const std::int32_t*)Key, _mm256_srli_epi32(Vector8, 24), sizeof(std::uint32_t) ) ); return Sum; } #endif void VirtualPage::DecryptTable(std::uint32_t PageIndex) { std::uint32_t PrevData = PageIndex & (~0x1FF); #if defined(__AVX2__) __m256i PrevData8 = _mm256_set1_epi32(PrevData); for( std::size_t i = 0; i < (PageSize / sizeof(std::uint32_t)); i += 8 ) { const __m256i CurData8 = _mm256_loadu_si256((__m256i*)(u32 + i)); // There is no true _mm_alignr_epi8 for AVX2 // An extra _mm256_permute2x128_si256 is needed PrevData8 = _mm256_alignr_epi8( CurData8, _mm256_permute2x128_si256(PrevData8, CurData8, _MM_SHUFFLE(0,2,0,1)), sizeof(std::uint32_t) * 3 ); __m256i CurPlain8 = _mm256_xor_si256( _mm256_xor_si256(CurData8, PrevData8), KeySum8(PrevData8, Keys::User) ); CurPlain8 = _mm256_shuffle_epi8( CurPlain8, _mm256_set_epi8( 13, 12, 15, 14, 9, 8, 11, 10, 5, 4, 7, 6, 1, 0, 3, 2, 13, 12, 15, 14, 9, 8, 11, 10, 5, 4, 7, 6, 1, 0, 3, 2 ) ); _mm256_storeu_si256((__m256i*)(u32 + i), CurPlain8); PrevData8 = CurData8; }; #else for( std::size_t i = 0; i < (PageSize / sizeof(std::uint32_t)); i++ ) { const std::uint32_t CurData = u32[i]; std::uint32_t X = PrevData ^ CurData; X ^= ( Keys::User[(PrevData >> 24) & 0xFF] + Keys::User[(PrevData >> 16) & 0xFF] + Keys::User[(PrevData >> 8) & 0xFF] + Keys::User[(PrevData >> 0) & 0xFF] ); u32[i] = static_cast((X << 16) | (X >> 16)); PrevData = CurData; }; #endif } void VirtualPage::DecryptData(std::uint32_t PageChecksum) { std::uint32_t PrevData = PageChecksum; #if defined(__AVX2__) __m256i PrevData8 = _mm256_set1_epi32(PrevData); for( std::size_t i = 0; i < (PageSize / sizeof(std::uint32_t)); i += 8 ) { const __m256i CurData8 = _mm256_loadu_si256((__m256i*)(u32 + i)); // There is no true _mm_alignr_epi8 for AVX2 // An extra _mm256_permute2x128_si256 is needed PrevData8 = _mm256_alignr_epi8( CurData8, _mm256_permute2x128_si256(PrevData8, CurData8, _MM_SHUFFLE(0,2,0,1)), sizeof(std::uint32_t) * 3 ); __m256i CurPlain8 = _mm256_sub_epi32( CurData8, _mm256_xor_si256(PrevData8, KeySum8(PrevData8, Keys::User)) ); _mm256_storeu_si256((__m256i*)(u32 + i), CurPlain8); PrevData8 = CurData8; }; #else for( std::size_t i = 0; i < (PageSize / sizeof(std::uint32_t)); i++ ) { const std::uint32_t CurData = u32[i]; u32[i] = CurData - (PrevData ^ ( Keys::User[(PrevData >> 24) & 0xFF] + Keys::User[(PrevData >> 16) & 0xFF] + Keys::User[(PrevData >> 8) & 0xFF] + Keys::User[(PrevData >> 0) & 0xFF] ) ); PrevData = CurData; } #endif } std::uint32_t VirtualPage::Checksum() { std::uint32_t Sum = 0; for( std::size_t i = 0; i < (PageSize / sizeof(std::uint32_t)); i++ ) { Sum = (2 * Sum | (Sum >> 31)) ^ u32[i]; } return Sum | 1; } /// ifstreambuf ifstreambuf::ifstreambuf(const std::uint32_t* Key) : Key(Key), CurrentPage(-1), PageCache(nullptr), PageCacheIndex(-1), TableCache(nullptr), TableCacheIndex(-1), PageCount(0) { setg( nullptr, nullptr, nullptr ); setp( nullptr, nullptr ); - PageCache = std::make_unique(); - TableCache = std::make_unique(); + PageCache = std::unique_ptr(new VirtualPage{}); + TableCache = std::unique_ptr(new VirtualPage{}); } ifstreambuf* ifstreambuf::open(const char* Name) { if( is_open() == true ) { return nullptr; } FileIn.open( Name, std::ios_base::binary | std::ios_base::ate ); if( FileIn.is_open() == false ) { close(); return nullptr; } const std::ifstream::pos_type FileSize = FileIn.tellg(); if( FileSize % VirtualPage::PageSize != 0 ) { // File size is not pagealigned close(); return nullptr; } PageCount = static_cast(FileSize) / VirtualPage::PageSize; seekpos( 0 ); return this; } ifstreambuf* ifstreambuf::open(const wchar_t* Name) { if( is_open() == true ) { return nullptr; } #if defined(_WIN32) FileIn.open( Name, std::ios_base::binary | std::ios_base::ate ); #else std::wstring_convert> Converter; std::string Name8 = Converter.to_bytes(std::wstring(Name)); FileIn.open( Name8, std::ios_base::binary | std::ios_base::ate ); #endif if( FileIn.is_open() == false ) { close(); return nullptr; } const std::ifstream::pos_type FileSize = FileIn.tellg(); if( FileSize % VirtualPage::PageSize != 0 ) { // File size is not pagealigned close(); return nullptr; } PageCount = static_cast(FileSize) / VirtualPage::PageSize; seekpos( 0 ); return this; } ifstreambuf* ifstreambuf::close() { if( FileIn.is_open() ) { FileIn.close(); return this; } return nullptr; } bool ifstreambuf::is_open() const { return FileIn.is_open(); } std::streambuf::int_type ifstreambuf::underflow() { if( FileIn.eof() ) { return traits_type::eof(); } if( gptr() == egptr() ) { // buffer depleated, get next block if( seekpos( (CurrentPage + 1) * VirtualPage::PageSize ) == std::streampos(std::streamoff(-1)) ) { // Seek position error return traits_type::eof(); } } return traits_type::to_int_type(*gptr()); } std::streambuf::pos_type ifstreambuf::seekoff( std::streambuf::off_type Offset, std::ios_base::seekdir Direction, - std::ios_base::openmode /*Mode*/ + std::ios_base::openmode /*Mode*/ ) { std::streambuf::pos_type Position; if( Direction & std::ios_base::beg ) { Position = (CurrentPage * VirtualPage::PageSize); // Current Page Position += (gptr() - egptr()); // Offset within page Position += Offset; } if( Direction & std::ios_base::cur ) { Position = Offset; } if( Direction & std::ios_base::end ) { Position = (PageCount * VirtualPage::PageSize) + Offset; } return seekpos( Position ); } std::streambuf::pos_type ifstreambuf::seekpos( std::streambuf::pos_type Position, std::ios_base::openmode Mode ) { if( Mode & std::ios_base::in ) { CurrentPage = static_cast(Position) / VirtualPage::PageSize; if( CurrentPage < PageCount ) { if( FetchPage(CurrentPage, &Buffer) ) { setg( reinterpret_cast(Buffer.u8), reinterpret_cast(Buffer.u8) + (Position % VirtualPage::PageSize), reinterpret_cast(Buffer.u8) + VirtualPage::PageSize ); return true; } } } setg( nullptr, nullptr, nullptr ); return std::streampos(std::streamoff(-1)); } bool ifstreambuf::FetchPage(std::uint32_t PageIndex, VirtualPage* Dest) { if( FileIn.fail() ) { return false; } if( PageIndex % VirtualPage::TableSpan == 0 ) // Table Block { if( PageIndex == TableCacheIndex ) { // Cache Hit if( Dest != nullptr ) { std::memcpy( Dest, TableCache.get(), VirtualPage::PageSize ); } return true; } FileIn.seekg( PageIndex * VirtualPage::PageSize, std::ios_base::beg ); FileIn.read( reinterpret_cast(TableCache.get()), VirtualPage::PageSize ); if( FileIn.fail() ) { return false; } TableCache.get()->DecryptTable(PageIndex); TableCacheIndex = PageIndex; if( Dest != nullptr ) { std::memcpy( Dest, TableCache.get(), VirtualPage::PageSize ); } } else // Data Block { if( PageIndex == PageCacheIndex ) { // Cache Hit if( Dest != nullptr ) { std::memcpy( Dest, PageCache.get(), VirtualPage::PageSize ); } return true; } // Prefetch nearest table // Ensure it is in the cache const std::uint32_t NearestTable = (PageIndex / VirtualPage::TableSpan) * VirtualPage::TableSpan; if( FetchPage(NearestTable, nullptr) == false ) { // Failed to fetch table return false; } FileIn.seekg( PageIndex * VirtualPage::PageSize, std::ios_base::beg ); FileIn.read( reinterpret_cast(PageCache.get()), VirtualPage::PageSize ); if( FileIn.fail() ) { return false; } PageCache.get()->DecryptData( TableCache.get()->PageEntries[PageIndex % VirtualPage::TableSpan].Checksum ); if( PageCache.get()->Checksum() != TableCache.get()->PageEntries[PageIndex % VirtualPage::TableSpan].Checksum ) { // Checksum mismatch, file corrupt return false; } PageCacheIndex = PageIndex; if( Dest != nullptr ) { std::memcpy( Dest, PageCache.get(), VirtualPage::PageSize ); } } return true; } /// ifstream ifstream::ifstream(const std::string& Path) : std::istream(new ifstreambuf()) { reinterpret_cast(rdbuf())->open( Path.c_str() ); } ifstream::ifstream(const char* Path) : std::istream(new ifstreambuf()) { reinterpret_cast(rdbuf())->open( Path ); } ifstream::ifstream(const std::wstring& Path) : std::istream(new ifstreambuf()) { reinterpret_cast(rdbuf())->open( Path.c_str() ); } ifstream::ifstream(const wchar_t* Path) : std::istream(new ifstreambuf()) { reinterpret_cast(rdbuf())->open( Path ); } void ifstream::open(const char* FilePath) const { reinterpret_cast(rdbuf())->close(); reinterpret_cast(rdbuf())->open( FilePath ); } void ifstream::open(const std::string& FilePath) const { open(FilePath.c_str()); } void ifstream::open(const wchar_t* FilePath) const { reinterpret_cast(rdbuf())->close(); reinterpret_cast(rdbuf())->open( FilePath ); } void ifstream::open(const std::wstring& FilePath) const { open(FilePath.c_str()); } bool ifstream::is_open() const { return reinterpret_cast(rdbuf())->is_open(); } ifstream::~ifstream() { if( rdbuf() ) { delete rdbuf(); } } VirtualFileVisitor::~VirtualFileVisitor() { } bool VirtualFileVisitor::VisitFolderBegin(VirtualFileEntry& /*Entry*/) { return true; } bool VirtualFileVisitor::VisitFolderEnd(VirtualFileEntry& /*Entry*/) { return true; } bool VirtualFileVisitor::VisitFile(VirtualFileEntry& /*Entry*/) { return true; } /// Virtual File System VirtualFileSystem::VirtualFileSystem(const char* FileName) : SaiStream(std::make_shared(FileName)) { } VirtualFileSystem::VirtualFileSystem(const wchar_t* FileName) : SaiStream(std::make_shared(FileName)) { } VirtualFileSystem::~VirtualFileSystem() { } bool VirtualFileSystem::IsOpen() const { return SaiStream->is_open(); } bool VirtualFileSystem::Exists(const char* Path) { return static_cast(GetEntry(Path)); } std::unique_ptr VirtualFileSystem::GetEntry(const char* Path) { VirtualPage CurPage; Read( 2 * VirtualPage::PageSize, CurPage ); std::string CurPath(Path); - const char* PathDelim = "./"; + const char* PathDelim = "./"; const char* CurToken = std::strtok(&CurPath[0], PathDelim); std::size_t CurEntry = 0; while( CurEntry < 64 && CurPage.FATEntries[CurEntry].Flags && CurToken ) { if( std::strcmp(CurToken, CurPage.FATEntries[CurEntry].Name) == 0 ) { // Match if( (CurToken = std::strtok(nullptr, PathDelim)) == nullptr ) { // No more tokens, done std::unique_ptr Entry(new VirtualFileEntry()); Entry->FATData = CurPage.FATEntries[CurEntry]; Entry->FileSystem = SaiStream; return Entry; } // Try to go further if( CurPage.FATEntries[CurEntry].Type != FATEntry::EntryType::Folder ) { // Part of the path was not a folder, cant go further return nullptr; } Read( CurPage.FATEntries[CurEntry].PageIndex * VirtualPage::PageSize, CurPage ); CurEntry = 0; continue; } CurEntry++; } return nullptr; } std::size_t VirtualFileSystem::Read( std::size_t Offset, void* Destination, std::size_t Size) const { SaiStream->seekg(Offset); SaiStream->read( reinterpret_cast(Destination), Size ); return Size; } void VirtualFileSystem::IterateFileSystem(VirtualFileVisitor& Visitor) { IterateFATBlock( 2, Visitor ); } void VirtualFileSystem::IterateFATBlock( std::size_t PageIndex, VirtualFileVisitor& Visitor ) { VirtualPage CurPage = {}; Read( PageIndex * VirtualPage::PageSize, CurPage ); for( std::size_t i = 0; i < std::extent::value && CurPage.FATEntries[i].Flags; i++ ) { VirtualFileEntry CurEntry; CurEntry.FATData = CurPage.FATEntries[i]; CurEntry.FileSystem = SaiStream; switch( CurEntry.GetType() ) { case FATEntry::EntryType::File: { Visitor.VisitFile(CurEntry); break; } case FATEntry::EntryType::Folder: { Visitor.VisitFolderBegin(CurEntry); IterateFATBlock( CurEntry.GetPageIndex(), Visitor ); Visitor.VisitFolderEnd(CurEntry); break; } } } } /// VirtualFileEntry VirtualFileEntry::VirtualFileEntry() : ReadPoint(0), FATData() { } VirtualFileEntry::~VirtualFileEntry() { } const char* VirtualFileEntry::GetName() const { return FATData.Name; } FATEntry::EntryType VirtualFileEntry::GetType() const { return FATData.Type; } std::time_t VirtualFileEntry::GetTimeStamp() const { return FATData.TimeStamp / 10000000ULL - 11644473600ULL; } std::size_t VirtualFileEntry::GetSize() const { return static_cast(FATData.Size); } std::size_t VirtualFileEntry::GetPageIndex() const { return static_cast(FATData.PageIndex); } std::size_t VirtualFileEntry::Tell() const { return ReadPoint; } void VirtualFileEntry::Seek(std::size_t Offset) { ReadPoint = Offset; } std::size_t VirtualFileEntry::Read(void* Destination, std::size_t Size) { if( std::shared_ptr SaiStream = FileSystem.lock() ) { // Because this is a high-level file-level read: // all table blocks must be abstracted away and "skipped" // or else we will be reading table-block data as file-data // this is a mess right now until I become more aware of some more sound // logic to do this. // What this is basically trying to do is skip reading every table block(blocks with index 0,512,1024,etc) // so this would need to SKIP all byte-ranges: // [0,4096],[2097152,2101248],[4194304,4198400],[TableIndex * 4096,TableIndex * 4096 + 4096] // and by skipping this then file-reads will appear to be perfectly continuous. // If you're reading this and have to work with this I'm so sorry. // - Wunkolo, 10/19/17 std::uint8_t* CurDest = reinterpret_cast(Destination); - std::size_t NextTableIndex = ((ReadPoint + (FATData.PageIndex * VirtualPage::PageSize)) / VirtualPage::PageSize & ~( - 0x1FF)) + VirtualPage::TableSpan; + std::size_t NextTableIndex = ((ReadPoint + (FATData.PageIndex * VirtualPage::PageSize)) / VirtualPage::PageSize & ~(0x1FF)) + VirtualPage::TableSpan; while( Size ) { // Requested offset that we want to read from const std::size_t CurOffset = ReadPoint + (FATData.PageIndex * VirtualPage::PageSize); const std::size_t CurrentIndex = CurOffset / VirtualPage::PageSize; // If we find ourselves within a table block, skip. if( NextTableIndex == CurrentIndex ) { // Align to immediately at end of current block ReadPoint += VirtualPage::PageSize - (ReadPoint % VirtualPage::PageSize); NextTableIndex += VirtualPage::TableSpan; continue; } const std::size_t NextTableOffset = NextTableIndex * VirtualPage::PageSize; const std::size_t CurStride = std::min( Size, NextTableOffset - CurOffset ); SaiStream->seekg(CurOffset); SaiStream->read( reinterpret_cast(CurDest), CurStride ); ReadPoint += CurStride; CurDest += CurStride; Size -= CurStride; } return Size; } return 0; } /// SaiDocument Document::Document(const char* FileName) : VirtualFileSystem(FileName) { } Document::Document(const wchar_t* FileName) : VirtualFileSystem(FileName) { } Document::~Document() { } std::tuple Document::GetCanvasSize() { if( std::unique_ptr Canvas = GetEntry("canvas") ) { std::uint32_t Alignment; // Always seems to be 0x10, bpc? Alignment? std::uint32_t Width, Height; Canvas->Read(Alignment); Canvas->Read(Width); Canvas->Read(Height); + + /** + std::uint32_t CurTag = 0; + std::uint32_t CurTagSize = 0; + + Canvas->Read(CurTag); + while(CurTag) + { + Canvas->Read(CurTagSize); + + switch (CurTag) { + case 'reso':{ + // 16.16 fixed point integer + std::uint32_t DotsPerInch; + // 0 = pixels, 1 = inch, 2 = cm, 3 = mm + std::uint16_t SizeUnits; + // 0 = pixel/inch, 1 = pixel/cm + std::uint16_t ResolutionUnits; + Canvas->Read(DotsPerInch); + Canvas->Read(SizeUnits); + Canvas->Read(ResolutionUnits); + std::cout << "\nCanvas Resolution "<Read(Unknown0); + std::cout << "\nwsrc "<< Unknown0; + break; + } + case 'lyid':{ + std::uint32_t Unknown0; + Canvas->Read(Unknown0); + std::cout << "\nlyid "<< Unknown0; + break; + } + case 'layr':{ + std::uint32_t SelectedLayerID; + Canvas->Read(SelectedLayerID); + std::cout << "\nSelected Layer ID "<< SelectedLayerID; + break; + } + default: + { + printf("%c%c%c%c | %u\n", + reinterpret_cast(&CurTag)[3], + reinterpret_cast(&CurTag)[2], + reinterpret_cast(&CurTag)[1], + reinterpret_cast(&CurTag)[0], + CurTagSize + ); + // for any streams that we do not handle, + // we just skip forward in the stream + Canvas->Seek(Canvas->Tell() + CurTagSize); + break; + } + + } + + Canvas->Read(CurTag); + } + */ + + return std::make_tuple(Width, Height); } return std::make_tuple(0, 0); } std::tuple< std::unique_ptr, std::uint32_t, std::uint32_t > Document::GetThumbnail() { if( std::unique_ptr Thumbnail = GetEntry("thumbnail") ) { ThumbnailHeader Header; Thumbnail->Read(Header.Width); Thumbnail->Read(Header.Height); Thumbnail->Read(Header.Magic); - if( Header.Magic != *(uint*)"23MB" ) + if( Header.Magic != *(uint*)"23MB" ) { return std::make_tuple(nullptr, 0, 0); } const std::size_t PixelCount = Header.Height * Header.Width; - std::unique_ptr Pixels - = std::make_unique(PixelCount * sizeof(std::uint32_t)); + std::unique_ptr Pixels( + new std::uint8_t[PixelCount * sizeof(std::uint32_t)]() + ); Thumbnail->Read( Pixels.get(), PixelCount * sizeof(std::uint32_t) ); //// BGRA to RGBA //std::size_t i = 0; //// Simd speedup, four pixels at a time //while( i < ((PixelCount * sizeof(std::uint32_t)) & ~0xF) ) //{ // const __m128i Swizzle = // _mm_set_epi8( // 15, 12, 13, 14, // 11, 8, 9, 10, // 7, 4, 5, 6, // 3, 0, 1, 2 // ); // __m128i QuadPixel = _mm_loadu_si128( // reinterpret_cast<__m128i*>(&Pixels[i]) // ); // QuadPixel = _mm_shuffle_epi8(QuadPixel, Swizzle); // _mm_store_si128( // reinterpret_cast<__m128i*>(&Pixels[i]), // QuadPixel // ); // i += (sizeof(std::uint32_t) * 4); //} //for( ; i < PixelCount * sizeof(std::uint32_t); i += sizeof(std::uint32_t) ) //{ // std::swap(Pixels[i], Pixels[i + 2]); //} return std::make_tuple(std::move(Pixels), Header.Width, Header.Height); } return std::make_tuple(nullptr, 0, 0); } /// Keys namespace Keys { const std::uint32_t User[256] = { 0x9913D29E,0x83F58D3D,0xD0BE1526,0x86442EB7,0x7EC69BFB,0x89D75F64,0xFB51B239,0xFF097C56, 0xA206EF1E,0x973D668D,0xC383770D,0x1CB4CCEB,0x36F7108B,0x40336BCD,0x84D123BD,0xAFEF5DF3, 0x90326747,0xCBFFA8DD,0x25B94703,0xD7C5A4BA,0xE40A17A0,0xEADAE6F2,0x6B738250,0x76ECF24A, 0x6F2746CC,0x9BF95E24,0x1ECA68C5,0xE71C5929,0x7817E56C,0x2F99C471,0x395A32B9,0x61438343, 0x5E3E4F88,0x80A9332C,0x1879C69F,0x7A03D354,0x12E89720,0xF980448E,0x03643576,0x963C1D7B, 0xBBED01D6,0xC512A6B1,0x51CB492B,0x44BADEC9,0xB2D54BC1,0x4E7C2893,0x1531C9A3,0x43A32CA5, 0x55B25A87,0x70D9FA79,0xEF5B4AE3,0x8AE7F495,0x923A8505,0x1D92650C,0xC94A9A5C,0x27D4BB14, 0x1372A9F7,0x0C19A7FE,0x64FA1A53,0xF1A2EB6D,0x9FEB910F,0x4CE10C4E,0x20825601,0x7DFC98C4, 0xA046C808,0x8E90E7BE,0x601DE357,0xF360F37C,0x00CD6F77,0xCC6AB9D4,0x24CC4E78,0xAB1E0BFC, 0x6A8BC585,0xFD70ABF0,0xD4A75261,0x1ABF5834,0x45DCFE17,0x5F67E136,0x948FD915,0x65AD9EF5, 0x81AB20E9,0xD36EAF42,0x0F7F45C7,0x1BAE72D9,0xBE116AC6,0xDF58B4D5,0x3F0B960E,0xC2613F98, 0xB065F8B0,0x6259F975,0xC49AEE84,0x29718963,0x0B6D991D,0x09CF7A37,0x692A6DF8,0x67B68B02, 0x2E10DBC2,0x6C34E93C,0xA84B50A1,0xAC6FC0BB,0x5CA6184C,0x34E46183,0x42B379A9,0x79883AB6, 0x08750921,0x35AF2B19,0xF7AA886A,0x49F281D3,0xA1768059,0x14568CFD,0x8B3625F6,0x3E1B2D9D, 0xF60E14CE,0x1157270A,0xDB5C7EB3,0x738A0AFA,0x19C248E5,0x590CBD62,0x7B37C312,0xFC00B148, 0xD808CF07,0xD6BD1C82,0xBD50F1D8,0x91DEA3B8,0xFA86B340,0xF5DF2A80,0x9A7BEA6E,0x1720B8F1, 0xED94A56B,0xBF02BE28,0x0D419FA8,0x073B4DBC,0x829E3144,0x029F43E1,0x71E6D51F,0xA9381F09, 0x583075E0,0xE398D789,0xF0E31106,0x75073EB5,0x5704863E,0x6EF1043B,0xBC407F33,0x8DBCFB25, 0x886C8F22,0x5AF4DD7A,0x2CEACA35,0x8FC969DC,0x9DB8D6B4,0xC65EDC2F,0xE60F9316,0x0A84519A, 0x3A294011,0xDCF3063F,0x41621623,0x228CB75B,0x28E9D166,0xAE631B7F,0x06D8C267,0xDA693C94, 0x54A5E860,0x7C2170F4,0xF2E294CB,0x5B77A0F9,0xB91522A6,0xEC549500,0x10DD78A7,0x3823E458, 0x77D3635A,0x018E3069,0xE039D055,0xD5C341BF,0x9C2400EA,0x85C0A1D1,0x66059C86,0x0416FF1A, 0xE27E05C8,0xB19C4C2D,0xFE4DF58F,0xD2F0CE2A,0x32E013C0,0xEED637D7,0xE9FEC1E8,0xA4890DCA, 0xF4180313,0x7291738C,0xE1B053A2,0x9801267E,0x2DA15BDB,0xADC4DA4F,0xCF95D474,0xC0265781, 0x1F226CED,0xA7472952,0x3C5F0273,0xC152BA68,0xDD66F09B,0x93C7EDCF,0x4F147404,0x3193425D, 0x26B5768A,0x0E683B2E,0x952FDF30,0x2A6BAE46,0xA3559270,0xB781D897,0xEB4ECB51,0xDE49394D, 0x483F629C,0x2153845E,0xB40D64E2,0x47DB0ED0,0x302D8E4B,0x4BF8125F,0x2BD2B0AC,0x3DC836EC, 0xC7871965,0xB64C5CDE,0x9EA8BC27,0xD1853490,0x3B42EC6F,0x63A4FD91,0xAA289D18,0x4D2B1E49, 0xB8A060AD,0xB5F6C799,0x6D1F7D1C,0xBA8DAAE6,0xE51A0FC3,0xD94890E7,0x167DF6D2,0x879BCD41, 0x5096AC1B,0x05ACB5DA,0x375D24EE,0x7F2EB6AA,0xA535F738,0xCAD0AD10,0xF8456E3A,0x23FD5492, 0xB3745532,0x53C1A272,0x469DFCDF,0xE897BF7D,0xA6BBE2AE,0x68CE38AF,0x5D783D0B,0x524F21E4, 0x4A257B31,0xCE7A07B2,0x562CE045,0x33B708A4,0x8CEE8AEF,0xC8FB71FF,0x74E52FAB,0xCDB18796 }; const std::uint32_t NotRemoveMe[256] = { 0xA0C62B54,0x0374CB94,0xB3A53F76,0x5B772C6B,0xF2B92931,0x80F923A9,0x7A22EF7A,0x216C7582, 0xEDFF8B71,0x8B0C6642,0xAF81AD2F,0x8E095A62,0x02926C0C,0xDD2F56B9,0xA3614155,0xF9AED6E4, 0x079C3E5E,0xE6D9E1FD,0x256F165C,0x77280767,0x5D2037A1,0x3019B3CE,0xFC13CC15,0xF457C85F, 0x728DF4E9,0x4405AA18,0x2AE0B950,0xE847316F,0xD69FA172,0x62F658E2,0xB0F21F89,0x8AFB852E, 0x1A3E924A,0xDBAD0B48,0x88ECBD5A,0xC53FC908,0x81251757,0x57D53685,0x73F463A3,0x048F4B58, 0xC36A46AC,0x9A8B6FBD,0x35DC9DC1,0xF76EABF5,0x9280D935,0xBFCC93FB,0x4B2BCA7D,0x60861DFC, 0x7C548877,0x2EA46821,0x7136998F,0x5AD45EDF,0x019BA6EF,0x6FC598C7,0x1DF383EC,0x39BAC06D, 0x5C3A5B1F,0x7827FB39,0x27FCA953,0x8601E843,0x6C429623,0xBA5DC127,0xCE659075,0x48291378, 0x5EDA6B5B,0xE355AC99,0xCF8C704D,0x965E6A29,0xF5035103,0x20582702,0x1B7909DB,0xCA974452, 0x7DB20E30,0x2807326C,0x2DF56D0E,0x084E9C41,0xA42DE39C,0x9170A5C3,0x9DB4F95D,0x53CA2068, 0x3488FC6E,0xD1BB7AE8,0xC61F81C5,0x310857E5,0xEF1694EE,0xF63067B1,0x3E621B8B,0x22523BFF, 0x0D37A4BA,0xCB83BECA,0x9BE78691,0xB7D84E2C,0x45A676DD,0x1F31F636,0x7FAB97C6,0x3CA15F33, 0xFA6DB6FE,0x67DD72DC,0x6B8948FA,0x9849FF4B,0xBE452E79,0x38AF6E7F,0x8FE211A7,0x941728B4, 0x63217749,0x70EF1280,0x13A9F201,0xACDB14A2,0x1184E73A,0x337E87B5,0xB6008EB7,0xC868C43C, 0x85F7DC83,0xD35AD519,0xF87310ED,0xA7C0D29B,0x361D2DCF,0xC1D27C3F,0x9C78DFE0,0x2C4FD8C4, 0x05357D9D,0x2B398964,0x182AC610,0xFD4A3873,0xE71E6416,0x842C4A05,0x5946F70F,0xB95FA366, 0x1C0B71CB,0x50CEFA06,0xAB9DC211,0x659ABCAE,0xD2E17FE7,0x581A0365,0xA61BE0B0,0xD460B084, 0xE21C5CF9,0x87B1D460,0x4DF8CF04,0x4C1573EA,0xCD967432,0xD58EBA12,0x5F2E9A3B,0x6A9955EB, 0x55A391AF,0xEBC1EED5,0xB59E8C7C,0x1E825946,0xAA18A04E,0x6891EDF3,0x663C542D,0xC459D37E, 0xC06453BC,0x460D223E,0x1690F8DE,0xC97580F7,0xA1F08D4F,0x56DE4381,0xEE06B5E3,0xC2FA05D1, 0x3794B488,0xEACD428E,0x7B2362C2,0xE97FDE9F,0xBB4C60D2,0xE4B3E2AB,0x74C93909,0x76AA2FDA, 0x9F049B7B,0x93BCDA8A,0x51BEC790,0x0FD6E4CC,0x8972E6AD,0xBCA70F40,0x405C2469,0x10673486, 0xBD104C97,0x49381E0D,0x063B456A,0x23D02634,0x43ACEC9E,0xE50E49F8,0x197DBF1B,0x8DF1BB9A, 0xB46B1CA6,0xD7E895A5,0xCC51A217,0xE1C2F196,0xDEB533C9,0x24FDC58D,0x32850822,0x12DF4DA8, 0x90BD3500,0x97C7F320,0xDA3450F4,0x2F534059,0xDC7B3D63,0x95B6CD98,0x09BF19D6,0xA5D15DBF, 0x42E47851,0xF07A021E,0x9ECB2A3D,0xE0C39F38,0x99714F95,0x3A5BEA4C,0xB2C4DD25,0xB13D47C0, 0xAD418A0B,0x6DEAB81C,0x83EE25F2,0x3B26AE47,0xA8B018D3,0xFF76E5F1,0xA2ED0461,0x26119ED8, 0x61EB0A74,0x15A2B187,0x4A93CE2A,0x7943A707,0x29E5B744,0x4E14F02B,0x0A698424,0xD9A03AE6, 0xEC87D7C8,0xA94021B8,0x3D95D1CD,0x6E2415BE,0x52E3F592,0x64A83CD9,0x8263C31D,0x41B87EB6, 0x8C50FD1A,0x47C80CD7,0xD844008C,0xB812E9AA,0x0B983013,0xFB7C520A,0x4F66FEBB,0x17E982D0, 0x00FE6914,0xFE0FD028,0x0C328F93,0x75021AF6,0x3FE6AFB2,0x7E330DE1,0xDF8ADB45,0x14D37B37, 0xD04D06A4,0x694B0156,0x0ECF6170,0xC756EBF0,0xF1B76526,0xF348A8B3,0xAE0A79A0,0x54D7B2D4 }; const std::uint32_t LocalState[256] = { 0x021CF107,0xE9253648,0x8AFBA619,0x8CF31842,0xBF40F860,0xA672F03E,0xFA2756AC,0x927B2E7E, 0x1E37D3C4,0x7C3A0524,0x4F284D1B,0xD8A31E9D,0xBA73B6E6,0xF399710D,0xBD8B1937,0x70FFE130, 0x056DAA4A,0xDC509CA1,0x07358DFF,0xDF30A2DC,0x67E7349F,0x49532C31,0x2393EBAA,0xE54DF202, 0x3A2C7EC9,0x98AB13EF,0x7FA52975,0x83E4792E,0x7485DA08,0x4A1823A8,0x77812011,0x8710BB89, 0x9B4E0C68,0x64125D8E,0x5F174A0E,0x33EA50E7,0xA5E168B0,0x1BD9B944,0x6D7D8FE0,0xEE66B84C, 0xF0DB530C,0xF8B06B72,0x97ED7DF8,0x126E0122,0x364BED23,0xA103B75C,0x3BC844FA,0xD0946501, 0x4E2F70F1,0x79A6F413,0x60B9E977,0xC1582F10,0x759B286A,0xE723EEF5,0x8BAC4B39,0xB074B188, 0xCC528E64,0x698700EE,0x44F9E5BB,0x7E336153,0xE2413AFD,0x91DCE2BE,0xFDCE9EC1,0xCAB2DE4F, 0x46C5A486,0xA0D630DB,0x1FCD5FCA,0xEA110891,0x3F20C6F9,0xE8F1B25D,0x6EFD10C8,0x889027AF, 0xF284AF3F,0x89EE9A61,0x58AF1421,0xE41B9269,0x260C6D71,0x5079D96E,0xD959E465,0x519CD72C, 0x73B64F5A,0x40BE5535,0x78386CBC,0x0A1A02CF,0xDBC126B6,0xAD02BC8D,0x22A85BC5,0xA28ABEC3, 0x5C643952,0xE35BC9AD,0xCBDACA63,0x4CA076A4,0x4B6121CB,0x9500BF7D,0x6F8E32BF,0xC06587E5, 0x21FAEF46,0x9C2AD2F6,0x7691D4A2,0xB13E4687,0xC7460AD6,0xDDFE54D5,0x81F516F3,0xC60D7438, 0xB9CB3BC7,0xC4770D94,0xF4571240,0x06862A50,0x30D343D3,0x5ACF52B2,0xACF4E68A,0x0FC2A59B, 0xB70AEACD,0x53AA5E80,0xCF624E8F,0xF1214CEB,0x936072DF,0x62193F18,0xF5491CDA,0x5D476958, 0xDA7A852D,0x5B053E12,0xC5A9F6D0,0xABD4A7D1,0xD25E6E82,0xA4D17314,0x2E148C4E,0x6B9F6399, 0xBC26DB47,0x8296DDCE,0x3E71D616,0x350E4083,0x2063F503,0x167833F2,0x115CDC5E,0x4208E715, 0x03A49B66,0x43A724BA,0xA3B71B8C,0x107584AE,0xC24AE0C6,0xB3FC6273,0x280F3795,0x1392C5D4, 0xD5BAC762,0xB46B5A3B,0xC9480B8B,0xC39783FC,0x17F2935B,0x9DB482F4,0xA7E9CC09,0x553F4734, 0x8DB5C3A3,0x7195EC7A,0xA8518A9A,0x0CE6CB2A,0x14D50976,0x99C077A5,0x012E1733,0x94EC3D7C, 0x3D825805,0x0E80A920,0x1D39D1AB,0xFCD85126,0x3C7F3C79,0x7A43780B,0xB26815D9,0xAF1F7F1C, 0xBB8D7C81,0xAAE5250F,0x34BC670A,0x1929C8D2,0xD6AE9FC0,0x1AE07506,0x416F3155,0x9EB38698, 0x8F22CF29,0x04E8065F,0xE07CFBDE,0x2AEF90E8,0x6CAD049C,0x4DC3A8CC,0x597E3596,0x08562B92, 0x52A21D6F,0xB6C9881D,0xFBD75784,0xF613FC32,0x54C6F757,0x66E2D57B,0xCD69FE9E,0x478CA13D, 0x2F5F6428,0x8E55913C,0xF9091185,0x0089E8B3,0x1C6A48BD,0x3844946D,0x24CC8B6B,0x6524AC2B, 0xD1F6A0F0,0x32980E51,0x8634CE17,0xED67417F,0x250BAEB9,0x84D2FD1A,0xEC6C4593,0x29D0C0B1, 0xEBDF42A9,0x0D3DCD45,0x72BF963A,0x27F0B590,0x159D5978,0x3104ABD7,0x903B1F27,0x9F886A56, 0x80540FA6,0x18F8AD1F,0xEF5A9870,0x85016FC2,0xC8362D41,0x6376C497,0xE1A15C67,0x6ABD806C, 0x569AC1E2,0xFE5D1AF7,0x61CADF59,0xCE063874,0xD4F722DD,0x37DEC2EC,0xAE70BDEA,0x0B2D99B4, 0x39B895FE,0x091E9DFB,0xA9150754,0x7D1D7A36,0x9A07B41E,0x5E8FE3B5,0xD34503A0,0xBE2BFAB7, 0x5742D0A7,0x48DDBA25,0x7BE3604D,0x2D4C66E9,0xB831FFB8,0xF7BBA343,0x451697E4,0x2C4FD84B, 0x96B17B00,0xB5C789E3,0xFFEBF9ED,0xD7C4B349,0xDE3281D8,0x689E4904,0xE683F32F,0x2B3CB0E1 }; const std::uint32_t System[256] = { 0x724FB987,0x4A3E70BE,0xCA549C50,0x34E263E1,0x2D5ED2FF,0x127F0E11,0x58A42B78,0x5F6D14AE, 0x7E2F745D,0xC3450384,0xCFBB15DE,0xDF0A6D8A,0xEF2545F3,0x6D8919DB,0xBC413C94,0xCCB0A198, 0xE42DBBD2,0x361C0B8C,0x8359731F,0x13D61E9F,0x7505F7CE,0x271D7957,0x429C0699,0xD84EC85F, 0x953391DD,0xB25DE567,0xC1BA2F97,0x2309B605,0x69A134D1,0x14A092F2,0x681500EF,0xB90148A7, 0x01AF398B,0x16FD5168,0x9E572161,0x0F7405E3,0x56AC576D,0xF275A349,0x1E8120C0,0x4BF64E3A, 0x5A90E85E,0xD27BC4F1,0x3BD2FFB1,0xD6B40FDC,0x26EC61CF,0xF744AD3F,0xCDE7C548,0x8AFFE60A, 0xE382CA47,0x87DA3E1B,0x8FA3DB36,0x5737C7E0,0xACD8CC17,0xD0CC3B66,0xD93D776B,0x37E5BE2B, 0xD38A1129,0x037E81D0,0x15B15072,0xA6493052,0x35BCD4B9,0xC4538D32,0xEC66C1D5,0xA20DF513, 0x5524EB75,0x92C10488,0xDA03D9FD,0x65168F4B,0x1902BA24,0x7439FA7D,0x1D8CB46F,0xFBC39389, 0xC5DF6A58,0x89E8FB00,0x50DBE0A1,0xAAE98AF8,0x6A7C6C9C,0x7712D6EC,0x4030D0CD,0x6052B585, 0x6132AA77,0xEB4A38C3,0x673AB1E6,0x1C3C07C6,0x91EA2C76,0x7A4C7EA0,0x10B3DCFC,0xBE7DF402, 0x2817D87A,0x25632264,0xBD8D02B0,0xF6D0F8A8,0xB1ED3AF0,0xE6C4F1CA,0x99E028B5,0xE5D48674, 0x09CF47B8,0x9D6EAF0E,0x0A721AFE,0xB6109E54,0x8D642344,0x9FEFC27C,0xF0CA520F,0x2C6BDA7E, 0x2E9DB06A,0x97DEFC2E,0x53C5F0EE,0xAD4B8C60,0xE9F36696,0xA8C68907,0x70B70A20,0x3D9F82AA, 0x7604A595,0x441A563B,0x39193D4A,0x33BF1DC7,0x31B283FB,0xA399F25B,0x642CE39E,0xF9E3B204, 0x79A87534,0x5DBE2943,0x9813E93E,0x47864AD6,0xD420D1BF,0x24A6C986,0xFE386EF7,0xD1B65AB7, 0x3A96BF2F,0x006FE1AB,0x22938E90,0x78FE7A40,0x5CE1319B,0x46F5EEF5,0xBB064BE4,0xB7271C22, 0xC0225D21,0xFA145B10,0x7C58BC33,0xF84654C2,0xEEF4691E,0x021BEC16,0xE16C1737,0x1BCB2603, 0x48A2954D,0xDD56A8FA,0xB8C8A48D,0x5277590B,0x1194E7A9,0x590F42B4,0x7B97C0D8,0x7142B714, 0xAEDD6BC8,0xBA116212,0x6B0E642C,0xF42ABDC5,0x6E76AC81,0xBF348819,0xCB790C59,0xDC6718AD, 0x80471230,0x84DC985C,0x2AEE32C1,0x4D35964F,0x0C6894AC,0x3EF2CDE5,0xB59B37A5,0x9BC9729D, 0x186A41AF,0xEA98A970,0x21F8A291,0x5487E2C9,0xE05F3F42,0xA523B86E,0x8C1E4062,0xA962F6CB, 0x0D4816E8,0x9A4DF92D,0x20439DCC,0xA0713645,0x43506FE9,0xC2EB4651,0xB4780D6C,0xAFC29B28, 0x1FCE5FD4,0x9C7385D3,0xCE00E463,0x38CD997F,0x452933DA,0xC9F7DEBA,0x0840A093,0xDB287B41, 0x90E48479,0x66FC6709,0x6C884C65,0x3FB56082,0xF5B87123,0xED367D1D,0x6F0C44F9,0x8270DD38, 0x0E314F83,0x1AE69F35,0xD5A51FB3,0xA761A671,0x850B4DED,0x06AE0892,0x5EAA2A06,0xC7FA80F6, 0xB0692D4E,0x81657F8F,0x948B0980,0xB3D97C01,0xFC80C3EA,0xFF9E53A4,0x30BD784C,0xF3AD970C, 0xA12E9A31,0x04D37646,0x072655A3,0xE8D5F353,0x4CA98BDF,0x7391FE56,0x7D5BEDA6,0x2BD7650D, 0x862B5C73,0x8B60A726,0x7F8ECB3C,0x517A49B6,0xD7B9CF5A,0x6308D5BC,0x0B3F68D7,0x62A7EA15, 0xC65AFD3D,0xAB8525B2,0xA451B308,0xE7C7AB18,0x88F91369,0x1783279A,0x4F95DF2A,0x41F158BD, 0xC8D1CEBB,0x325CD3E2,0xF1928739,0x9355AE8E,0x2FC05EC4,0x4E0735E7,0xDE3B10D9,0x8E18C61A, 0xE29AEF25,0x4984D7A2,0x051F247B,0x29AB9055,0xFD2101F4,0x96FB2E1C,0x5BF04327,0x3C8F1BEB, }; } Layer::Layer(VirtualFileEntry &entry): - ParentLayer(0) + ParentLayer(0), + TexScale(100), + TexOpacity(20), + Effect(0), + EffectOpacity(100), + EffectWidth(1) { entry.Read(header); std::uint32_t CurTag = 0; std::uint32_t CurTagSize = 0; //std::cout << "starting tags" << entry.Tell(); entry.Read(CurTag); while(CurTag) { entry.Read(CurTagSize); switch( CurTag ) { - //lorg - case 'name': - { - entry.Read(layerName); - break; - } - case 'pfid': - { - entry.Read(ParentLayer); - break; - } - default: - { - // for any streams that we do not handle, - // we just skip forward in the stream - entry.Seek(entry.Tell() + CurTagSize); - break; - } + case 'name': + { + entry.Read(layerName); + break; + } + case 'pfid': + case 'plid': + { + //pfid is parentfolder, for layers and folders, while plid is parentlayer, for masks. + entry.Read(ParentLayer); + break; + } + case 'texn': + { + // Texture name if there's a texture effect enabled. + entry.Read(TexName); + break; + } + case 'texp': + { + // Texture options + entry.Read(TexScale); + entry.Read(TexOpacity); + break; + } + case 'peff': + { + entry.Read(Effect); + entry.Read(EffectOpacity); + entry.Read(EffectWidth); + break; + } + default: + { + std::cout << layerName << header.Identifier; + printf("%c%c%c%c | %u\n", + reinterpret_cast(&CurTag)[3], + reinterpret_cast(&CurTag)[2], + reinterpret_cast(&CurTag)[1], + reinterpret_cast(&CurTag)[0], + CurTagSize + ); + // for any streams that we do not handle, + // we just skip forward in the stream + entry.Seek(entry.Tell() + CurTagSize); + break; + } } entry.Read(CurTag); } //std::cout << " end" << entry.Tell() << " "; } Layer::~Layer() { } sai::LayerClass Layer::LayerType() { return sai::LayerClass(header.LayerClass); } uint32_t Layer::Identifier() { return header.Identifier; } std::tuple Layer::Position() { return std::make_tuple(header.Bounds.X, header.Bounds.Y); } std::tuple Layer::Size() { return std::make_tuple(header.Bounds.Width, header.Bounds.Height); } int Layer::Opacity() { return int(header.Opacity); } bool Layer::IsVisible() { return bool(header.Visible); } bool Layer::IsPreserveOpacity() { return bool(header.PreserveOpacity); } bool Layer::IsClipping() { return bool(header.Clipping); } BlendingMode Layer::Blending() { return BlendingMode(header.Blending); } char *Layer::LayerName() { return layerName; } uint32_t Layer::ParentID() { return ParentLayer; } +char *Layer::TextureName() +{ + return TexName; +} + +int Layer::TextureScale() +{ + return TexScale; +} + +int Layer::TextureOpacity() +{ + return TexOpacity; +} + +int Layer::LayerEffect() +{ + return Effect; +} + +int Layer::LayerEffectOpacity() +{ + return EffectOpacity; +} + +int Layer::LayerEffectWidth() +{ + return EffectWidth; +} + } diff --git a/plugins/impex/sai/kis_sai_converter.cpp b/plugins/impex/sai/kis_sai_converter.cpp index de6b1b7cca..5bc8c236aa 100644 --- a/plugins/impex/sai/kis_sai_converter.cpp +++ b/plugins/impex/sai/kis_sai_converter.cpp @@ -1,224 +1,366 @@ /* * Copyright (c) 2019 Wolthera van Hövell tot Westerflier * * 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 2 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "sai.hpp" +//Necessary for simd +#include +#include + #include #include #include #include #include #include #include #include #include #include #include #include "kis_sai_converter.h" class SaiLayerVisitor : public sai::VirtualFileVisitor { public: SaiLayerVisitor(KisImageSP image) : m_image(image) , FolderDepth(0) , parentNodeList(QMap()) , clippedLayers(QVector()) { } ~SaiLayerVisitor() override { } bool VisitFolderBegin(sai::VirtualFileEntry& Entry) override { qDebug() << "begin folder" << Entry.GetSize() << Entry.GetTimeStamp() << Entry.GetName(); ++FolderDepth; return true; } bool VisitFolderEnd(sai::VirtualFileEntry& /*Entry*/) override { qDebug() << "end folder"; --FolderDepth; return true; } bool VisitFile(sai::VirtualFileEntry& Entry) override { qDebug() << Entry.GetSize() << Entry.GetTimeStamp() << Entry.GetName(); if (FolderDepth>0) { sai::Layer layerData = sai::Layer(Entry); + qDebug() << "adding layer" << layerData.LayerName(); if (layerData.LayerType() == sai::LayerClass::Layer) { KisPaintLayerSP layer = new KisPaintLayer(m_image, layerData.LayerName(), 255); layer->setVisible(layerData.IsVisible()); layer->setAlphaLocked(layerData.IsPreserveOpacity()); quint8 opacity = qRound(layerData.Opacity() * 2.55); layer->setOpacity(opacity); layer->setCompositeOpId(BlendingMode(layerData.Blending())); layer->setX(int(std::get<0>(layerData.Position()))); layer->setY(int(std::get<1>(layerData.Position()))); + + ReadRasterDataIntoLayer(layer, Entry, quint32(std::get<0>(layerData.Size())), quint32(std::get<1>(layerData.Size()))); // Bounds; if (layerData.IsClipping() || !clippedLayers.isEmpty()) { //we should add it to a list so we can make a clipping group. //All clipped layers and the first non-clipped layer go in the group. layer->disableAlphaChannel(layerData.IsClipping()); clippedLayers.append(layer); } else { if (layerData.ParentID() == 0) { m_image->addNode(layer); } else { if (parentNodeList.contains(layerData.ParentID())) { m_image->addNode(layer, parentNodeList.value(layerData.ParentID())); } } } parentNodeList.insert(layerData.Identifier(), layer); } /*else if (layerData.LayerType() == sai::LayerClass::Linework) { break; }*/ else if (layerData.LayerType() == sai::LayerClass::Set) { KisGroupLayerSP layer = new KisGroupLayer(m_image, layerData.LayerName(), 255); layer->setVisible(layerData.IsVisible()); quint8 opacity = qRound(layerData.Opacity() * 2.55); layer->setOpacity(opacity); layer->setX(0); layer->setY(0); if (layerData.Blending() != sai::BlendingMode::PassThrough) { layer->setCompositeOpId(BlendingMode(layerData.Blending())); } else { layer->setPassThroughMode(true); } if (layerData.ParentID() == 0) { m_image->addNode(layer); } else { if (parentNodeList.contains(layerData.ParentID())) { m_image->addNode(layer, parentNodeList.value(layerData.ParentID())); } } parentNodeList.insert(layerData.Identifier(), layer); } else if (layerData.LayerType() == sai::LayerClass::Mask) { KisTransparencyMaskSP layer = new KisTransparencyMask(); //only interesting thing here is identifying data and identifying parent layer. if (parentNodeList.contains(layerData.ParentID())) { m_image->addNode(layer, parentNodeList.value(layerData.ParentID())); } } else { qDebug() << "unknown layer type"; } if (!clippedLayers.isEmpty() && layerData.IsClipping() == false) { //XXX: Make translatable KisGroupLayerSP clipgroup = new KisGroupLayer(m_image, "Clipping Group", 255); KisNodeSP clippedLayer; while(!clippedLayers.isEmpty()) { clippedLayer = clippedLayers.takeLast(); qDebug() << clippedLayer->name(); m_image->addNode(clippedLayer, clipgroup); } m_image->addNode(clipgroup); qDebug() < parentNodeList; QVector clippedLayers; QString BlendingMode(sai::BlendingMode mode) { QString s = ""; switch (mode) { case sai::BlendingMode::Shade: s = COMPOSITE_SUBTRACT; break; case sai::BlendingMode::Binary: s = COMPOSITE_DISSOLVE; break; case sai::BlendingMode::Normal: s = COMPOSITE_OVER; break; case sai::BlendingMode::Screen: s = COMPOSITE_SCREEN; break; case sai::BlendingMode::Multiply: s = COMPOSITE_MULT; break; case sai::BlendingMode::LumiShade: s = COMPOSITE_LINEAR_LIGHT; break; case sai::BlendingMode::Luminosity: s = COMPOSITE_LUMINOSITY_SAI; break; case sai::BlendingMode::PassThrough: s = "passthrough"; break; case sai::BlendingMode::Overlay: s = COMPOSITE_OVERLAY; break; } return s; } + void ReadRasterDataIntoLayer(KisPaintLayerSP layer, sai::VirtualFileEntry &entry, quint32 width, quint32 height) { + std::vector BlockMap; + //TileData.resize((width / 32) * (height / 32)); + entry.Read(BlockMap.data(), (width / 32) * (height / 32)); + + + for( std::size_t y = 0; y < (height / 32); y++ ) { + for( std::size_t x = 0; x < (width / 32); x++ ) { + if( BlockMap[(width / 32) * y + x] ) { + std::array CompressedTile; + alignas(sizeof(__m128i)) std::array DecompressedTile; + std::uint8_t Channel = 0; + std::uint16_t Size = 0; + while( entry.Read(Size) ) { + entry.Read(CompressedTile.data(), Size); + + RLEDecompress32( + DecompressedTile.data(), + CompressedTile.data(), + Size, + 1024, + Channel + ); + + Channel++; + if( Channel >= 4 ) { + for( std::size_t i = 0; i < 4; i++ ) { + std::uint16_t Size = entry.Read(); + entry.Seek(entry.Tell() + Size); + } + } + } + /* + * // Current 32x32 tile within final image + std::uint32_t *ImageBlock = reinterpret_cast(LayerImage.data()) + + (x * 32) + + ((y * LayerHead.Bounds.Width) * 32); + */ + for( std::size_t i = 0; i < (32 * 32) / 4; i++ ) { + __m128i QuadPixel = _mm_load_si128(reinterpret_cast<__m128i*>(DecompressedTile.data()) + i); + // ABGR to ARGB, if you want. + // Do your swizzling here + QuadPixel = _mm_shuffle_epi8(QuadPixel, + _mm_set_epi8( + 15, 12, 13, 14, + 11, 8, 9, 10, + 7, 4, 5, 6, + 3, 0, 1, 2)); + /// Alpha is pre-multiplied, convert to straight + // Get Alpha into [0.0,1.0] range + __m128 Scale = _mm_div_ps( + _mm_cvtepi32_ps( + _mm_shuffle_epi8( + QuadPixel, + _mm_set_epi8( + -1, -1, -1, 15, + -1, -1, -1, 11, + -1, -1, -1, 7, + -1, -1, -1, 3 + ) + ) + ), _mm_set1_ps(255.0f)); + + // Normalize each channel into straight color + for( std::uint8_t i = 0; i < 3; i++ ) + { + __m128i CurChannel = _mm_srli_epi32(QuadPixel, i * 8); + CurChannel = _mm_and_si128(CurChannel, _mm_set1_epi32(0xFF)); + __m128 ChannelFloat = _mm_cvtepi32_ps(CurChannel); + + ChannelFloat = _mm_div_ps(ChannelFloat, _mm_set1_ps(255.0));// [0,255] to [0,1] + ChannelFloat = _mm_div_ps(ChannelFloat, Scale); + ChannelFloat = _mm_mul_ps(ChannelFloat, _mm_set1_ps(255.0));// [0,1] to [0,255] + + CurChannel = _mm_cvtps_epi32(ChannelFloat); + CurChannel = _mm_and_si128(CurChannel, _mm_set1_epi32(0xff)); + CurChannel = _mm_slli_epi32(CurChannel, i * 8); + + QuadPixel = _mm_andnot_si128(_mm_set1_epi32(0xFF << (i * 8)), QuadPixel); + QuadPixel = _mm_or_si128(QuadPixel, CurChannel); + } + + // Write directly to final image + /** + _mm_store_si128( + reinterpret_cast<__m128i*>(ImageBlock) + (i % 8) + ((i / 8) * (LayerHead.Bounds.Width / 4)), + QuadPixel + ); + */ + + } + } + + } + } + } + void RLEDecompress32(void* Destination, const std::uint8_t *Source, std::size_t SourceSize, std::size_t IntCount, std::size_t Channel) + { + std::uint8_t *Write = reinterpret_cast(Destination) + Channel; + std::size_t WriteCount = 0; + + while( WriteCount < IntCount ) + { + std::uint8_t Length = *Source++; + if( Length == 128 ) // No-op + { + } + else if( Length < 128 ) // Copy + { + // Copy the next Length+1 bytes + Length++; + WriteCount += Length; + while( Length ) + { + *Write = *Source++; + Write += 4; + Length--; + } + } + else if( Length > 128 ) // Repeating byte + { + // Repeat next byte exactly "-Length + 1" times + Length ^= 0xFF; + Length += 2; + WriteCount += Length; + std::uint8_t Value = *Source++; + while( Length ) + { + *Write = Value; + Write += 4; + Length--; + } + } + } + } }; KisSaiConverter::KisSaiConverter(KisDocument *doc) : QObject(0), m_doc(doc) { } KisImportExportErrorCode KisSaiConverter::buildImage(const QString &filename) { sai::Document saiFile(QFile::encodeName(filename)); if (!saiFile.IsOpen()) { dbgFile << "Could not open the file, either it does not exist, either it is not a Sai file :" << filename; return ImportExportCodes::FileFormatIncorrect; } std::tuple size = saiFile.GetCanvasSize(); m_image = new KisImage(m_doc->createUndoStore(), int(std::get<0>(size)), int(std::get<1>(size)), KoColorSpaceRegistry::instance()->rgb8(), "file"); SaiLayerVisitor visitor(m_image); saiFile.IterateFileSystem(visitor); return ImportExportCodes::OK; } KisImageSP KisSaiConverter::image() { return m_image; }