diff --git a/inc/thorvg.h b/inc/thorvg.h index 12316fa0..4d454c41 100644 --- a/inc/thorvg.h +++ b/inc/thorvg.h @@ -1376,8 +1376,13 @@ public: /** * @brief Export the given @p paint data to the given @p path * + * If the saver module supports any compression mechanism, it will optmize the data size. + * This might affect the encoding/decoding time slow down in cases, You can turn off the compression + * if your system whole focuse on the speed. + * * @param[in] paint The paint to be saved with all its associated properties. * @param[in] path A path to the file, in which the paint data is to be saved. + * @param[in] compress Use data compression if it's available. * * @retval Result::Success When succeed. * @retval Result::NonSupport When trying to save a file with an unknown extension nor non supported format. @@ -1388,7 +1393,7 @@ public: * * @BETA_API */ - Result save(std::unique_ptr paint, const std::string& path) noexcept; + Result save(std::unique_ptr paint, const std::string& path, bool compress = true) noexcept; /** * @brief Guarantees that the saving task is finished. diff --git a/src/examples/images/test.tvg b/src/examples/images/test.tvg index fe0a79d2..36f5a1d8 100644 Binary files a/src/examples/images/test.tvg and b/src/examples/images/test.tvg differ diff --git a/src/lib/meson.build b/src/lib/meson.build index 5a7b70c0..ee2992c3 100644 --- a/src/lib/meson.build +++ b/src/lib/meson.build @@ -16,6 +16,7 @@ source_file = [ 'tvgFill.h', 'tvgLoader.h', 'tvgLoadModule.h', + 'tvgLzw.h', 'tvgPictureImpl.h', 'tvgRender.h', 'tvgSaveModule.h', @@ -28,6 +29,7 @@ source_file = [ 'tvgGlCanvas.cpp', 'tvgInitializer.cpp', 'tvgLinearGradient.cpp', + 'tvgLzw.cpp', 'tvgLoader.cpp', 'tvgPaint.cpp', 'tvgPicture.cpp', diff --git a/src/lib/tvgBinaryDesc.h b/src/lib/tvgBinaryDesc.h index fe87890b..a0189b64 100644 --- a/src/lib/tvgBinaryDesc.h +++ b/src/lib/tvgBinaryDesc.h @@ -34,13 +34,19 @@ using TvgBinTag = TvgBinByte; using TvgBinFlag = TvgBinByte; - //Header #define TVG_HEADER_SIGNATURE "ThorVG" #define TVG_HEADER_SIGNATURE_LENGTH 6 #define TVG_HEADER_VERSION "000400" //Major 00, Minor 04, Micro 00 #define TVG_HEADER_VERSION_LENGTH 6 -#define TVG_HEADER_RESERVED_LENGTH 4 //Storing flags for extensions +#define TVG_HEADER_RESERVED_LENGTH 1 //Storing flags for extensions +#define TVG_HEADER_COMPRESS_SIZE 12 //TVG_HEADER_UNCOMPRESSED_SIZE + TVG_HEADER_COMPRESSED_SIZE + TVG_HEADER_COMPRESSED_SIZE_BITS +//Compress Size +#define TVG_HEADER_UNCOMPRESSED_SIZE 4 //SIZE (TvgBinCounter) +#define TVG_HEADER_COMPRESSED_SIZE 4 //SIZE (TvgBinCounter) +#define TVG_HEADER_COMPRESSED_SIZE_BITS 4 //SIZE (TvgBinCounter) +//Reserved Flag +#define TVG_HEAD_FLAG_COMPRESSED 0x01 //Paint Type #define TVG_TAG_CLASS_PICTURE (TvgBinTag)0xfc diff --git a/src/lib/tvgLzw.cpp b/src/lib/tvgLzw.cpp new file mode 100644 index 00000000..adbcb3fc --- /dev/null +++ b/src/lib/tvgLzw.cpp @@ -0,0 +1,427 @@ +/* + * Copyright (c) 2020-2021 Samsung Electronics Co., Ltd. All rights reserved. + + * 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. + */ + +/* + * Lempel–Ziv–Welch (LZW) encoder/decoder by Guilherme R. Lampert(guilherme.ronaldo.lampert@gmail.com) + + * This is the compression scheme used by the GIF image format and the Unix 'compress' tool. + * Main differences from this implementation is that End Of Input (EOI) and Clear Codes (CC) + * are not stored in the output and the max code length in bits is 12, vs 16 in compress. + * + * EOI is simply detected by the end of the data stream, while CC happens if the + * dictionary gets filled. Data is written/read from bit streams, which handle + * byte-alignment for us in a transparent way. + + * The decoder relies on the hardcoded data layout produced by the encoder, since + * no additional reconstruction data is added to the output, so they must match. + * The nice thing about LZW is that we can reconstruct the dictionary directly from + * the stream of codes generated by the encoder, so this avoids storing additional + * headers in the bit stream. + + * The output code length is variable. It starts with the minimum number of bits + * required to store the base byte-sized dictionary and automatically increases + * as the dictionary gets larger (it starts at 9-bits and grows to 10-bits when + * code 512 is added, then 11-bits when 1024 is added, and so on). If the dictionary + * is filled (4096 items for a 12-bits dictionary), the whole thing is cleared and + * the process starts over. This is the main reason why the encoder and the decoder + * must match perfectly, since the lengths of the codes will not be specified with + * the data itself. + + * USEFUL LINKS: + * https://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Welch + * http://rosettacode.org/wiki/LZW_compression + * http://www.cs.duke.edu/csed/curious/compression/lzw.html + * http://www.cs.cf.ac.uk/Dave/Multimedia/node214.html + * http://marknelson.us/1989/10/01/lzw-data-compression/ + */ +#include "config.h" + +#if defined(THORVG_TVG_SAVER_SUPPORT) || defined(THORVG_TVG_LOADER_SUPPORT) + +/************************************************************************/ +/* Internal Class Implementation */ +/************************************************************************/ + +#include +#include +#include "tvgLzw.h" + +//LZW Dictionary helper: +constexpr int Nil = -1; +constexpr int MaxDictBits = 12; +constexpr int StartBits = 9; +constexpr int FirstCode = (1 << (StartBits - 1)); // 256 +constexpr int MaxDictEntries = (1 << MaxDictBits); // 4096 + + +//Round up to the next power-of-two number, e.g. 37 => 64 +static int nextPowerOfTwo(int num) +{ + --num; + for (size_t i = 1; i < sizeof(num) * 8; i <<= 1) { + num = num | num >> i; + } + return ++num; +} + + +struct BitStreamWriter +{ + uint8_t* stream; //Growable buffer to store our bits. Heap allocated & owned by the class instance. + int bytesAllocated; //Current size of heap-allocated stream buffer *in bytes*. + int granularity; //Amount bytesAllocated multiplies by when auto-resizing in appendBit(). + int currBytePos; //Current byte being written to, from 0 to bytesAllocated-1. + int nextBitPos; //Bit position within the current byte to access next. 0 to 7. + int numBitsWritten; //Number of bits in use from the stream buffer, not including byte-rounding padding. + + void internalInit() + { + stream = nullptr; + bytesAllocated = 0; + granularity = 2; + currBytePos = 0; + nextBitPos = 0; + numBitsWritten = 0; + } + + uint8_t* allocBytes(const int bytesWanted, uint8_t * oldPtr, const int oldSize) + { + auto newMemory = static_cast(malloc(bytesWanted)); + memset(newMemory, 0, bytesWanted); + + if (oldPtr) { + memcpy(newMemory, oldPtr, oldSize); + free(oldPtr); + } + return newMemory; + } + + BitStreamWriter() + { + /* 8192 bits for a start (1024 bytes). It will resize if needed. + Default granularity is 2. */ + internalInit(); + allocate(8192); + } + + BitStreamWriter(const int initialSizeInBits, const int growthGranularity = 2) + { + internalInit(); + setGranularity(growthGranularity); + allocate(initialSizeInBits); + } + + ~BitStreamWriter() + { + free(stream); + } + + void allocate(int bitsWanted) + { + //Require at least a byte. + if (bitsWanted <= 0) bitsWanted = 8; + + //Round upwards if needed: + if ((bitsWanted % 8) != 0) bitsWanted = nextPowerOfTwo(bitsWanted); + + //We might already have the required count. + const int sizeInBytes = bitsWanted / 8; + if (sizeInBytes <= bytesAllocated) return; + + stream = allocBytes(sizeInBytes, stream, bytesAllocated); + bytesAllocated = sizeInBytes; + } + + void appendBit(const int bit) + { + const uint32_t mask = uint32_t(1) << nextBitPos; + stream[currBytePos] = (stream[currBytePos] & ~mask) | (-bit & mask); + ++numBitsWritten; + + if (++nextBitPos == 8) { + nextBitPos = 0; + if (++currBytePos == bytesAllocated) allocate(bytesAllocated * granularity * 8); + } + } + + void appendBitsU64(const uint64_t num, const int bitCount) + { + for (int b = 0; b < bitCount; ++b) { + const uint64_t mask = uint64_t(1) << b; + const int bit = !!(num & mask); + appendBit(bit); + } + } + + uint8_t* release() + { + auto oldPtr = stream; + internalInit(); + return oldPtr; + } + + void setGranularity(const int growthGranularity) + { + granularity = (growthGranularity >= 2) ? growthGranularity : 2; + } + + int getByteCount() const + { + int usedBytes = numBitsWritten / 8; + int leftovers = numBitsWritten % 8; + if (leftovers != 0) ++usedBytes; + return usedBytes; + } +}; + + +struct BitStreamReader +{ + const uint8_t* stream; // Pointer to the external bit stream. Not owned by the reader. + const int sizeInBytes; // Size of the stream *in bytes*. Might include padding. + const int sizeInBits; // Size of the stream *in bits*, padding *not* include. + int currBytePos = 0; // Current byte being read in the stream. + int nextBitPos = 0; // Bit position within the current byte to access next. 0 to 7. + int numBitsRead = 0; // Total bits read from the stream so far. Never includes byte-rounding padding. + + BitStreamReader(const uint8_t* bitStream, const int byteCount, const int bitCount) : stream(bitStream), sizeInBytes(byteCount), sizeInBits(bitCount) + { + } + + bool readNextBit(int& bitOut) + { + if (numBitsRead >= sizeInBits) return false; //We are done. + + const uint32_t mask = uint32_t(1) << nextBitPos; + bitOut = !!(stream[currBytePos] & mask); + ++numBitsRead; + + if (++nextBitPos == 8) { + nextBitPos = 0; + ++currBytePos; + } + return true; + } + + uint64_t readBitsU64(const int bitCount) + { + uint64_t num = 0; + for (int b = 0; b < bitCount; ++b) { + int bit; + if (!readNextBit(bit)) break; + /* Based on a "Stanford bit-hack": + http://graphics.stanford.edu/~seander/bithacks.html#ConditionalSetOrClearBitsWithoutBranching */ + const uint64_t mask = uint64_t(1) << b; + num = (num & ~mask) | (-bit & mask); + } + return num; + } + + bool isEndOfStream() const + { + return numBitsRead >= sizeInBits; + } +}; + + +struct Dictionary +{ + struct Entry + { + int code; + int value; + }; + + //Dictionary entries 0-255 are always reserved to the byte/ASCII range. + int size; + Entry entries[MaxDictEntries]; + + Dictionary() + { + /* First 256 dictionary entries are reserved to the byte/ASCII range. + Additional entries follow for the character sequences found in the input. + Up to 4096 - 256 (MaxDictEntries - FirstCode). */ + size = FirstCode; + + for (int i = 0; i < size; ++i) { + entries[i].code = Nil; + entries[i].value = i; + } + } + + int findIndex(const int code, const int value) const + { + if (code == Nil) return value; + + //Linear search for now. + //TODO: Worth optimizing with a proper hash-table? + for (int i = 0; i < size; ++i) { + if (entries[i].code == code && entries[i].value == value) return i; + } + return Nil; + } + + bool add(const int code, const int value) + { + if (size == MaxDictEntries) return false; + entries[size].code = code; + entries[size].value = value; + ++size; + return true; + } + + bool flush(int & codeBitsWidth) + { + if (size == (1 << codeBitsWidth)) { + ++codeBitsWidth; + if (codeBitsWidth > MaxDictBits) { + //Clear the dictionary (except the first 256 byte entries). + codeBitsWidth = StartBits; + size = FirstCode; + return true; + } + } + return false; + } +}; + + +static bool outputByte(int code, uint8_t*& output, int outputSizeBytes, int& bytesDecodedSoFar) +{ + if (bytesDecodedSoFar >= outputSizeBytes) return false; + *output++ = static_cast(code); + ++bytesDecodedSoFar; + return true; +} + + +static bool outputSequence(const Dictionary& dict, int code, uint8_t*& output, int outputSizeBytes, int& bytesDecodedSoFar, int& firstByte) +{ + /* A sequence is stored backwards, so we have to write + it to a temp then output the buffer in reverse. */ + int i = 0; + uint8_t sequence[MaxDictEntries]; + + do { + sequence[i++] = dict.entries[code].value; + code = dict.entries[code].code; + } while (code >= 0); + + firstByte = sequence[--i]; + + for (; i >= 0; --i) { + if (!outputByte(sequence[i], output, outputSizeBytes, bytesDecodedSoFar)) return false; + } + return true; +} + + + +/************************************************************************/ +/* External Class Implementation */ +/************************************************************************/ + +namespace tvg { + +uint8_t* lzwDecode(const uint8_t* compressed, uint32_t compressedSizeBytes, uint32_t compressedSizeBits, uint32_t uncompressedSizeBytes) +{ + int code = Nil; + int prevCode = Nil; + int firstByte = 0; + int bytesDecoded = 0; + int codeBitsWidth = StartBits; + auto uncompressed = (uint8_t*) malloc(sizeof(uint8_t) * uncompressedSizeBytes); + auto ptr = uncompressed; + + /* We'll reconstruct the dictionary based on the bit stream codes. + Unlike Huffman encoding, we don't store the dictionary as a prefix to the data. */ + Dictionary dictionary; + BitStreamReader bitStream(compressed, compressedSizeBytes, compressedSizeBits); + + /* We check to avoid an overflow of the user buffer. + If the buffer is smaller than the decompressed size, we break the loop and return the current decompression count. */ + while (!bitStream.isEndOfStream()) { + code = static_cast(bitStream.readBitsU64(codeBitsWidth)); + + if (prevCode == Nil) { + if (!outputByte(code, ptr, uncompressedSizeBytes, bytesDecoded)) break; + firstByte = code; + prevCode = code; + continue; + } + if (code >= dictionary.size) { + if (!outputSequence(dictionary, prevCode, ptr, uncompressedSizeBytes, bytesDecoded, firstByte)) break; + if (!outputByte(firstByte, ptr, uncompressedSizeBytes, bytesDecoded)) break; + } else if (!outputSequence(dictionary, code, ptr, uncompressedSizeBytes, bytesDecoded, firstByte)) break; + + dictionary.add(prevCode, firstByte); + if (dictionary.flush(codeBitsWidth)) prevCode = Nil; + else prevCode = code; + } + + return uncompressed; +} + + +uint8_t* lzwEncode(const uint8_t* uncompressed, uint32_t uncompressedSizeBytes, uint32_t* compressedSizeBytes, uint32_t* compressedSizeBits) +{ + //LZW encoding context: + int code = Nil; + int codeBitsWidth = StartBits; + Dictionary dictionary; + + //Output bit stream we write to. This will allocate memory as needed to accommodate the encoded data. + BitStreamWriter bitStream; + + for (; uncompressedSizeBytes > 0; --uncompressedSizeBytes, ++uncompressed) { + const int value = *uncompressed; + const int index = dictionary.findIndex(code, value); + + if (index != Nil) { + code = index; + continue; + } + + //Write the dictionary code using the minimum bit-with: + bitStream.appendBitsU64(code, codeBitsWidth); + + //Flush it when full so we can restart the sequences. + if (!dictionary.flush(codeBitsWidth)) { + //There's still space for this sequence. + dictionary.add(code, value); + } + code = value; + } + + //Residual code at the end: + if (code != Nil) bitStream.appendBitsU64(code, codeBitsWidth); + + //Pass ownership of the compressed data buffer to the user pointer: + *compressedSizeBytes = bitStream.getByteCount(); + *compressedSizeBits = bitStream.numBitsWritten; + + return bitStream.release(); +} + +} + +#endif \ No newline at end of file diff --git a/src/lib/tvgLzw.h b/src/lib/tvgLzw.h new file mode 100644 index 00000000..3fdb439a --- /dev/null +++ b/src/lib/tvgLzw.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020-2021 Samsung Electronics Co., Ltd. All rights reserved. + + * 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. + */ +#ifndef _TVG_LZW_H_ +#define _TVG_LZW_H_ + +namespace tvg +{ + uint8_t* lzwEncode(const uint8_t* uncompressed, uint32_t uncompressedSizeBytes, uint32_t* compressedSizeBytes, uint32_t* compressedSizeBits); + uint8_t* lzwDecode(const uint8_t* compressed, uint32_t compressedSizeBytes, uint32_t compressedSizeBits, uint32_t uncompressedSizeBytes); +} + +#endif //_TVG_LZW_H \ No newline at end of file diff --git a/src/lib/tvgSaveModule.h b/src/lib/tvgSaveModule.h index 4521b38b..6342670b 100644 --- a/src/lib/tvgSaveModule.h +++ b/src/lib/tvgSaveModule.h @@ -32,7 +32,7 @@ class SaveModule public: virtual ~SaveModule() {} - virtual bool save(Paint* paint, const string& path) = 0; + virtual bool save(Paint* paint, const string& path, bool compress) = 0; virtual bool close() = 0; //Utility Method: Iterator Delegator diff --git a/src/lib/tvgSaver.cpp b/src/lib/tvgSaver.cpp index 1c5fafdb..ffa2bf1a 100644 --- a/src/lib/tvgSaver.cpp +++ b/src/lib/tvgSaver.cpp @@ -97,7 +97,7 @@ Saver::~Saver() } -Result Saver::save(std::unique_ptr paint, const string& path) noexcept +Result Saver::save(std::unique_ptr paint, const string& path, bool compress) noexcept { //Already on saving an other resource. if (pImpl->saveModule) return Result::InsufficientCondition; @@ -106,7 +106,7 @@ Result Saver::save(std::unique_ptr paint, const string& path) noexcept if (!p) return Result::MemoryCorruption; if (auto saveModule = _find(path)) { - if (saveModule->save(p, path)) { + if (saveModule->save(p, path, compress)) { pImpl->saveModule = saveModule; return Result::Success; } else { diff --git a/src/loaders/tvg/tvgTvgBinInterpreter.cpp b/src/loaders/tvg/tvgTvgBinInterpreter.cpp index b549e9ad..b6180c95 100644 --- a/src/loaders/tvg/tvgTvgBinInterpreter.cpp +++ b/src/loaders/tvg/tvgTvgBinInterpreter.cpp @@ -460,6 +460,7 @@ static Paint* _parsePaint(TvgBinBlock baseBlock) } + /************************************************************************/ /* External Class Implementation */ /************************************************************************/ @@ -477,4 +478,4 @@ unique_ptr TvgBinInterpreter::run(const char *ptr, const char* end) } return scene; -} +} \ No newline at end of file diff --git a/src/loaders/tvg/tvgTvgLoader.cpp b/src/loaders/tvg/tvgTvgLoader.cpp index 6d0433a8..5dfa02bb 100644 --- a/src/loaders/tvg/tvgTvgLoader.cpp +++ b/src/loaders/tvg/tvgTvgLoader.cpp @@ -23,6 +23,7 @@ #include #include "tvgLoader.h" #include "tvgTvgLoader.h" +#include "tvgLzw.h" /************************************************************************/ @@ -62,15 +63,34 @@ bool TvgLoader::readHeader() TVGLOG("TVG", "This TVG file expects a higher version(%d) of ThorVG symbol(%d)", this->version, THORVG_VERSION_NUMBER()); } - //3. Reserved - ptr += TVG_HEADER_RESERVED_LENGTH; - - //4. View Size + //3. View Size READ_FLOAT(&w, ptr); ptr += SIZE(float); READ_FLOAT(&h, ptr); ptr += SIZE(float); + //4. Reserved + if (*ptr & TVG_HEAD_FLAG_COMPRESSED) compressed = true; + ptr += TVG_HEADER_RESERVED_LENGTH; + + //5. Compressed Size if any + if (compressed) { + auto p = reinterpret_cast(const_cast(ptr)); + + //TVG_HEADER_UNCOMPRESSED_SIZE + uncompressedSize = *static_cast(p); + ++p; + + //TVG_HEADER_COMPRESSED_SIZE + compressedSize = *static_cast(p); + ++p; + + //TVG_HEADER_COMPRESSED_SIZE_BITS + compressedSizeBits = *static_cast(p); + } + + ptr += TVG_HEADER_COMPRESS_SIZE; + //Decide the proper Tvg Binary Interpreter based on the current file version if (this->version >= 0) interpreter = new TvgBinInterpreter; @@ -186,7 +206,15 @@ void TvgLoader::run(unsigned tid) { if (root) root.reset(); - root = interpreter->run(ptr, data + size); + auto data = const_cast(ptr); + + if (compressed) { + data = (char*) lzwDecode((uint8_t*) data, compressedSize, compressedSizeBits, uncompressedSize); + } + + root = interpreter->run(data, data + uncompressedSize); + + if (compressed) delete(data); if (!root) clear(); } diff --git a/src/loaders/tvg/tvgTvgLoader.h b/src/loaders/tvg/tvgTvgLoader.h index ba756d03..d276ded3 100644 --- a/src/loaders/tvg/tvgTvgLoader.h +++ b/src/loaders/tvg/tvgTvgLoader.h @@ -36,7 +36,11 @@ public: uint16_t version = 0; unique_ptr root = nullptr; TvgBinInterpreterBase* interpreter = nullptr; + uint32_t uncompressedSize = 0; + uint32_t compressedSize = 0; + uint32_t compressedSizeBits = 0; bool copy = false; + bool compressed = false; ~TvgLoader(); diff --git a/src/savers/tvg/tvgTvgSaver.cpp b/src/savers/tvg/tvgTvgSaver.cpp index 11df2b4c..baf079c6 100644 --- a/src/savers/tvg/tvgTvgSaver.cpp +++ b/src/savers/tvg/tvgTvgSaver.cpp @@ -23,6 +23,7 @@ #include #include "tvgSaveModule.h" #include "tvgTvgSaver.h" +#include "tvgLzw.h" #define SIZE(A) sizeof(A) @@ -128,12 +129,75 @@ static bool _merge(Shape* from, Shape* to) } +bool TvgSaver::saveEncoding(const std::string& path) +{ + if (!compress) return flushTo(path); + + //Try encoding + auto uncompressed = buffer.data + headerSize; + auto uncompressedSize = buffer.count - headerSize; + + uint32_t compressedSize, compressedSizeBits; + + auto compressed = lzwEncode(uncompressed, uncompressedSize, &compressedSize, &compressedSizeBits); + + //Failed compression. + if (!compressed) return flushTo(path); + + //Optimization is ineffective. + if (compressedSize >= uncompressedSize) { + free(compressed); + return flushTo(path); + } + + TVGLOG("TVG_SAVER", "%s, compressed: %d -> %d, saved rate: %3.2f%%", path.c_str(), uncompressedSize, compressedSize, (1 - ((float) compressedSize / (float) uncompressedSize)) * 100); + + //Update compress size in the header. + uncompressed -= (TVG_HEADER_COMPRESS_SIZE + TVG_HEADER_RESERVED_LENGTH); + + //Compression Flag + *uncompressed |= TVG_HEAD_FLAG_COMPRESSED; + uncompressed += TVG_HEADER_RESERVED_LENGTH; + + //Uncompressed Size + *reinterpret_cast(uncompressed) = uncompressedSize; + uncompressed += TVG_HEADER_UNCOMPRESSED_SIZE; + + //Comprssed Size + *reinterpret_cast(uncompressed) = compressedSize; + uncompressed += TVG_HEADER_COMPRESSED_SIZE; + + //Compressed Size Bits + *reinterpret_cast(uncompressed) = compressedSizeBits; + + //Good optimization, flush to file. + auto fp = fopen(path.c_str(), "w+"); + if (!fp) goto fail; + + //write header + if (fwrite(buffer.data, SIZE(uint8_t), headerSize, fp) == 0) goto fail; + + //write compressed data + if (fwrite(compressed, SIZE(uint8_t), compressedSize, fp) == 0) goto fail; + + fclose(fp); + free(compressed); + + return true; + +fail: + if (fp) fclose(fp); + if (compressed) free(compressed); + return false; +} + + bool TvgSaver::flushTo(const std::string& path) { - FILE* fp = fopen(path.c_str(), "w+"); + auto fp = fopen(path.c_str(), "w+"); if (!fp) return false; - if (fwrite(buffer.data, SIZE(char), buffer.count, fp) == 0) { + if (fwrite(buffer.data, SIZE(uint8_t), buffer.count, fp) == 0) { fclose(fp); return false; } @@ -146,7 +210,8 @@ bool TvgSaver::flushTo(const std::string& path) /* WARNING: Header format shall not changed! */ bool TvgSaver::writeHeader() { - auto headerSize = TVG_HEADER_SIGNATURE_LENGTH + TVG_HEADER_VERSION_LENGTH + TVG_HEADER_RESERVED_LENGTH + SIZE(vsize); + headerSize = TVG_HEADER_SIGNATURE_LENGTH + TVG_HEADER_VERSION_LENGTH + SIZE(vsize) + TVG_HEADER_RESERVED_LENGTH + TVG_HEADER_COMPRESS_SIZE; + buffer.grow(headerSize); //1. Signature @@ -158,11 +223,14 @@ bool TvgSaver::writeHeader() memcpy(ptr, TVG_HEADER_VERSION, TVG_HEADER_VERSION_LENGTH); ptr += TVG_HEADER_VERSION_LENGTH; - buffer.count += (TVG_HEADER_SIGNATURE_LENGTH + TVG_HEADER_VERSION_LENGTH + TVG_HEADER_RESERVED_LENGTH); + buffer.count += (TVG_HEADER_SIGNATURE_LENGTH + TVG_HEADER_VERSION_LENGTH); //3. View Size writeData(vsize, SIZE(vsize)); + //4. Reserved data + Compress size + buffer.count += TVG_HEADER_RESERVED_LENGTH + TVG_HEADER_COMPRESS_SIZE; + return true; } @@ -592,7 +660,7 @@ void TvgSaver::run(unsigned tid) { if (!writeHeader()) return; if (serialize(paint, nullptr) == 0) return; - if (!flushTo(path)) return; + if (!saveEncoding(path)) return; } @@ -623,7 +691,7 @@ bool TvgSaver::close() } -bool TvgSaver::save(Paint* paint, const string& path) +bool TvgSaver::save(Paint* paint, const string& path, bool compress) { close(); @@ -637,6 +705,7 @@ bool TvgSaver::save(Paint* paint, const string& path) } this->paint = paint; + this->compress = compress; TaskScheduler::request(this); diff --git a/src/savers/tvg/tvgTvgSaver.h b/src/savers/tvg/tvgTvgSaver.h index 90761736..4d70e88d 100644 --- a/src/savers/tvg/tvgTvgSaver.h +++ b/src/savers/tvg/tvgTvgSaver.h @@ -35,9 +35,12 @@ private: Array buffer; Paint* paint = nullptr; char *path = nullptr; + uint32_t headerSize; float vsize[2] = {0.0f, 0.0f}; + bool compress; bool flushTo(const std::string& path); + bool saveEncoding(const std::string& path); void reserveCount(); bool writeHeader(); @@ -64,7 +67,7 @@ private: public: ~TvgSaver(); - bool save(Paint* paint, const string& path) override; + bool save(Paint* paint, const string& path, bool compress) override; bool close() override; void run(unsigned tid) override; };