/* * Copyright (c) 2021 - 2024 the ThorVG project. 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. */ #include #include "tvgSaveModule.h" #include "tvgTvgSaver.h" #include "tvgCompressor.h" #include "tvgShape.h" #include "tvgFill.h" #include "tvgPicture.h" #ifdef _WIN32 #include #elif defined(__linux__) #include #else #include #endif static FILE* _fopen(const char* filename, const char* mode) { #if defined(_MSC_VER) && defined(__clang__) FILE *fp; auto err = fopen_s(&fp, filename, mode); if (err != 0) return nullptr; return fp; #else auto fp = fopen(filename, mode); if (!fp) return nullptr; return fp; #endif } #define SIZE(A) sizeof(A) /************************************************************************/ /* Internal Class Implementation */ /************************************************************************/ static inline TvgBinCounter SERIAL_DONE(TvgBinCounter cnt) { return SIZE(TvgBinTag) + SIZE(TvgBinCounter) + cnt; } /* if the properties are identical, we can merge the shapes. */ static bool _merge(Shape* from, Shape* to) { uint8_t r, g, b, a; uint8_t r2, g2, b2, a2; //fill if (from->fill() || to->fill()) return false; r = g = b = a = r2 = g2 = b2 = a2 = 0; from->fillColor(&r, &g, &b, &a); to->fillColor(&r2, &g2, &b2, &a2); if (r != r2 || g != g2 || b != b2 || a != a2 || a < 255) return false; auto fromRule = from->fillRule(); if (fromRule == FillRule::EvenOdd || fromRule != to->fillRule()) return false; //composition if (from->composite(nullptr) != CompositeMethod::None) return false; if (to->composite(nullptr) != CompositeMethod::None) return false; //opacity if (from->opacity() != to->opacity()) return false; //transform auto t1 = from->transform(); auto t2 = to->transform(); if (t1 != t2) return false; //stroke if (P(from)->strokeFirst() != P(to)->strokeFirst()) return false; r = g = b = a = r2 = g2 = b2 = a2 = 0; from->strokeColor(&r, &g, &b, &a); to->strokeColor(&r2, &g2, &b2, &a2); if (r != r2 || g != g2 || b != b2 || a != a2) return false; if (fabs(from->strokeWidth() - to->strokeWidth()) > FLOAT_EPSILON) return false; //OPTIMIZE: Yet we can't merge outlining shapes unless we can support merging shapes feature. if (from->strokeWidth() > 0 || to->strokeWidth() > 0) return false; if (from->strokeCap() != to->strokeCap()) return false; if (from->strokeJoin() != to->strokeJoin()) return false; if (from->strokeDash(nullptr) > 0 || to->strokeDash(nullptr) > 0) return false; if (from->strokeFill() || to->strokeFill()) return false; if (fabsf(from->strokeMiterlimit() - to->strokeMiterlimit()) > FLOAT_EPSILON) return false; //fill rule if (from->fillRule() != to->fillRule()) return false; //Good, identical shapes, we can merge them. const PathCommand* cmds = nullptr; auto cmdCnt = from->pathCommands(&cmds); const Point* pts = nullptr; auto ptsCnt = from->pathCoords(&pts); to->appendPath(cmds, cmdCnt, pts, ptsCnt); return true; } 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 memcpy(uncompressed, &uncompressedSize, TVG_HEADER_UNCOMPRESSED_SIZE); uncompressed += TVG_HEADER_UNCOMPRESSED_SIZE; //Compressed Size memcpy(uncompressed, &compressedSize, TVG_HEADER_COMPRESSED_SIZE); uncompressed += TVG_HEADER_COMPRESSED_SIZE; //Compressed Size Bits memcpy(uncompressed, &compressedSizeBits, TVG_HEADER_COMPRESSED_SIZE_BITS); //Good optimization, flush to file. auto fp = _fopen(path.c_str(), "wb+"); 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) { auto fp = _fopen(path.c_str(), "wb+"); if (!fp) return false; if (fwrite(buffer.data, SIZE(uint8_t), buffer.count, fp) == 0) { fclose(fp); return false; } fclose(fp); return true; } /* WARNING: Header format shall not changed! */ bool TvgSaver::writeHeader() { headerSize = TVG_HEADER_SIGNATURE_LENGTH + TVG_HEADER_VERSION_LENGTH + SIZE(vsize) + TVG_HEADER_RESERVED_LENGTH + TVG_HEADER_COMPRESS_SIZE; buffer.grow(headerSize); //1. Signature auto ptr = buffer.end(); memcpy(ptr, TVG_HEADER_SIGNATURE, TVG_HEADER_SIGNATURE_LENGTH); ptr += TVG_HEADER_SIGNATURE_LENGTH; //2. Version memcpy(ptr, TVG_HEADER_VERSION, TVG_HEADER_VERSION_LENGTH); ptr += TVG_HEADER_VERSION_LENGTH; buffer.count += (TVG_HEADER_SIGNATURE_LENGTH + TVG_HEADER_VERSION_LENGTH); //3. View Size writeData(vsize, SIZE(vsize)); ptr += SIZE(vsize); //4. Reserved data + Compress size memset(ptr, 0x00, TVG_HEADER_RESERVED_LENGTH + TVG_HEADER_COMPRESS_SIZE); buffer.count += (TVG_HEADER_RESERVED_LENGTH + TVG_HEADER_COMPRESS_SIZE); return true; } void TvgSaver::writeTag(TvgBinTag tag) { buffer.grow(SIZE(TvgBinTag)); memcpy(buffer.end(), &tag, SIZE(TvgBinTag)); buffer.count += SIZE(TvgBinTag); } void TvgSaver::writeCount(TvgBinCounter cnt) { buffer.grow(SIZE(TvgBinCounter)); memcpy(buffer.end(), &cnt, SIZE(TvgBinCounter)); buffer.count += SIZE(TvgBinCounter); } void TvgSaver::writeReservedCount(TvgBinCounter cnt) { memcpy(buffer.end() - cnt - SIZE(TvgBinCounter), &cnt, SIZE(TvgBinCounter)); } void TvgSaver::reserveCount() { buffer.grow(SIZE(TvgBinCounter)); buffer.count += SIZE(TvgBinCounter); } TvgBinCounter TvgSaver::writeData(const void* data, TvgBinCounter cnt) { buffer.grow(cnt); memcpy(buffer.end(), data, cnt); buffer.count += cnt; return cnt; } TvgBinCounter TvgSaver::writeTagProperty(TvgBinTag tag, TvgBinCounter cnt, const void* data) { auto growCnt = SERIAL_DONE(cnt); buffer.grow(growCnt); auto ptr = buffer.end(); *ptr = tag; ++ptr; memcpy(ptr, &cnt, SIZE(TvgBinCounter)); ptr += SIZE(TvgBinCounter); memcpy(ptr, data, cnt); ptr += cnt; buffer.count += growCnt; return growCnt; } TvgBinCounter TvgSaver::writeTransform(const Matrix* transform, TvgBinTag tag) { if (!identity(transform)) return writeTagProperty(tag, SIZE(Matrix), transform); return 0; } TvgBinCounter TvgSaver::serializePaint(const Paint* paint, const Matrix* pTransform) { TvgBinCounter cnt = 0; //opacity auto opacity = paint->opacity(); if (opacity < 255) { cnt += writeTagProperty(TVG_TAG_PAINT_OPACITY, SIZE(opacity), &opacity); } //composite const Paint* cmpTarget = nullptr; auto cmpMethod = paint->composite(&cmpTarget); if (cmpMethod != CompositeMethod::None && cmpTarget) { cnt += serializeComposite(cmpTarget, cmpMethod, pTransform); } return cnt; } /* Propagate parents properties to the child so that we can skip saving the parent. */ TvgBinCounter TvgSaver::serializeChild(const Paint* parent, const Paint* child, const Matrix* transform) { const Paint* compTarget = nullptr; auto compMethod = parent->composite(&compTarget); /* If the parent & the only child have composition, we can't skip the parent... Or if the parent has the transform and composition, we can't skip the parent... */ if (compMethod != CompositeMethod::None) { if (transform || child->composite(nullptr) != CompositeMethod::None) return 0; } //propagate opacity uint32_t opacity = parent->opacity(); if (opacity < 255) { uint32_t tmp = (child->opacity() * opacity); if (tmp > 0) tmp /= 255; const_cast(child)->opacity(tmp); } //propagate composition if (compTarget) const_cast(child)->composite(cast(compTarget->duplicate()), compMethod); return serialize(child, transform); } TvgBinCounter TvgSaver::serializeScene(const Scene* scene, const Matrix* pTransform, const Matrix* cTransform) { auto it = IteratorAccessor::iterator(scene); if (it->count() == 0) { delete(it); return 0; } //Case - Only Child: Skip saving this scene. if (it->count() == 1) { auto cnt = serializeChild(scene, it->next(), cTransform); if (cnt > 0) { delete(it); return cnt; } } it->begin(); //Case - Serialize Scene & its children writeTag(TVG_TAG_CLASS_SCENE); reserveCount(); auto cnt = serializeChildren(it, cTransform, true) + serializePaint(scene, pTransform); delete(it); writeReservedCount(cnt); return SERIAL_DONE(cnt); } TvgBinCounter TvgSaver::serializeFill(const Fill* fill, TvgBinTag tag, const Matrix* pTransform) { const Fill::ColorStop* stops = nullptr; auto stopsCnt = fill->colorStops(&stops); if (!stops || stopsCnt == 0) return 0; writeTag(tag); reserveCount(); TvgBinCounter cnt = 0; //radial fill if (fill->type() == Type::RadialGradient) { const RadialGradient* radial = static_cast(fill); float args[3]; radial->radial(args, args + 1, args + 2); cnt += writeTagProperty(TVG_TAG_FILL_RADIAL_GRADIENT, SIZE(args), args); //focal if (!tvg::zero(P(radial)->fx)|| !tvg::zero(P(radial)->fy) || P(radial)->fr > 0.0f) { args[0] = P(radial)->fx; args[1] = P(radial)->fy; args[2] = P(radial)->fr; cnt += writeTagProperty(TVG_TAG_FILL_RADIAL_GRADIENT_FOCAL, SIZE(args), args); } //linear fill } else { float args[4]; static_cast(fill)->linear(args, args + 1, args + 2, args + 3); cnt += writeTagProperty(TVG_TAG_FILL_LINEAR_GRADIENT, SIZE(args), args); } if (auto flag = static_cast(fill->spread())) cnt += writeTagProperty(TVG_TAG_FILL_FILLSPREAD, SIZE(TvgBinFlag), &flag); cnt += writeTagProperty(TVG_TAG_FILL_COLORSTOPS, stopsCnt * SIZE(Fill::ColorStop), stops); auto gTransform = fill->transform(); if (pTransform) gTransform = *pTransform * gTransform; cnt += writeTransform(&gTransform, TVG_TAG_FILL_TRANSFORM); writeReservedCount(cnt); return SERIAL_DONE(cnt); } TvgBinCounter TvgSaver::serializeStroke(const Shape* shape, const Matrix* pTransform, bool preTransform) { writeTag(TVG_TAG_SHAPE_STROKE); reserveCount(); //width auto width = shape->strokeWidth(); if (preTransform) width *= sqrtf(powf(pTransform->e11, 2.0f) + powf(pTransform->e21, 2.0f)); //we know x/y scaling factors are same. auto cnt = writeTagProperty(TVG_TAG_SHAPE_STROKE_WIDTH, SIZE(width), &width); //cap if (auto flag = static_cast(shape->strokeCap())) cnt += writeTagProperty(TVG_TAG_SHAPE_STROKE_CAP, SIZE(TvgBinFlag), &flag); //join if (auto flag = static_cast(shape->strokeJoin())) cnt += writeTagProperty(TVG_TAG_SHAPE_STROKE_JOIN, SIZE(TvgBinFlag), &flag); //order if (auto flag = static_cast(P(shape)->strokeFirst())) cnt += writeTagProperty(TVG_TAG_SHAPE_STROKE_ORDER, SIZE(TvgBinFlag), &flag); //fill if (auto fill = shape->strokeFill()) { cnt += serializeFill(fill, TVG_TAG_SHAPE_STROKE_FILL, (preTransform ? pTransform : nullptr)); } else { uint8_t color[4] = {0, 0, 0, 0}; shape->strokeColor(color, color + 1, color + 2, color + 3); cnt += writeTagProperty(TVG_TAG_SHAPE_STROKE_COLOR, SIZE(color), &color); } //dash const float* dashPattern = nullptr; float offset = 0.0f; auto dashCnt = P(shape)->rs.strokeDash(&dashPattern, &offset); if (dashPattern && dashCnt > 0) { TvgBinCounter dashCntSize = SIZE(dashCnt); TvgBinCounter dashPtrnSize = dashCnt * SIZE(dashPattern[0]); writeTag(TVG_TAG_SHAPE_STROKE_DASHPTRN); writeCount(dashCntSize + dashPtrnSize); cnt += writeData(&dashCnt, dashCntSize); cnt += writeData(dashPattern, dashPtrnSize); cnt += SIZE(TvgBinTag) + SIZE(TvgBinCounter); } //miterlimit (the default value is 4) auto miterlimit = shape->strokeMiterlimit(); if (fabsf(miterlimit - 4.0f) > FLOAT_EPSILON) { cnt += writeTagProperty(TVG_TAG_SHAPE_STROKE_MITERLIMIT, SIZE(miterlimit), &miterlimit); } //dash offset if (!tvg::zero(offset)) { cnt += writeTagProperty(TVG_TAG_SHAPE_STROKE_DASH_OFFSET, SIZE(offset), &offset); } writeReservedCount(cnt); return SERIAL_DONE(cnt); } TvgBinCounter TvgSaver::serializePath(const Shape* shape, const Matrix* transform, bool preTransform) { const PathCommand* cmds = nullptr; auto cmdCnt = shape->pathCommands(&cmds); const Point* pts = nullptr; auto ptsCnt = shape->pathCoords(&pts); if (!cmds || !pts || cmdCnt == 0 || ptsCnt == 0) return 0; writeTag(TVG_TAG_SHAPE_PATH); reserveCount(); /* Reduce the binary size. Convert PathCommand(4 bytes) to TvgBinFlag(1 byte) */ TvgBinFlag* outCmds = (TvgBinFlag*)alloca(SIZE(TvgBinFlag) * cmdCnt); for (uint32_t i = 0; i < cmdCnt; ++i) { outCmds[i] = static_cast(cmds[i]); } auto cnt = writeData(&cmdCnt, SIZE(cmdCnt)); cnt += writeData(&ptsCnt, SIZE(ptsCnt)); cnt += writeData(outCmds, SIZE(TvgBinFlag) * cmdCnt); //transform? if (preTransform) { if (!tvg::equal(transform->e11, 1.0f) || !tvg::zero(transform->e12) || !tvg::zero(transform->e13) || !tvg::zero(transform->e21) || !tvg::equal(transform->e22, 1.0f) || !tvg::zero(transform->e23) || !tvg::zero(transform->e31) || !tvg::zero(transform->e32) || !tvg::equal(transform->e33, 1.0f)) { auto p = const_cast(pts); for (uint32_t i = 0; i < ptsCnt; ++i) { *p *= *transform; ++p; } } } cnt += writeData(pts, ptsCnt * SIZE(pts[0])); writeReservedCount(cnt); return SERIAL_DONE(cnt); } TvgBinCounter TvgSaver::serializeShape(const Shape* shape, const Matrix* pTransform, const Matrix* cTransform) { writeTag(TVG_TAG_CLASS_SHAPE); reserveCount(); TvgBinCounter cnt = 0; //fill rule if (auto flag = static_cast(shape->fillRule())) { cnt = writeTagProperty(TVG_TAG_SHAPE_FILLRULE, SIZE(TvgBinFlag), &flag); } //the pre-transformation can't be applied in the case when the stroke is dashed or irregularly scaled bool preTransform = true; //stroke if (shape->strokeWidth() > 0) { uint8_t color[4] = {0, 0, 0, 0}; shape->strokeColor(color, color + 1, color + 2, color + 3); auto fill = shape->strokeFill(); if (fill || color[3] > 0) { if (!tvg::equal(cTransform->e11, cTransform->e22) || (tvg::zero(cTransform->e11) && !tvg::equal(cTransform->e12, cTransform->e21)) || shape->strokeDash(nullptr) > 0) preTransform = false; cnt += serializeStroke(shape, cTransform, preTransform); } } //fill if (auto fill = shape->fill()) { cnt += serializeFill(fill, TVG_TAG_SHAPE_FILL, (preTransform ? cTransform : nullptr)); } else { uint8_t color[4] = {0, 0, 0, 0}; shape->fillColor(color, color + 1, color + 2, color + 3); if (color[3] > 0) cnt += writeTagProperty(TVG_TAG_SHAPE_COLOR, SIZE(color), color); } cnt += serializePath(shape, cTransform, preTransform); if (!preTransform) cnt += writeTransform(cTransform, TVG_TAG_PAINT_TRANSFORM); cnt += serializePaint(shape, pTransform); writeReservedCount(cnt); return SERIAL_DONE(cnt); } /* Picture has either a vector scene or a bitmap. */ TvgBinCounter TvgSaver::serializePicture(const Picture* picture, const Matrix* pTransform, const Matrix* cTransform) { auto it = IteratorAccessor::iterator(picture); //Case - Vector Scene: if (it->count() == 1) { auto cnt = serializeChild(picture, it->next(), cTransform); //Only child, Skip to save Picture... if (cnt > 0) { delete(it); return cnt; /* Unfortunately, we can't skip the Picture because it might have a compositor, Serialize Scene(instead of the Picture) & its scene. */ } else { writeTag(TVG_TAG_CLASS_SCENE); reserveCount(); auto cnt = serializeChildren(it, cTransform, true) + serializePaint(picture, pTransform); writeReservedCount(cnt); delete(it); return SERIAL_DONE(cnt); } } delete(it); //Case - Bitmap Image: uint32_t w, h; auto pixels = P(picture)->data(&w, &h); if (!pixels) return 0; writeTag(TVG_TAG_CLASS_PICTURE); reserveCount(); TvgBinCounter cnt = 0; TvgBinCounter sizeCnt = SIZE(w); TvgBinCounter imgSize = w * h * SIZE(pixels[0]); writeTag(TVG_TAG_PICTURE_RAW_IMAGE); writeCount(2 * sizeCnt + imgSize); cnt += writeData(&w, sizeCnt); cnt += writeData(&h, sizeCnt); cnt += writeData(pixels, imgSize); cnt += SIZE(TvgBinTag) + SIZE(TvgBinCounter); //mesh: currently only available in bitmap image. const Polygon* triangles = nullptr; auto triangleCnt = picture->mesh(&triangles); if (triangles && triangleCnt > 0) { TvgBinCounter triangleCntSize = SIZE(triangleCnt); TvgBinCounter trianglesSize = triangleCnt * SIZE(triangles[0]); writeTag(TVG_TAG_PICTURE_MESH); writeCount(triangleCntSize + trianglesSize); cnt += writeData(&triangleCnt, triangleCntSize); cnt += writeData(triangles, trianglesSize); cnt += SIZE(TvgBinTag) + SIZE(TvgBinCounter); } //Bitmap picture needs the transform info. cnt += writeTransform(cTransform, TVG_TAG_PAINT_TRANSFORM); cnt += serializePaint(picture, pTransform); writeReservedCount(cnt); return SERIAL_DONE(cnt); } TvgBinCounter TvgSaver::serializeComposite(const Paint* cmpTarget, CompositeMethod cmpMethod, const Matrix* pTransform) { writeTag(TVG_TAG_PAINT_CMP_TARGET); reserveCount(); auto flag = static_cast(cmpMethod); auto cnt = writeTagProperty(TVG_TAG_PAINT_CMP_METHOD, SIZE(TvgBinFlag), &flag); cnt += serialize(cmpTarget, pTransform, true); writeReservedCount(cnt); return SERIAL_DONE(cnt); } TvgBinCounter TvgSaver::serializeChildren(Iterator* it, const Matrix* pTransform, bool reserved) { TvgBinCounter cnt = 0; //Merging shapes. the result is written in the children. Array children(it->count()); children.push(it->next()); while (auto child = it->next()) { if (child->type() == Type::Shape) { //only dosable if the previous child is a shape. auto target = children.last(); if (target->type() == Type::Shape) { if (_merge((Shape*)child, (Shape*)target)) { continue; } } } children.push(child); } //TODO: Keep this for the compatibility, Remove in TVG 1.0 release //The children of a reserved scene if (reserved && children.count > 1) { cnt += writeTagProperty(TVG_TAG_SCENE_RESERVEDCNT, SIZE(children.count), &children.count); } //Serialize merged children. auto child = children.data; for (uint32_t i = 0; i < children.count; ++i, ++child) { cnt += serialize(*child, pTransform); } return cnt; } TvgBinCounter TvgSaver::serialize(const Paint* paint, const Matrix* pTransform, bool compTarget) { if (!paint) return 0; //Invisible paint, no point to save it if the paint is not the composition target... if (!compTarget && paint->opacity() == 0) return 0; auto transform = const_cast(paint)->transform(); if (pTransform) transform = *pTransform * transform; switch (paint->type()) { case Type::Shape: return serializeShape(static_cast(paint), pTransform, &transform); case Type::Scene: return serializeScene(static_cast(paint), pTransform, &transform); case Type::Picture: return serializePicture(static_cast(paint), pTransform, &transform); case Type::Text: { TVGERR("TVG", "TODO: Text Serialization!"); return 0; } default: return 0; } return 0; } void TvgSaver::run(unsigned tid) { if (!writeHeader()) return; //Serialize Root Paint, without its transform. Matrix transform = {1, 0, 0, 0, 1, 0, 0, 0, 1}; if (paint->opacity() > 0) { switch (paint->type()) { case Type::Shape: { serializeShape(static_cast(paint), nullptr, &transform); break; } case Type::Scene: { serializeScene(static_cast(paint), nullptr, &transform); break; } case Type::Picture: { serializePicture(static_cast(paint), nullptr, &transform); break; } case Type::Text: { TVGERR("TVG", "TODO: Text Serialization!"); break; } default: break; } } if (!saveEncoding(path)) return; } /************************************************************************/ /* External Class Implementation */ /************************************************************************/ TvgSaver::~TvgSaver() { close(); } bool TvgSaver::close() { this->done(); if (paint && P(paint)->refCnt == 0) delete(paint); paint = nullptr; free(path); path = nullptr; buffer.reset(); return true; } bool TvgSaver::save(Paint* paint, const string& path, bool compress) { close(); float x, y; x = y = 0; paint->bounds(&x, &y, &vsize[0], &vsize[1], false); //cut off the negative space if (x < 0) vsize[0] += x; if (y < 0) vsize[1] += y; if (vsize[0] < FLOAT_EPSILON || vsize[1] < FLOAT_EPSILON) { TVGLOG("TVG_SAVER", "Saving paint(%p) has zero view size.", paint); return false; } this->path = strdup(path.c_str()); if (!this->path) return false; this->paint = paint; this->compress = compress; TaskScheduler::request(this); return true; } bool TvgSaver::save(TVG_UNUSED Animation* animation, TVG_UNUSED Paint* bg, TVG_UNUSED const string& path, TVG_UNUSED uint32_t quality, TVG_UNUSED uint32_t fps) { TVGLOG("TVG_SAVER", "Animation is not supported."); return false; }