From d5e04be2a181163938ed3e3621f9ba28b452c22c Mon Sep 17 00:00:00 2001 From: Mira Grudzinska <67589014+mgrudzinska@users.noreply.github.com> Date: Mon, 19 Jul 2021 10:31:36 +0200 Subject: [PATCH] tvg_saver: implementation of the Saver class The Saver class enables to save any Paint object (Scene, Shape, Picture) in a binary file. To read the file the tvg loader should be used. To save a paint a new API was introduced - tvg::Saver::save. --- inc/thorvg.h | 33 ++++ src/lib/meson.build | 2 + src/lib/tvgBinaryDesc.h | 1 + src/lib/tvgPictureImpl.h | 1 + src/lib/tvgSaver.cpp | 61 ++++++ src/lib/tvgSaverImpl.h | 494 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 592 insertions(+) create mode 100644 src/lib/tvgSaver.cpp create mode 100644 src/lib/tvgSaverImpl.h diff --git a/inc/thorvg.h b/inc/thorvg.h index 3a79822..e172570 100644 --- a/inc/thorvg.h +++ b/inc/thorvg.h @@ -1378,6 +1378,39 @@ public: _TVG_DISABLE_CTOR(Initializer); }; + +/** + * @class Saver + * + * @brief A class enabling saving a paint in a binary format. + * + * @BETA_API + */ +class TVG_EXPORT Saver +{ +public: + ~Saver(); + + /** + * @brief Saves all the paints from the tree in a binary format. + * + * @param[in] paint The root paint to be saved with all its nodes. + * @param[in] path A path to the file, in which the data is to be saved. + * + * @retval Result::Success When succeed. + * @retval Result::InvalidArguments the @p path is empty or @c nullptr is passed as the @p paint. + * @retval Result::FailedAllocation An internal error with a memory allocation for the Saver object. + * @retval Result::MemoryCorruption When casting in the internal function implementation failed. + * @retval Result::Unknown Others. + * + * @BETA_API + */ + static Result save(std::unique_ptr paint, const std::string& path) noexcept; + + _TVG_DECLARE_PRIVATE(Saver); +}; + + /** @}*/ } //namespace diff --git a/src/lib/meson.build b/src/lib/meson.build index 089a8fc..c535560 100644 --- a/src/lib/meson.build +++ b/src/lib/meson.build @@ -18,6 +18,7 @@ source_file = [ 'tvgLoaderMgr.h', 'tvgPictureImpl.h', 'tvgRender.h', + 'tvgSaverImpl.h', 'tvgSceneImpl.h', 'tvgShapeImpl.h', 'tvgTaskScheduler.h', @@ -32,6 +33,7 @@ source_file = [ 'tvgPicture.cpp', 'tvgRadialGradient.cpp', 'tvgRender.cpp', + 'tvgSaver.cpp', 'tvgScene.cpp', 'tvgShape.cpp', 'tvgSwCanvas.cpp', diff --git a/src/lib/tvgBinaryDesc.h b/src/lib/tvgBinaryDesc.h index d5e5ace..6d665cf 100644 --- a/src/lib/tvgBinaryDesc.h +++ b/src/lib/tvgBinaryDesc.h @@ -54,6 +54,7 @@ struct tvgBlock #define TVG_BIN_HEADER_SIGNATURE_LENGTH 3 #define TVG_BIN_HEADER_VERSION "000" #define TVG_BIN_HEADER_VERSION_LENGTH 3 + #define TVG_BIN_HEADER_DATA_LENGTH 2 #endif #define TVG_PICTURE_BEGIN_INDICATOR (TvgIndicator)0xfc diff --git a/src/lib/tvgPictureImpl.h b/src/lib/tvgPictureImpl.h index e2bf355..1178afb 100644 --- a/src/lib/tvgPictureImpl.h +++ b/src/lib/tvgPictureImpl.h @@ -221,6 +221,7 @@ struct Picture::Impl Paint::Iterator begin() { + reload(); return Paint::Iterator(picture, paint); } diff --git a/src/lib/tvgSaver.cpp b/src/lib/tvgSaver.cpp new file mode 100644 index 0000000..761c571 --- /dev/null +++ b/src/lib/tvgSaver.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 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. + */ +#include "tvgSaverImpl.h" +#include + +/************************************************************************/ +/* Internal Class Implementation */ +/************************************************************************/ + +/************************************************************************/ +/* External Class Implementation */ +/************************************************************************/ + +Saver::Saver() : pImpl(new Impl(this)) +{ +} + + +Saver::~Saver() +{ + delete(pImpl); +} + + +Result Saver::save(std::unique_ptr paint, const std::string& path) noexcept +{ + if (!paint || path.empty()) return Result::InvalidArguments; + + auto saver = unique_ptr(new Saver()); + if (!saver) return Result::FailedAllocation; + + auto p = paint.release(); + if (!p) return Result::MemoryCorruption; + + if (saver->pImpl->save(p, path)) { + delete p; + return Result::Success; + } + + delete p; + return Result::Unknown; +} diff --git a/src/lib/tvgSaverImpl.h b/src/lib/tvgSaverImpl.h new file mode 100644 index 0000000..8013238 --- /dev/null +++ b/src/lib/tvgSaverImpl.h @@ -0,0 +1,494 @@ +/* + * Copyright (c) 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_SAVER_IMPL_H_ +#define _TVG_SAVER_IMPL_H_ + +#include "tvgPaint.h" +#include "tvgBinaryDesc.h" +#include +#include +#include + +struct Saver::Impl +{ + Saver* saver; + char* buffer = nullptr; + char* pointer = nullptr; + uint32_t size = 0; + uint32_t reserved = 0; + + + Impl(Saver* s) : saver(s) + { + } + + + ~Impl() + { + clearBuffer(); + } + + + bool prepareBuffer() + { + reserved = TVG_BIN_HEADER_SIGNATURE_LENGTH + TVG_BIN_HEADER_VERSION_LENGTH + TVG_BIN_HEADER_DATA_LENGTH; + buffer = static_cast(malloc(reserved)); + if (!buffer) { + reserved = 0; + return false; + } + pointer = buffer; + return true; + } + + + void resizeBuffer(uint32_t newSize) + { + //OPTIMIZE ME: find more optimal alg ? "*2" is not opt when raw/png is used + reserved += 100; + if (newSize > reserved) reserved = newSize + 100; + + auto bufferOld = buffer; + + buffer = static_cast(realloc(buffer, reserved)); + + if (buffer != bufferOld) + pointer = buffer + (pointer - bufferOld); + } + + + void rewindBuffer(ByteCounter bytesNum) + { + if (pointer - bytesNum < buffer) return; + + pointer -= bytesNum; + size -= bytesNum; + } + + + void clearBuffer() + { + if (buffer) free(buffer); + buffer = nullptr; + pointer = nullptr; + size = 0; + reserved = 0; + } + + + bool saveBufferToFile(const std::string& path) + { + ofstream outFile; + outFile.open(path, ios::out | ios::trunc | ios::binary); + if (!outFile.is_open()) return false; + outFile.write(buffer, size); + outFile.close(); + + return true; + } + + + bool writeHeader() + { + const char *tvg = TVG_BIN_HEADER_SIGNATURE; + const char *version = TVG_BIN_HEADER_VERSION; + //TODO - unused header data + uint16_t dataByteCnt = 0; + ByteCounter headerByteCnt = TVG_BIN_HEADER_SIGNATURE_LENGTH + TVG_BIN_HEADER_VERSION_LENGTH + TVG_BIN_HEADER_DATA_LENGTH; + if (size + headerByteCnt > reserved) resizeBuffer(headerByteCnt); + + memcpy(pointer, tvg, TVG_BIN_HEADER_SIGNATURE_LENGTH); + pointer += TVG_BIN_HEADER_SIGNATURE_LENGTH; + memcpy(pointer, version, TVG_BIN_HEADER_VERSION_LENGTH); + pointer += TVG_BIN_HEADER_VERSION_LENGTH; + memcpy(pointer, &dataByteCnt, TVG_BIN_HEADER_DATA_LENGTH); + pointer += TVG_BIN_HEADER_DATA_LENGTH; + + size += headerByteCnt; + return true; + } + + + void writeMemberIndicator(TvgIndicator ind) + { + if (size + TVG_INDICATOR_SIZE > reserved) resizeBuffer(size + TVG_INDICATOR_SIZE); + + memcpy(pointer, &ind, TVG_INDICATOR_SIZE); + pointer += TVG_INDICATOR_SIZE; + size += TVG_INDICATOR_SIZE; + } + + + void writeMemberDataSize(ByteCounter byteCnt) + { + if (size + BYTE_COUNTER_SIZE > reserved) resizeBuffer(size + BYTE_COUNTER_SIZE); + + memcpy(pointer, &byteCnt, BYTE_COUNTER_SIZE); + pointer += BYTE_COUNTER_SIZE; + size += BYTE_COUNTER_SIZE; + } + + + void writeMemberDataSizeAt(ByteCounter byteCnt) + { + memcpy(pointer - byteCnt - BYTE_COUNTER_SIZE, &byteCnt, BYTE_COUNTER_SIZE); + } + + + void skipInBufferMemberDataSize() + { + if (size + BYTE_COUNTER_SIZE > reserved) resizeBuffer(size + BYTE_COUNTER_SIZE); + pointer += BYTE_COUNTER_SIZE; + size += BYTE_COUNTER_SIZE; + } + + + ByteCounter writeMemberData(const void* data, ByteCounter byteCnt) + { + if (size + byteCnt > reserved) resizeBuffer(size + byteCnt); + + memcpy(pointer, data, byteCnt); + pointer += byteCnt; + size += byteCnt; + + return byteCnt; + } + + + ByteCounter writeMember(TvgIndicator ind, ByteCounter byteCnt, const void* data) + { + ByteCounter blockByteCnt = TVG_INDICATOR_SIZE + BYTE_COUNTER_SIZE + byteCnt; + + if (size + blockByteCnt > reserved) resizeBuffer(size + blockByteCnt); + + memcpy(pointer, &ind, TVG_INDICATOR_SIZE); + pointer += TVG_INDICATOR_SIZE; + memcpy(pointer, &byteCnt, BYTE_COUNTER_SIZE); + pointer += BYTE_COUNTER_SIZE; + memcpy(pointer, data, byteCnt); + pointer += byteCnt; + + size += blockByteCnt; + + return blockByteCnt; + } + + + ByteCounter serializePaint(const Paint* paint) + { + ByteCounter paintDataByteCnt = 0; + + uint8_t opacity = paint->opacity(); + if (opacity < 255) { + paintDataByteCnt += writeMember(TVG_PAINT_OPACITY_INDICATOR, sizeof(opacity), &opacity); + } + + Matrix m = const_cast(paint)->transform(); + if (fabs(m.e11 - 1) > FLT_EPSILON || fabs(m.e12) > FLT_EPSILON || fabs(m.e13) > FLT_EPSILON || + fabs(m.e21) > FLT_EPSILON || fabs(m.e22 - 1) > FLT_EPSILON || fabs(m.e23) > FLT_EPSILON || + fabs(m.e31) > FLT_EPSILON || fabs(m.e32) > FLT_EPSILON || fabs(m.e33 - 1) > FLT_EPSILON) { + paintDataByteCnt += writeMember(TVG_PAINT_TRANSFORM_MATRIX_INDICATOR, sizeof(m), &m); + } + + const Paint* cmpTarget = nullptr; + auto cmpMethod = paint->composite(&cmpTarget); + if (cmpMethod != CompositeMethod::None && cmpTarget) { + paintDataByteCnt += serializeComposite(cmpTarget, cmpMethod); + } + + return paintDataByteCnt; + } + + + ByteCounter serializeScene(const Paint* paint) + { + auto scene = static_cast(paint); + if (!scene) return 0; + + ByteCounter sceneDataByteCnt = 0; + + writeMemberIndicator(TVG_SCENE_BEGIN_INDICATOR); + skipInBufferMemberDataSize(); + + sceneDataByteCnt += serializeChildren(paint); + sceneDataByteCnt += serializePaint(scene); + + writeMemberDataSizeAt(sceneDataByteCnt); + + return TVG_INDICATOR_SIZE + BYTE_COUNTER_SIZE + sceneDataByteCnt; + } + + + ByteCounter serializeShapeFill(const Fill* f, TvgIndicator fillTvgFlag) + { + ByteCounter fillDataByteCnt = 0; + const Fill::ColorStop* stops = nullptr; + auto stopsCnt = f->colorStops(&stops); + if (!stops || stopsCnt == 0) return 0; + + writeMemberIndicator(fillTvgFlag); + skipInBufferMemberDataSize(); + + if (f->id() == FILL_ID_RADIAL) { + float argRadial[3]; + auto radGrad = static_cast(f); + if (radGrad->radial(argRadial, argRadial + 1,argRadial + 2) != Result::Success) { + rewindBuffer(TVG_FLAG_SIZE + BYTE_COUNTER_SIZE); + return 0; + } + fillDataByteCnt += writeMember(TVG_FILL_RADIAL_GRADIENT_INDICATOR, sizeof(argRadial), argRadial); + } + else { + float argLinear[4]; + auto linGrad = static_cast(f); + if (linGrad->linear(argLinear, argLinear + 1, argLinear + 2, argLinear + 3) != Result::Success) { + rewindBuffer(TVG_FLAG_SIZE + BYTE_COUNTER_SIZE); + return 0; + } + fillDataByteCnt += writeMember(TVG_FILL_LINEAR_GRADIENT_INDICATOR, sizeof(argLinear), argLinear); + } + + auto flag = static_cast(f->spread()); + fillDataByteCnt += writeMember(TVG_FILL_FILLSPREAD_INDICATOR, TVG_FLAG_SIZE, &flag); + + fillDataByteCnt += writeMember(TVG_FILL_COLORSTOPS_INDICATOR, stopsCnt * sizeof(stops), stops); + + writeMemberDataSizeAt(fillDataByteCnt); + + return TVG_INDICATOR_SIZE + BYTE_COUNTER_SIZE + fillDataByteCnt; + } + + + ByteCounter serializeShapeStroke(const Shape* shape) + { + ByteCounter strokeDataByteCnt = 0; + TvgFlag flag; + + writeMemberIndicator(TVG_SHAPE_STROKE_INDICATOR); + skipInBufferMemberDataSize(); + + flag = static_cast(shape->strokeCap()); + strokeDataByteCnt += writeMember(TVG_SHAPE_STROKE_CAP_INDICATOR, TVG_FLAG_SIZE, &flag); + + flag = static_cast(shape->strokeJoin()); + strokeDataByteCnt += writeMember(TVG_SHAPE_STROKE_JOIN_INDICATOR, TVG_FLAG_SIZE, &flag); + + float width = shape->strokeWidth(); + strokeDataByteCnt += writeMember(TVG_SHAPE_STROKE_WIDTH_INDICATOR, sizeof(width), &width); + + if (auto fill = shape->strokeFill()) { + strokeDataByteCnt += serializeShapeFill(fill, TVG_SHAPE_STROKE_FILL_INDICATOR); + } else { + uint8_t color[4] = {0, 0, 0, 0}; + shape->strokeColor(color, color + 1, color + 2, color + 3); + strokeDataByteCnt += writeMember(TVG_SHAPE_STROKE_COLOR_INDICATOR, sizeof(color), &color); + } + + const float* dashPattern = nullptr; + uint32_t dashCnt = shape->strokeDash(&dashPattern); + if (dashPattern && dashCnt > 0) { + ByteCounter dashCntByteCnt = sizeof(dashCnt); + ByteCounter dashPtrnByteCnt = dashCnt * sizeof(dashPattern[0]); + + writeMemberIndicator(TVG_SHAPE_STROKE_DASHPTRN_INDICATOR); + writeMemberDataSize(dashCntByteCnt + dashPtrnByteCnt); + strokeDataByteCnt += writeMemberData(&dashCnt, dashCntByteCnt); + strokeDataByteCnt += writeMemberData(dashPattern, dashPtrnByteCnt); + strokeDataByteCnt += TVG_INDICATOR_SIZE + BYTE_COUNTER_SIZE; + } + + writeMemberDataSizeAt(strokeDataByteCnt); + + return TVG_INDICATOR_SIZE + BYTE_COUNTER_SIZE + strokeDataByteCnt; + } + + + ByteCounter serializeShapePath(const Shape* shape) + { + const PathCommand* cmds = nullptr; + uint32_t cmdCnt = shape->pathCommands(&cmds); + const Point* pts = nullptr; + uint32_t ptsCnt = shape->pathCoords(&pts); + + if (!cmds || !pts || !cmdCnt || !ptsCnt) return 0; + + ByteCounter pathDataByteCnt = 0; + + writeMemberIndicator(TVG_SHAPE_PATH_INDICATOR); + skipInBufferMemberDataSize(); + + pathDataByteCnt += writeMemberData(&cmdCnt, sizeof(cmdCnt)); + pathDataByteCnt += writeMemberData(&ptsCnt, sizeof(ptsCnt)); + pathDataByteCnt += writeMemberData(cmds, cmdCnt * sizeof(cmds[0])); + pathDataByteCnt += writeMemberData(pts, ptsCnt * sizeof(pts[0])); + + writeMemberDataSizeAt(pathDataByteCnt); + + return TVG_INDICATOR_SIZE + BYTE_COUNTER_SIZE + pathDataByteCnt; + } + + + ByteCounter serializeShape(const Paint* paint) + { + auto shape = static_cast(paint); + if (!shape) return 0; + + ByteCounter shapeDataByteCnt = 0; + + writeMemberIndicator(TVG_SHAPE_BEGIN_INDICATOR); + skipInBufferMemberDataSize(); + + TvgFlag ruleTvgFlag = (shape->fillRule() == FillRule::EvenOdd) ? TVG_SHAPE_FILLRULE_EVENODD_FLAG : TVG_SHAPE_FILLRULE_WINDING_FLAG; + shapeDataByteCnt += writeMember(TVG_SHAPE_FILLRULE_INDICATOR, TVG_FLAG_SIZE, &ruleTvgFlag); + + if (shape->strokeWidth() > 0) { + shapeDataByteCnt += serializeShapeStroke(shape); + } + + if (auto fill = shape->fill()) { + shapeDataByteCnt += serializeShapeFill(fill, TVG_SHAPE_FILL_INDICATOR); + } else { + uint8_t color[4] = {0, 0, 0, 0}; + shape->fillColor(color, color + 1, color + 2, color + 3); + shapeDataByteCnt += writeMember(TVG_SHAPE_COLOR_INDICATOR, sizeof(color), color); + } + + shapeDataByteCnt += serializeShapePath(shape); + + shapeDataByteCnt += serializeChildren(paint); + shapeDataByteCnt += serializePaint(shape); + + writeMemberDataSizeAt(shapeDataByteCnt); + + return TVG_INDICATOR_SIZE + BYTE_COUNTER_SIZE + shapeDataByteCnt; + } + + + ByteCounter serializePicture(const Paint* paint) + { + auto picture = static_cast(paint); + if (!picture) return 0; + auto pixels = picture->data(); + + ByteCounter pictureDataByteCnt = 0; + + writeMemberIndicator(TVG_PICTURE_BEGIN_INDICATOR); + skipInBufferMemberDataSize(); + + if (pixels) { + //TODO - loader expects uints + float vw, vh; + picture->viewbox(nullptr, nullptr, &vw, &vh); + + uint32_t w = static_cast(vw); + uint32_t h = static_cast(vh); + ByteCounter wByteCnt = sizeof(w); // same as h size + ByteCounter pixelsByteCnt = w * h * sizeof(pixels[0]); + + writeMemberIndicator(TVG_RAW_IMAGE_BEGIN_INDICATOR); + writeMemberDataSize(2 * wByteCnt + pixelsByteCnt); + pictureDataByteCnt += writeMemberData(&w, wByteCnt); + pictureDataByteCnt += writeMemberData(&h, wByteCnt); + pictureDataByteCnt += writeMemberData(pixels, pixelsByteCnt); + pictureDataByteCnt += TVG_INDICATOR_SIZE + BYTE_COUNTER_SIZE; + } else { + pictureDataByteCnt += serializeChildren(paint); + } + + pictureDataByteCnt += serializePaint(picture); + + writeMemberDataSizeAt(pictureDataByteCnt); + + return TVG_INDICATOR_SIZE + BYTE_COUNTER_SIZE + pictureDataByteCnt; + } + + + ByteCounter serializeComposite(const Paint* cmpTarget, CompositeMethod cmpMethod) + { + ByteCounter cmpDataByteCnt = 0; + + writeMemberIndicator(TVG_PAINT_CMP_TARGET_INDICATOR); + skipInBufferMemberDataSize(); + + auto cmpMethodTvgFlag = static_cast(cmpMethod); + cmpDataByteCnt += writeMember(TVG_PAINT_CMP_METHOD_INDICATOR, TVG_FLAG_SIZE, &cmpMethodTvgFlag); + + cmpDataByteCnt += serialize(cmpTarget); + + writeMemberDataSizeAt(cmpDataByteCnt); + + return TVG_INDICATOR_SIZE + BYTE_COUNTER_SIZE + cmpDataByteCnt; + } + + + ByteCounter serializeChildren(const Paint* paint) + { + if (!paint) return 0; + ByteCounter dataByteCnt = 0; + + for (auto it = paint->begin(); it != paint->end(); ++it) { + dataByteCnt += serialize(&(*it)); + } + + return dataByteCnt; + } + + + ByteCounter serialize(const Paint* paint) + { + if (!paint) return 0; + ByteCounter dataByteCnt = 0; + + switch (paint->id()) { + case PAINT_ID_SHAPE: { + dataByteCnt += serializeShape(paint); + break; + } + case PAINT_ID_SCENE: { + dataByteCnt += serializeScene(paint); + break; + } + case PAINT_ID_PICTURE: { + dataByteCnt += serializePicture(paint); + break; + } + } + + return dataByteCnt; + } + + + bool save(const Paint* paint, const std::string& path) + { + if (!prepareBuffer()) return false; + if (!writeHeader()) return false; + + if (serialize(paint) == 0) return false; + + if (!saveBufferToFile(path)) return false; + clearBuffer(); + + return true; + } +}; + +#endif //_TVG_SAVER_IMPL_H_ -- 2.7.4