Rebased ref, commits from common ancestor: commit 32c630f29ce6e0619de66cfbde3a8f24910f6269 Author: Michael Meeks <michael.me...@collabora.com> AuthorDate: Mon Oct 23 20:59:10 2017 +0100 Commit: Michael Meeks <michael.me...@collabora.com> CommitDate: Fri Oct 27 18:30:29 2017 +0100
Deltas - collapse multiple rows to a single row. Change-Id: Ia2a617c2adbbc4e66b7c773c2280ec609aead16e diff --git a/kit/Delta.hpp b/kit/Delta.hpp index 82f871bbf..e1890370c 100644 --- a/kit/Delta.hpp +++ b/kit/Delta.hpp @@ -64,6 +64,7 @@ class DeltaGenerator { // How do the rows look against each other ? size_t lastMatchOffset = 0; + size_t lastCopy = 0; for (int y = 0; y < prev._height; ++y) { // Life is good where rows match: @@ -78,12 +79,25 @@ class DeltaGenerator { if (prev._rows[match].identical(cur._rows[y])) { // TODO: if offsets are >256 - use 16bits? + if (lastCopy > 0) + { + char cnt = output[lastCopy]; + if (output[lastCopy + 1] + cnt == (char)(match) && + output[lastCopy + 2] + cnt == (char)(y)) + { + output[lastCopy]++; + matched = true; + continue; + } + } - // hopefully find blocks of this. lastMatchOffset = match - y; - output.push_back('c'); // copy-row + output.push_back('c'); // copy-row + lastCopy = output.size(); + output.push_back(1); // count output.push_back(match); // src - output.push_back(y); // dest + output.push_back(y); // dest + matched = true; continue; } diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js index 23c529a3d..a647eea22 100644 --- a/loleaflet/src/layer/tile/TileLayer.js +++ b/loleaflet/src/layer/tile/TileLayer.js @@ -1306,13 +1306,20 @@ L.TileLayer = L.GridLayer.extend({ // FIXME; can we operate directly on the image ? var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); - var oldData = new Uint8ClampedArray(imgData); + var oldData = new Uint8ClampedArray(imgData.data); + var delta = img; var pixSize = canvas.width * canvas.height * 4; var offset = 0; console.log('Applying a delta of length ' + delta.length + ' pix size: ' + pixSize + '\nhex: ' + hex2string(delta)); + // Green-tinge the old-Data ... +// for (var i = 0; i < pixSize; ++i) +// { +// oldData[i*4 + 1] = 128; +// } + // wipe to grey. // for (var i = 0; i < pixSize * 4; ++i) // { @@ -1325,15 +1332,19 @@ L.TileLayer = L.GridLayer.extend({ switch (delta[i]) { case 99: // 'c': // copy row - var srcRow = delta[i+1]; - var destRow = delta[i+2] - i+= 3; - console.log('copy row ' + srcRow + ' to ' + destRow); - var src = srcRow * canvas.width * 4; - var dest = destRow * canvas.width * 4; - for (var i = 0; i < canvas.width * 4; ++i) + var count = delta[i+1]; + var srcRow = delta[i+2]; + var destRow = delta[i+3]; + i+= 4; + console.log('copy ' + count + ' row(s) ' + srcRow + ' to ' + destRow); + for (var cnt = 0; cnt < count; ++cnt) { - imgData.data[dest + i] = oldData[src + i]; + var src = (srcRow + cnt) * canvas.width * 4; + var dest = (destRow + cnt) * canvas.width * 4; + for (var j = 0; j < canvas.width * 4; ++j) + { + imgData.data[dest + j] = oldData[src + j]; + } } break; case 100: // 'd': // new run @@ -1351,7 +1362,8 @@ L.TileLayer = L.GridLayer.extend({ imgData.data[offset - 2] = 256; // debug - blue terminator break; default: - console.log('Unknown code ' + delta[i]); + console.log('ERROR: Unknown code ' + delta[i] + + ' at offset ' + i); i = delta.length; break; } diff --git a/test/DeltaTests.cpp b/test/DeltaTests.cpp index 705b7d3ea..66868523c 100644 --- a/test/DeltaTests.cpp +++ b/test/DeltaTests.cpp @@ -84,15 +84,19 @@ std::vector<char> DeltaTests::applyDelta( { case 'c': // copy row. { - int srcRow = (uint8_t)(delta[i+1]); - int destRow = (uint8_t)(delta[i+2]); - - std::cout << "copy row " << srcRow << " to " << destRow << "\n"; - const char *src = &pixmap[width * srcRow * 4]; - char *dest = &output[width * destRow * 4]; - for (size_t j = 0; j < width * 4; ++j) - *dest++ = *src++; - i += 3; + int count = (uint8_t)(delta[i+1]); + int srcRow = (uint8_t)(delta[i+2]); + int destRow = (uint8_t)(delta[i+3]); + +// std::cout << "copy " << count <<" row(s) " << srcRow << " to " << destRow << "\n"; + for (int cnt = 0; cnt < count; ++cnt) + { + const char *src = &pixmap[width * (srcRow + cnt) * 4]; + char *dest = &output[width * (destRow + cnt) * 4]; + for (size_t j = 0; j < width * 4; ++j) + *dest++ = *src++; + } + i += 4; break; } case 'd': // new run @@ -102,7 +106,7 @@ std::vector<char> DeltaTests::applyDelta( size_t length = (uint8_t)(delta[i+3]); i += 4; - std::cout << "new " << length << " at " << destCol << ", " << destRow << "\n"; +// std::cout << "new " << length << " at " << destCol << ", " << destRow << "\n"; CPPUNIT_ASSERT(length <= width - destCol); char *dest = &output[width * destRow * 4 + destCol * 4]; commit 6751a49bfc61f0f82d31cf6f1bbfe6821c784c52 Author: Michael Meeks <michael.me...@collabora.com> AuthorDate: Fri Sep 29 18:05:03 2017 +0100 Commit: Michael Meeks <michael.me...@collabora.com> CommitDate: Fri Oct 27 18:30:29 2017 +0100 Convert Javascript to row deltas. Change-Id: I2ec612c2bc047dc36f86c2935178c964f9feae11 diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js index 07ec9aa67..23c529a3d 100644 --- a/loleaflet/src/layer/tile/TileLayer.js +++ b/loleaflet/src/layer/tile/TileLayer.js @@ -1306,6 +1306,7 @@ L.TileLayer = L.GridLayer.extend({ // FIXME; can we operate directly on the image ? var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); + var oldData = new Uint8ClampedArray(imgData); var delta = img; var pixSize = canvas.width * canvas.height * 4; var offset = 0; @@ -1319,38 +1320,43 @@ L.TileLayer = L.GridLayer.extend({ // } // Apply delta. - for (var i = 1; i < delta.length && - offset < pixSize;) + for (var i = 1; i < delta.length;) { -// var span = delta[i]; -// var isChangedRun = span & 64; -// if (span >= 128) -// { - // first char is a control code. - var isChangedRun = delta[i++] & 64; - span = delta[i++]; - span += delta[i++] * 256; -// } - if (isChangedRun) { - console.log('apply new span of size ' + span + ' at offset ' + i + ' into delta at byte: ' + offset); + switch (delta[i]) + { + case 99: // 'c': // copy row + var srcRow = delta[i+1]; + var destRow = delta[i+2] + i+= 3; + console.log('copy row ' + srcRow + ' to ' + destRow); + var src = srcRow * canvas.width * 4; + var dest = destRow * canvas.width * 4; + for (var i = 0; i < canvas.width * 4; ++i) + { + imgData.data[dest + i] = oldData[src + i]; + } + break; + case 100: // 'd': // new run + var destRow = delta[i+1]; + var destCol = delta[i+2]; + var span = delta[i+3]; + var offset = destRow * canvas.width * 4 + destCol * 4; + i += 4; + console.log('apply new span of size ' + span + ' at pos ' + destCol + ', ' + destRow + ' into delta at byte: ' + offset); span *= 4; + imgData.data[offset + 1] = 256; // debug - greener start while (span-- > 0) { imgData.data[offset++] = delta[i++]; } imgData.data[offset - 2] = 256; // debug - blue terminator - } else { - console.log('apply unchanged span of size ' + span + ' at offset ' + i + ' into delta at byte: ' + offset); - offset += span * 4; - imgData.data[offset - 3] = 256; // debug - green terminator + break; + default: + console.log('Unknown code ' + delta[i]); + i = delta.length; + break; } } - while (offset < pixSize) // debug - { - imgData.data[offset] = 256; // redden the remaining section. - imgData.data[offset+3] = 256; // with some alpha - offset += 4; - } ctx.putImageData(imgData, 0, 0); tile.oldWireId = tile.wireId; commit 9c44563c0d4cf7052c22f43b542e56319dc61fa6 Author: Michael Meeks <michael.me...@collabora.com> AuthorDate: Thu Sep 28 09:45:46 2017 +0100 Commit: Michael Meeks <michael.me...@collabora.com> CommitDate: Fri Oct 27 18:30:29 2017 +0100 Make delta-builder row-based. Change-Id: Ic59324535c4f412abc4e83774073eb8f57290704 diff --git a/kit/Delta.hpp b/kit/Delta.hpp index 95f6a1f18..82f871bbf 100644 --- a/kit/Delta.hpp +++ b/kit/Delta.hpp @@ -21,106 +21,149 @@ /// A quick and dirty delta generator for last tile changes class DeltaGenerator { + struct DeltaBitmapRow { + uint64_t _crc; + std::vector<uint32_t> _pixels; + + bool identical(const DeltaBitmapRow &other) const + { + if (_crc != other._crc) + return false; + return _pixels == other._pixels; + } + }; + struct DeltaData { TileWireId _wid; - std::shared_ptr<std::vector<uint32_t>> _rawData; + int _width; + int _height; + std::vector<DeltaBitmapRow> _rows; }; - std::vector<DeltaData> _deltaEntries; + std::vector<std::shared_ptr<DeltaData>> _deltaEntries; bool makeDelta( - const DeltaData &prevData, - const DeltaData &curData, + const DeltaData &prev, + const DeltaData &cur, std::vector<char>& output) { - std::vector<uint32_t> &prev = *prevData._rawData.get(); - std::vector<uint32_t> &cur = *curData._rawData.get(); - - // FIXME: should we split and compress alpha separately ? - - if (prev.size() != cur.size()) + // TODO: should we split and compress alpha separately ? + if (prev._width != cur._width || prev._height != cur._height) { - LOG_ERR("mis-sized delta: " << prev.size() << " vs " << cur.size() << "bytes"); + LOG_ERR("mis-sized delta: " << prev._width << "x" << prev._height << " vs " + << cur._width << "x" << cur._height); return false; } output.push_back('D'); - LOG_TRC("building delta of " << prev.size() << "bytes"); - // FIXME: really lame - some RLE might help etc. - for (size_t i = 0; i < prev.size();) + LOG_TRC("building delta of a " << cur._width << "x" << cur._height << " bitmap"); + + // row move/copy src/dest is a byte. + assert (prev._height <= 256); + // column position is a byte. + assert (prev._width <= 256); + + // How do the rows look against each other ? + size_t lastMatchOffset = 0; + for (int y = 0; y < prev._height; ++y) { - int sameCount = 0; - while (i + sameCount < prev.size() && - prev[i+sameCount] == cur[i+sameCount]) - { - ++sameCount; - } - if (sameCount > 0) + // Life is good where rows match: + if (prev._rows[y].identical(cur._rows[y])) + continue; + + // Hunt for other rows + bool matched = false; + for (int yn = 0; yn < prev._height && !matched; ++yn) { -#if 0 - if (sameCount < 64) - output.push_back(sameCount); - else -#endif + size_t match = (y + lastMatchOffset + yn) % prev._height; + if (prev._rows[match].identical(cur._rows[y])) { - output.push_back(0x80 | 0x00); // long-same - output.push_back(sameCount & 0xff); - output.push_back(sameCount >> 8); + // TODO: if offsets are >256 - use 16bits? + + // hopefully find blocks of this. + lastMatchOffset = match - y; + output.push_back('c'); // copy-row + output.push_back(match); // src + output.push_back(y); // dest + matched = true; + continue; } - i += sameCount; - LOG_TRC("identical " << sameCount << "pixels"); } + if (matched) + continue; - int diffCount = 0; - while (i + diffCount < prev.size() && - (prev[i+diffCount] != cur[i+diffCount])) + // Our row is just that different: + const DeltaBitmapRow &curRow = cur._rows[y]; + const DeltaBitmapRow &prevRow = prev._rows[y]; + for (int x = 0; x < prev._width;) { - ++diffCount; - } - - if (diffCount > 0) - { -#if 0 - if (diffCount < 64) - output.push_back(0x40 & diffCount); - else -#endif + int same; + for (same = 0; same + x < prev._width && + prevRow._pixels[x+same] == curRow._pixels[x+same];) + ++same; + + x += same; + + int diff; + for (diff = 0; diff + x < prev._width && + (prevRow._pixels[x+diff] == curRow._pixels[x+diff] || diff < 2) && + diff < 254;) + ++diff; + if (diff > 0) { - output.push_back(0x80 | 0x40); // long-diff - output.push_back(diffCount & 0xff); - output.push_back(diffCount >> 8); - } + output.push_back('d'); + output.push_back(y); + output.push_back(x); + output.push_back(diff); + + size_t dest = output.size(); + output.resize(dest + diff * 4); + memcpy(&output[dest], &curRow._pixels[x], diff * 4); - size_t dest = output.size(); - output.resize(dest + diffCount * 4); - memcpy(&output[dest], &cur[i], diffCount * 4); - LOG_TRC("different " << diffCount << "pixels"); - i += diffCount; + LOG_TRC("different " << diff << "pixels"); + x += diff; + } } } + return true; } - std::shared_ptr<std::vector<uint32_t>> dataToVector( + std::shared_ptr<DeltaData> dataToDeltaData( + TileWireId wid, unsigned char* pixmap, size_t startX, size_t startY, int width, int height, int bufferWidth, int bufferHeight) { + auto data = std::make_shared<DeltaData>(); + data->_wid = wid; + assert (startX + width <= (size_t)bufferWidth); assert (startY + height <= (size_t)bufferHeight); - auto vector = std::make_shared<std::vector<uint32_t>>(); - LOG_TRC("Converting data to vector of size " + LOG_TRC("Converting pixel data to delta data of size " << (width * height * 4) << " width " << width << " height " << height); - vector->resize(width * height); + data->_width = width; + data->_height = height; + data->_rows.resize(height); for (int y = 0; y < height; ++y) { + DeltaBitmapRow &row = data->_rows[y]; size_t position = ((startY + y) * bufferWidth * 4) + (startX * 4); - memcpy(&(*vector)[y * width], pixmap + position, width * 4); + int32_t *src = reinterpret_cast<int32_t *>(pixmap + position); + + // We get the hash ~for free as we copy - with a cheap hash. + uint64_t crc = 0x7fffffff - 1; + row._pixels.resize(width); + for (int x = 0; x < width; ++x) + { + crc = (crc << 7) + crc + src[x]; + row._pixels[x] = src[x]; + } } - return vector; + return data; } public: @@ -143,17 +186,15 @@ class DeltaGenerator { if (_deltaEntries.size() > 6) // FIXME: hard-coded ... _deltaEntries.erase(_deltaEntries.begin()); - // FIXME: assuming width etc. are all constant & so on. - DeltaData update; - update._wid = wid; - update._rawData = dataToVector(pixmap, startX, startY, width, height, - bufferWidth, bufferHeight); + std::shared_ptr<DeltaData> update = + dataToDeltaData(wid, pixmap, startX, startY, width, height, + bufferWidth, bufferHeight); _deltaEntries.push_back(update); for (auto &old : _deltaEntries) { - if (oldWid == old._wid) - return makeDelta(old, update, output); + if (oldWid == old->_wid) + return makeDelta(*old, *update, output); } return false; } diff --git a/test/DeltaTests.cpp b/test/DeltaTests.cpp index 3532931e1..705b7d3ea 100644 --- a/test/DeltaTests.cpp +++ b/test/DeltaTests.cpp @@ -57,61 +57,85 @@ class DeltaTests : public CPPUNIT_NS::TestFixture const std::vector<char> &delta); void assertEqual(const std::vector<char> &a, - const std::vector<char> &b); + const std::vector<char> &b, + int width, int height); }; // Quick hack for debugging std::vector<char> DeltaTests::applyDelta( const std::vector<char> &pixmap, - png_uint_32 /* width */, png_uint_32 /* height */, + png_uint_32 width, png_uint_32 height, const std::vector<char> &delta) { CPPUNIT_ASSERT(delta.size() >= 4); CPPUNIT_ASSERT(delta[0] == 'D'); -// std::cout << "apply delta of size " << delta.size() << "\n"; + std::cout << "apply delta of size " << delta.size() << "\n"; + // start with the same state. std::vector<char> output = pixmap; - CPPUNIT_ASSERT_EQUAL(output.size(), pixmap.size()); - size_t offset = 0; - for (size_t i = 1; i < delta.size() && - offset < output.size();) + CPPUNIT_ASSERT_EQUAL(output.size(), size_t(pixmap.size())); + CPPUNIT_ASSERT_EQUAL(output.size(), size_t(width * height * 4)); + + size_t offset = 0, i; + for (i = 1; i < delta.size() && offset < output.size();) { - bool isChangedRun = delta[i++] & 64; - CPPUNIT_ASSERT(i < delta.size()); - uint32_t span = (unsigned char)delta[i++]; - CPPUNIT_ASSERT(i < delta.size()); - span += ((unsigned char)delta[i++])*256; - CPPUNIT_ASSERT(i < delta.size() || - (i == delta.size() && !isChangedRun)); - span *= 4; -// std::cout << "span " << span << " offset " << offset << "\n"; - if (isChangedRun) + switch (delta[i]) + { + case 'c': // copy row. + { + int srcRow = (uint8_t)(delta[i+1]); + int destRow = (uint8_t)(delta[i+2]); + + std::cout << "copy row " << srcRow << " to " << destRow << "\n"; + const char *src = &pixmap[width * srcRow * 4]; + char *dest = &output[width * destRow * 4]; + for (size_t j = 0; j < width * 4; ++j) + *dest++ = *src++; + i += 3; + break; + } + case 'd': // new run { - CPPUNIT_ASSERT(offset + span <= output.size()); - memcpy(&output[offset], &delta[i], span); - i += span; + int destRow = (uint8_t)(delta[i+1]); + int destCol = (uint8_t)(delta[i+2]); + size_t length = (uint8_t)(delta[i+3]); + i += 4; + + std::cout << "new " << length << " at " << destCol << ", " << destRow << "\n"; + CPPUNIT_ASSERT(length <= width - destCol); + + char *dest = &output[width * destRow * 4 + destCol * 4]; + for (size_t j = 0; j < length * 4 && i < delta.size(); ++j) + *dest++ = delta[i++]; + break; + } + default: + std::cout << "Unknown delta code " << delta[i] << "\n"; + CPPUNIT_ASSERT(false); + break; } - offset += span; } - CPPUNIT_ASSERT_EQUAL(pixmap.size(), output.size()); - CPPUNIT_ASSERT_EQUAL(output.size(), offset); + CPPUNIT_ASSERT_EQUAL(delta.size(), i); return output; } void DeltaTests::assertEqual(const std::vector<char> &a, - const std::vector<char> &b) + const std::vector<char> &b, + int width, int /* height */) { CPPUNIT_ASSERT_EQUAL(a.size(), b.size()); for (size_t i = 0; i < a.size(); ++i) { if (a[i] != b[i]) { - std::cout << "Differences starting at byte " << i; + std::cout << "Differences starting at byte " << i << " " + << (i/4 % width) << ", " << (i / (width * 4)) << ":\n"; size_t len; - for (len = 0; a[i+len] != b[i+len] && i + len < a.size(); ++len) + for (len = 0; (a[i+len] != b[i+len] || len < 8) && i + len < a.size(); ++len) { - std::cout << std::hex << (int)((unsigned char)a[i+len]) << " "; + std::cout << std::hex << (int)((unsigned char)a[i+len]) << " != "; + std::cout << std::hex << (int)((unsigned char)b[i+len]) << " "; if (len > 0 && (len % 16 == 0)) std::cout<< "\n"; } @@ -157,7 +181,7 @@ void DeltaTests::testDeltaSequence() // Apply it to move to the second frame std::vector<char> reText2 = applyDelta(text, width, height, delta); - assertEqual(reText2, text2); + assertEqual(reText2, text2, width, height); // Build a delta between text & text2Wid std::vector<char> two2one; @@ -169,7 +193,7 @@ void DeltaTests::testDeltaSequence() // Apply it to get back to where we started std::vector<char> reText = applyDelta(text2, width, height, two2one); - assertEqual(reText, text); + assertEqual(reText, text, width, height); } void DeltaTests::testRandomDeltas() commit 07cf8b2e4a93964cd72bb14c119f6e9f6dd6cdf2 Author: Michael Meeks <michael.me...@collabora.com> AuthorDate: Sat Sep 16 17:32:20 2017 +0100 Commit: Michael Meeks <michael.me...@collabora.com> CommitDate: Fri Oct 27 18:30:29 2017 +0100 Start of Delta unit-tests. Change-Id: I1a25f5347c0d7430000146bb585a041d363bcf37 diff --git a/kit/Delta.hpp b/kit/Delta.hpp index b2b6a13e9..95f6a1f18 100644 --- a/kit/Delta.hpp +++ b/kit/Delta.hpp @@ -10,8 +10,10 @@ #define INCLUDED_DELTA_HPP #include <vector> +#include <assert.h> +#include <Log.hpp> -#ifdef TILE_WIRE_ID +#ifndef TILE_WIRE_ID # define TILE_WIRE_ID typedef uint32_t TileWireId; #endif @@ -125,7 +127,7 @@ class DeltaGenerator { DeltaGenerator() {} /** - * Creates a delta if possible: + * Creates a delta between @oldWid and pixmap if possible: * if so - returns @true and appends the delta to @output * stores @pixmap, and other data to accelerate delta * creation in a limited size cache. diff --git a/test/DeltaTests.cpp b/test/DeltaTests.cpp new file mode 100644 index 000000000..3532931e1 --- /dev/null +++ b/test/DeltaTests.cpp @@ -0,0 +1,181 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include "config.h" + +#include <cppunit/extensions/HelperMacros.h> + +#include "Delta.hpp" +#include "Util.hpp" +#include "Png.hpp" +#include "helpers.hpp" + +/// Delta unit-tests. +class DeltaTests : public CPPUNIT_NS::TestFixture +{ + CPPUNIT_TEST_SUITE(DeltaTests); + + CPPUNIT_TEST(testDeltaSequence); + CPPUNIT_TEST(testRandomDeltas); + + CPPUNIT_TEST_SUITE_END(); + + void testDeltaSequence(); + void testRandomDeltas(); + + std::vector<char> loadPng(const char *relpath, + png_uint_32& height, + png_uint_32& width, + png_uint_32& rowBytes) + { + std::ifstream file(relpath); + std::stringstream buffer; + buffer << file.rdbuf(); + file.close(); + std::vector<png_bytep> rows = + Png::decodePNG(buffer, height, width, rowBytes); + std::vector<char> output; + for (png_uint_32 y = 0; y < height; ++y) + { + for (png_uint_32 i = 0; i < width * 4; ++i) + { + output.push_back(rows[y][i]); + } + } + return output; + } + + std::vector<char> applyDelta( + const std::vector<char> &pixmap, + png_uint_32 width, png_uint_32 height, + const std::vector<char> &delta); + + void assertEqual(const std::vector<char> &a, + const std::vector<char> &b); +}; + +// Quick hack for debugging +std::vector<char> DeltaTests::applyDelta( + const std::vector<char> &pixmap, + png_uint_32 /* width */, png_uint_32 /* height */, + const std::vector<char> &delta) +{ + CPPUNIT_ASSERT(delta.size() >= 4); + CPPUNIT_ASSERT(delta[0] == 'D'); + +// std::cout << "apply delta of size " << delta.size() << "\n"; + + std::vector<char> output = pixmap; + CPPUNIT_ASSERT_EQUAL(output.size(), pixmap.size()); + size_t offset = 0; + for (size_t i = 1; i < delta.size() && + offset < output.size();) + { + bool isChangedRun = delta[i++] & 64; + CPPUNIT_ASSERT(i < delta.size()); + uint32_t span = (unsigned char)delta[i++]; + CPPUNIT_ASSERT(i < delta.size()); + span += ((unsigned char)delta[i++])*256; + CPPUNIT_ASSERT(i < delta.size() || + (i == delta.size() && !isChangedRun)); + span *= 4; +// std::cout << "span " << span << " offset " << offset << "\n"; + if (isChangedRun) + { + CPPUNIT_ASSERT(offset + span <= output.size()); + memcpy(&output[offset], &delta[i], span); + i += span; + } + offset += span; + } + CPPUNIT_ASSERT_EQUAL(pixmap.size(), output.size()); + CPPUNIT_ASSERT_EQUAL(output.size(), offset); + return output; +} + +void DeltaTests::assertEqual(const std::vector<char> &a, + const std::vector<char> &b) +{ + CPPUNIT_ASSERT_EQUAL(a.size(), b.size()); + for (size_t i = 0; i < a.size(); ++i) + { + if (a[i] != b[i]) + { + std::cout << "Differences starting at byte " << i; + size_t len; + for (len = 0; a[i+len] != b[i+len] && i + len < a.size(); ++len) + { + std::cout << std::hex << (int)((unsigned char)a[i+len]) << " "; + if (len > 0 && (len % 16 == 0)) + std::cout<< "\n"; + } + std::cout << " size " << len << "\n"; + CPPUNIT_ASSERT(false); + } + } +} + +void DeltaTests::testDeltaSequence() +{ + DeltaGenerator gen; + + png_uint_32 height, width, rowBytes; + const TileWireId textWid = 1; + std::vector<char> text = + DeltaTests::loadPng("data/delta-text.png", + height, width, rowBytes); + CPPUNIT_ASSERT(height == 256 && width == 256 && rowBytes == 256*4); + CPPUNIT_ASSERT_EQUAL(size_t(256 * 256 * 4), text.size()); + + const TileWireId text2Wid = 2; + std::vector<char> text2 = + DeltaTests::loadPng("data/delta-text2.png", + height, width, rowBytes); + CPPUNIT_ASSERT(height == 256 && width == 256 && rowBytes == 256*4); + CPPUNIT_ASSERT_EQUAL(size_t(256 * 256 * 4), text2.size()); + + std::vector<char> delta; + // Stash it in the cache + CPPUNIT_ASSERT(gen.createDelta( + reinterpret_cast<unsigned char *>(&text[0]), + 0, 0, width, height, width, height, + delta, textWid, 0) == false); + CPPUNIT_ASSERT(delta.size() == 0); + + // Build a delta between text2 & textWid + CPPUNIT_ASSERT(gen.createDelta( + reinterpret_cast<unsigned char *>(&text2[0]), + 0, 0, width, height, width, height, + delta, text2Wid, textWid) == true); + CPPUNIT_ASSERT(delta.size() > 0); + + // Apply it to move to the second frame + std::vector<char> reText2 = applyDelta(text, width, height, delta); + assertEqual(reText2, text2); + + // Build a delta between text & text2Wid + std::vector<char> two2one; + CPPUNIT_ASSERT(gen.createDelta( + reinterpret_cast<unsigned char *>(&text[0]), + 0, 0, width, height, width, height, + two2one, textWid, text2Wid) == true); + CPPUNIT_ASSERT(two2one.size() > 0); + + // Apply it to get back to where we started + std::vector<char> reText = applyDelta(text2, width, height, two2one); + assertEqual(reText, text); +} + +void DeltaTests::testRandomDeltas() +{ +} + +CPPUNIT_TEST_SUITE_REGISTRATION(DeltaTests); + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/test/Makefile.am b/test/Makefile.am index aa54e0cb4..98426f72e 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -47,6 +47,7 @@ wsd_sources = \ test_base_source = \ TileQueueTests.cpp \ WhiteBoxTests.cpp \ + DeltaTests.cpp \ $(wsd_sources) test_all_source = \ diff --git a/test/data/delta-text.png b/test/data/delta-text.png new file mode 100644 index 000000000..3d48d9bf2 Binary files /dev/null and b/test/data/delta-text.png differ diff --git a/test/data/delta-text2.png b/test/data/delta-text2.png new file mode 100644 index 000000000..d05b897ca Binary files /dev/null and b/test/data/delta-text2.png differ commit 85183d2a029038d0fcdc016309d09e7cb00145f5 Author: Michael Meeks <michael.me...@collabora.com> AuthorDate: Sat Sep 16 17:27:31 2017 +0100 Commit: Michael Meeks <michael.me...@collabora.com> CommitDate: Fri Oct 27 18:30:29 2017 +0100 Move the Delta generator out into its own file. Change-Id: I7f7553c292970b1a52879b6d6c14e67172022310 diff --git a/kit/Delta.hpp b/kit/Delta.hpp new file mode 100644 index 000000000..b2b6a13e9 --- /dev/null +++ b/kit/Delta.hpp @@ -0,0 +1,162 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +#ifndef INCLUDED_DELTA_HPP +#define INCLUDED_DELTA_HPP + +#include <vector> + +#ifdef TILE_WIRE_ID +# define TILE_WIRE_ID + typedef uint32_t TileWireId; +#endif + +/// A quick and dirty delta generator for last tile changes +class DeltaGenerator { + + struct DeltaData { + TileWireId _wid; + std::shared_ptr<std::vector<uint32_t>> _rawData; + }; + std::vector<DeltaData> _deltaEntries; + + bool makeDelta( + const DeltaData &prevData, + const DeltaData &curData, + std::vector<char>& output) + { + std::vector<uint32_t> &prev = *prevData._rawData.get(); + std::vector<uint32_t> &cur = *curData._rawData.get(); + + // FIXME: should we split and compress alpha separately ? + + if (prev.size() != cur.size()) + { + LOG_ERR("mis-sized delta: " << prev.size() << " vs " << cur.size() << "bytes"); + return false; + } + + output.push_back('D'); + LOG_TRC("building delta of " << prev.size() << "bytes"); + // FIXME: really lame - some RLE might help etc. + for (size_t i = 0; i < prev.size();) + { + int sameCount = 0; + while (i + sameCount < prev.size() && + prev[i+sameCount] == cur[i+sameCount]) + { + ++sameCount; + } + if (sameCount > 0) + { +#if 0 + if (sameCount < 64) + output.push_back(sameCount); + else +#endif + { + output.push_back(0x80 | 0x00); // long-same + output.push_back(sameCount & 0xff); + output.push_back(sameCount >> 8); + } + i += sameCount; + LOG_TRC("identical " << sameCount << "pixels"); + } + + int diffCount = 0; + while (i + diffCount < prev.size() && + (prev[i+diffCount] != cur[i+diffCount])) + { + ++diffCount; + } + + if (diffCount > 0) + { +#if 0 + if (diffCount < 64) + output.push_back(0x40 & diffCount); + else +#endif + { + output.push_back(0x80 | 0x40); // long-diff + output.push_back(diffCount & 0xff); + output.push_back(diffCount >> 8); + } + + size_t dest = output.size(); + output.resize(dest + diffCount * 4); + memcpy(&output[dest], &cur[i], diffCount * 4); + LOG_TRC("different " << diffCount << "pixels"); + i += diffCount; + } + } + return true; + } + + std::shared_ptr<std::vector<uint32_t>> dataToVector( + unsigned char* pixmap, size_t startX, size_t startY, + int width, int height, + int bufferWidth, int bufferHeight) + { + assert (startX + width <= (size_t)bufferWidth); + assert (startY + height <= (size_t)bufferHeight); + + auto vector = std::make_shared<std::vector<uint32_t>>(); + LOG_TRC("Converting data to vector of size " + << (width * height * 4) << " width " << width + << " height " << height); + + vector->resize(width * height); + for (int y = 0; y < height; ++y) + { + size_t position = ((startY + y) * bufferWidth * 4) + (startX * 4); + memcpy(&(*vector)[y * width], pixmap + position, width * 4); + } + + return vector; + } + + public: + DeltaGenerator() {} + + /** + * Creates a delta if possible: + * if so - returns @true and appends the delta to @output + * stores @pixmap, and other data to accelerate delta + * creation in a limited size cache. + */ + bool createDelta( + unsigned char* pixmap, size_t startX, size_t startY, + int width, int height, + int bufferWidth, int bufferHeight, + std::vector<char>& output, + TileWireId wid, TileWireId oldWid) + { + // First store a copy for later: + if (_deltaEntries.size() > 6) // FIXME: hard-coded ... + _deltaEntries.erase(_deltaEntries.begin()); + + // FIXME: assuming width etc. are all constant & so on. + DeltaData update; + update._wid = wid; + update._rawData = dataToVector(pixmap, startX, startY, width, height, + bufferWidth, bufferHeight); + _deltaEntries.push_back(update); + + for (auto &old : _deltaEntries) + { + if (oldWid == old._wid) + return makeDelta(old, update, output); + } + return false; + } +}; + +#endif + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/kit/Kit.cpp b/kit/Kit.cpp index 92ebf3c1d..cbd6da688 100644 --- a/kit/Kit.cpp +++ b/kit/Kit.cpp @@ -66,6 +66,7 @@ #include "Unit.hpp" #include "UserMessages.hpp" #include "Util.hpp" +#include "Delta.hpp" #include "common/SigUtil.hpp" #include "common/Seccomp.hpp" @@ -285,141 +286,6 @@ namespace #endif } -/// A quick and dirty delta generator for last tile changes -struct DeltaGenerator { - - struct DeltaData { - TileWireId _wid; - std::shared_ptr<std::vector<uint32_t>> _rawData; - }; - std::vector<DeltaData> _deltaEntries; - - bool makeDelta( - const DeltaData &prevData, - const DeltaData &curData, - std::vector<char>& output) - { - std::vector<uint32_t> &prev = *prevData._rawData.get(); - std::vector<uint32_t> &cur = *curData._rawData.get(); - - // FIXME: should we split and compress alpha separately ? - - if (prev.size() != cur.size()) - { - LOG_ERR("mis-sized delta: " << prev.size() << " vs " << cur.size() << "bytes"); - return false; - } - - output.push_back('D'); - LOG_TRC("building delta of " << prev.size() << "bytes"); - // FIXME: really lame - some RLE might help etc. - for (size_t i = 0; i < prev.size();) - { - int sameCount = 0; - while (i + sameCount < prev.size() && - prev[i+sameCount] == cur[i+sameCount]) - { - ++sameCount; - } - if (sameCount > 0) - { -#if 0 - if (sameCount < 64) - output.push_back(sameCount); - else -#endif - { - output.push_back(0x80 | 0x00); // long-same - output.push_back(sameCount & 0xff); - output.push_back(sameCount >> 8); - } - i += sameCount; - LOG_TRC("identical " << sameCount << "pixels"); - } - - int diffCount = 0; - while (i + diffCount < prev.size() && - (prev[i+diffCount] != cur[i+diffCount])) - { - ++diffCount; - } - - if (diffCount > 0) - { -#if 0 - if (diffCount < 64) - output.push_back(0x40 & diffCount); - else -#endif - { - output.push_back(0x80 | 0x40); // long-diff - output.push_back(diffCount & 0xff); - output.push_back(diffCount >> 8); - } - - size_t dest = output.size(); - output.resize(dest + diffCount * 4); - memcpy(&output[dest], &cur[i], diffCount * 4); - LOG_TRC("different " << diffCount << "pixels"); - i += diffCount; - } - } - return true; - } - - std::shared_ptr<std::vector<uint32_t>> dataToVector( - unsigned char* pixmap, size_t startX, size_t startY, - int width, int height, - int bufferWidth, int bufferHeight) - { - assert (startX + width <= (size_t)bufferWidth); - assert (startY + height <= (size_t)bufferHeight); - - auto vector = std::make_shared<std::vector<uint32_t>>(); - LOG_TRC("Converting data to vector of size " - << (width * height * 4) << " width " << width - << " height " << height); - - vector->resize(width * height); - for (int y = 0; y < height; ++y) - { - size_t position = ((startY + y) * bufferWidth * 4) + (startX * 4); - memcpy(&(*vector)[y * width], pixmap + position, width * 4); - } - - return vector; - } - - // Creates a bespoke delta file-format ... - bool createDelta( - unsigned char* pixmap, size_t startX, size_t startY, - int width, int height, - int bufferWidth, int bufferHeight, - std::vector<char>& output, - TileWireId wid, TileWireId oldWid) - { - // First store a copy for later: - if (_deltaEntries.size() > 6) // FIXME: hard-coded ... - _deltaEntries.erase(_deltaEntries.begin()); - - // FIXME: assuming width etc. are all constant & so on. - DeltaData update; - update._wid = wid; - update._rawData = dataToVector(pixmap, startX, startY, width, height, - bufferWidth, bufferHeight); - _deltaEntries.push_back(update); - - for (auto &old : _deltaEntries) - { - if (oldWid == old._wid) - return makeDelta(old, update, output); - } - return false; - } -}; - - - /// A quick & dirty cache of the last few PNGs /// and their hashes to avoid re-compression /// wherever possible. diff --git a/wsd/TileDesc.hpp b/wsd/TileDesc.hpp index a4c61636f..7e738f64c 100644 --- a/wsd/TileDesc.hpp +++ b/wsd/TileDesc.hpp @@ -20,6 +20,7 @@ #include "Exceptions.hpp" #include "Protocol.hpp" +#define TILE_WIRE_ID typedef uint32_t TileWireId; typedef uint64_t TileBinaryHash; commit 7c1f99b1ee08e4c244712d1e9571198b2d9fe5e0 Author: Michael Meeks <michael.me...@collabora.com> AuthorDate: Tue Sep 12 09:32:30 2017 +0100 Commit: Michael Meeks <michael.me...@collabora.com> CommitDate: Fri Oct 27 18:30:29 2017 +0100 Insert pixels from 'new' not 'old'. Change-Id: I117348885073b740ed8b2df84d805854b2f00767 diff --git a/kit/Kit.cpp b/kit/Kit.cpp index 65b7271e9..92ebf3c1d 100644 --- a/kit/Kit.cpp +++ b/kit/Kit.cpp @@ -357,7 +357,9 @@ struct DeltaGenerator { output.push_back(diffCount >> 8); } - output.insert(output.end(), (char *)&cur[i], (char *)&cur[i+diffCount]); + size_t dest = output.size(); + output.resize(dest + diffCount * 4); + memcpy(&output[dest], &cur[i], diffCount * 4); LOG_TRC("different " << diffCount << "pixels"); i += diffCount; } @@ -401,16 +403,16 @@ struct DeltaGenerator { _deltaEntries.erase(_deltaEntries.begin()); // FIXME: assuming width etc. are all constant & so on. - DeltaData reference; - reference._wid = wid; - reference._rawData = dataToVector(pixmap, startX, startY, width, height, - bufferWidth, bufferHeight); - _deltaEntries.push_back(reference); + DeltaData update; + update._wid = wid; + update._rawData = dataToVector(pixmap, startX, startY, width, height, + bufferWidth, bufferHeight); + _deltaEntries.push_back(update); - for (auto &it : _deltaEntries) + for (auto &old : _deltaEntries) { - if (oldWid == it._wid) - return makeDelta(reference, it, output); + if (oldWid == old._wid) + return makeDelta(old, update, output); } return false; } commit b065ab1348fc3e8edfb3762825d9c77daa3781d1 Author: Michael Meeks <michael.me...@collabora.com> AuthorDate: Wed Aug 23 17:01:18 2017 +0100 Commit: Michael Meeks <michael.me...@collabora.com> CommitDate: Fri Oct 27 18:30:29 2017 +0100 Deltas should be pixel based, add debugging. Change-Id: I3b47b738ee71d015911e3d77b59b5f3cb34ecd75 diff --git a/kit/Kit.cpp b/kit/Kit.cpp index 3ae69853a..65b7271e9 100644 --- a/kit/Kit.cpp +++ b/kit/Kit.cpp @@ -290,7 +290,7 @@ struct DeltaGenerator { struct DeltaData { TileWireId _wid; - std::shared_ptr<std::vector<char>> _rawData; + std::shared_ptr<std::vector<uint32_t>> _rawData; }; std::vector<DeltaData> _deltaEntries; @@ -299,8 +299,10 @@ struct DeltaGenerator { const DeltaData &curData, std::vector<char>& output) { - std::vector<char> &prev = *prevData._rawData.get(); - std::vector<char> &cur = *curData._rawData.get(); + std::vector<uint32_t> &prev = *prevData._rawData.get(); + std::vector<uint32_t> &cur = *curData._rawData.get(); + + // FIXME: should we split and compress alpha separately ? if (prev.size() != cur.size()) { @@ -311,43 +313,59 @@ struct DeltaGenerator { output.push_back('D'); LOG_TRC("building delta of " << prev.size() << "bytes"); // FIXME: really lame - some RLE might help etc. - for (size_t i = 0; i < prev.size(); ++i) + for (size_t i = 0; i < prev.size();) { int sameCount = 0; while (i + sameCount < prev.size() && prev[i+sameCount] == cur[i+sameCount]) { - if (sameCount >= 127) - break; ++sameCount; } if (sameCount > 0) - output.push_back(sameCount); - i += sameCount; - LOG_TRC("identical " << sameCount << "bytes"); + { +#if 0 + if (sameCount < 64) + output.push_back(sameCount); + else +#endif + { + output.push_back(0x80 | 0x00); // long-same + output.push_back(sameCount & 0xff); + output.push_back(sameCount >> 8); + } + i += sameCount; + LOG_TRC("identical " << sameCount << "pixels"); + } int diffCount = 0; while (i + diffCount < prev.size() && - (prev[i+diffCount] != cur[i+diffCount] || diffCount < 3)) + (prev[i+diffCount] != cur[i+diffCount])) { - if (diffCount >= 127) - break; ++diffCount; } if (diffCount > 0) { - output.push_back(diffCount | 0x80); - for (int j = 0; j < diffCount; ++j) - output.push_back(cur[i+j]); +#if 0 + if (diffCount < 64) + output.push_back(0x40 & diffCount); + else +#endif + { + output.push_back(0x80 | 0x40); // long-diff + output.push_back(diffCount & 0xff); + output.push_back(diffCount >> 8); + } + + output.insert(output.end(), (char *)&cur[i], (char *)&cur[i+diffCount]); + LOG_TRC("different " << diffCount << "pixels"); + i += diffCount; } - LOG_TRC("different " << diffCount << "bytes"); - i += diffCount; } return true; } - std::shared_ptr<std::vector<char>> dataToVector( + std::shared_ptr<std::vector<uint32_t>> dataToVector( unsigned char* pixmap, size_t startX, size_t startY, int width, int height, int bufferWidth, int bufferHeight) @@ -355,16 +373,16 @@ struct DeltaGenerator { assert (startX + width <= (size_t)bufferWidth); assert (startY + height <= (size_t)bufferHeight); - auto vector = std::make_shared<std::vector<char>>(); + auto vector = std::make_shared<std::vector<uint32_t>>(); LOG_TRC("Converting data to vector of size " << (width * height * 4) << " width " << width << " height " << height); - vector->resize(width * height * 4); + vector->resize(width * height); for (int y = 0; y < height; ++y) { size_t position = ((startY + y) * bufferWidth * 4) + (startX * 4); - memcpy(&(*vector)[y * width * 4], pixmap + position, width * 4); + memcpy(&(*vector)[y * width], pixmap + position, width * 4); } return vector; diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js index a33fd6581..07ec9aa67 100644 --- a/loleaflet/src/layer/tile/TileLayer.js +++ b/loleaflet/src/layer/tile/TileLayer.js @@ -13,6 +13,18 @@ if (typeof String.prototype.startsWith !== 'function') { }; } +function hex2string(inData) +{ + hexified = []; + data = new Uint8Array(inData); + for (var i = 0; i < data.length; i++) { + hex = data[i].toString(16); + paddedHex = ('00' + hex).slice(-2); + hexified.push(paddedHex); + } + return hexified.join(''); +} + L.Compatibility = { clipboardGet: function (event) { var text = null; @@ -1283,7 +1295,6 @@ L.TileLayer = L.GridLayer.extend({ } else if (tile && typeof(img) == 'object') { // 'Uint8Array' delta - console.log('hit here with a delta'); var canvas = document.createElement('canvas'); canvas.width = 256; canvas.height = 256; @@ -1298,19 +1309,48 @@ L.TileLayer = L.GridLayer.extend({ var delta = img; var pixSize = canvas.width * canvas.height * 4; var offset = 0; + + console.log('Applying a delta of length ' + delta.length + ' pix size: ' + pixSize + '\nhex: ' + hex2string(delta)); + + // wipe to grey. +// for (var i = 0; i < pixSize * 4; ++i) +// { +// imgData.data[i] = 128; +// } + + // Apply delta. for (var i = 1; i < delta.length && - offset < pixSize; ++i) + offset < pixSize;) { - var span = delta[i] & 127; - if (delta[i] & 128) { // changed run. +// var span = delta[i]; +// var isChangedRun = span & 64; +// if (span >= 128) +// { + // first char is a control code. + var isChangedRun = delta[i++] & 64; + span = delta[i++]; + span += delta[i++] * 256; +// } + if (isChangedRun) { console.log('apply new span of size ' + span + ' at offset ' + i + ' into delta at byte: ' + offset); + span *= 4; while (span-- > 0) { - imgData.data[offset++] = delta[++i]; + imgData.data[offset++] = delta[i++]; } + imgData.data[offset - 2] = 256; // debug - blue terminator } else { - offset += span; + console.log('apply unchanged span of size ' + span + ' at offset ' + i + ' into delta at byte: ' + offset); + offset += span * 4; + imgData.data[offset - 3] = 256; // debug - green terminator } } + + while (offset < pixSize) // debug + { + imgData.data[offset] = 256; // redden the remaining section. + imgData.data[offset+3] = 256; // with some alpha + offset += 4; + } ctx.putImageData(imgData, 0, 0); tile.oldWireId = tile.wireId; commit 766a88da33bee95b3c77db2b4b8d7368ed710a6e Author: Michael Meeks <michael.me...@collabora.com> AuthorDate: Fri Jun 30 17:22:12 2017 +0100 Commit: Michael Meeks <michael.me...@collabora.com> CommitDate: Fri Oct 27 18:30:29 2017 +0100 Start of delta creator. Change-Id: Idf186cda4f11e2418d9ff9f435825832c6b10294 diff --git a/kit/Kit.cpp b/kit/Kit.cpp index 665ab3af4..3ae69853a 100644 --- a/kit/Kit.cpp +++ b/kit/Kit.cpp @@ -285,6 +285,121 @@ namespace #endif } +/// A quick and dirty delta generator for last tile changes +struct DeltaGenerator { + + struct DeltaData { + TileWireId _wid; + std::shared_ptr<std::vector<char>> _rawData; + }; + std::vector<DeltaData> _deltaEntries; + + bool makeDelta( + const DeltaData &prevData, + const DeltaData &curData, + std::vector<char>& output) + { + std::vector<char> &prev = *prevData._rawData.get(); + std::vector<char> &cur = *curData._rawData.get(); + + if (prev.size() != cur.size()) + { + LOG_ERR("mis-sized delta: " << prev.size() << " vs " << cur.size() << "bytes"); + return false; + } + + output.push_back('D'); + LOG_TRC("building delta of " << prev.size() << "bytes"); + // FIXME: really lame - some RLE might help etc. + for (size_t i = 0; i < prev.size(); ++i) + { + int sameCount = 0; + while (i + sameCount < prev.size() && + prev[i+sameCount] == cur[i+sameCount]) + { + if (sameCount >= 127) + break; + ++sameCount; + } + if (sameCount > 0) + output.push_back(sameCount); + i += sameCount; + LOG_TRC("identical " << sameCount << "bytes"); + + int diffCount = 0; + while (i + diffCount < prev.size() && + (prev[i+diffCount] != cur[i+diffCount] || diffCount < 3)) + { + if (diffCount >= 127) + break; + ++diffCount; + } + + if (diffCount > 0) + { + output.push_back(diffCount | 0x80); + for (int j = 0; j < diffCount; ++j) + output.push_back(cur[i+j]); + } + LOG_TRC("different " << diffCount << "bytes"); + i += diffCount; + } + return true; + } + + std::shared_ptr<std::vector<char>> dataToVector( + unsigned char* pixmap, size_t startX, size_t startY, + int width, int height, + int bufferWidth, int bufferHeight) + { + assert (startX + width <= (size_t)bufferWidth); + assert (startY + height <= (size_t)bufferHeight); + + auto vector = std::make_shared<std::vector<char>>(); + LOG_TRC("Converting data to vector of size " + << (width * height * 4) << " width " << width + << " height " << height); + + vector->resize(width * height * 4); + for (int y = 0; y < height; ++y) + { + size_t position = ((startY + y) * bufferWidth * 4) + (startX * 4); + memcpy(&(*vector)[y * width * 4], pixmap + position, width * 4); + } + + return vector; + } + + // Creates a bespoke delta file-format ... + bool createDelta( + unsigned char* pixmap, size_t startX, size_t startY, + int width, int height, + int bufferWidth, int bufferHeight, + std::vector<char>& output, + TileWireId wid, TileWireId oldWid) + { + // First store a copy for later: + if (_deltaEntries.size() > 6) // FIXME: hard-coded ... + _deltaEntries.erase(_deltaEntries.begin()); + + // FIXME: assuming width etc. are all constant & so on. + DeltaData reference; + reference._wid = wid; + reference._rawData = dataToVector(pixmap, startX, startY, width, height, + bufferWidth, bufferHeight); + _deltaEntries.push_back(reference); + + for (auto &it : _deltaEntries) + { + if (oldWid == it._wid) + return makeDelta(reference, it, output); + } + return false; + } +}; + + + /// A quick & dirty cache of the last few PNGs /// and their hashes to avoid re-compression /// wherever possible. @@ -310,6 +425,7 @@ class PngCache size_t _cacheHits; size_t _cacheTests; TileWireId _nextId; + DeltaGenerator _deltaGen; std::map< TileBinaryHash, CacheEntry > _cache; std::map< TileWireId, TileBinaryHash > _wireToHash; @@ -406,9 +522,15 @@ class PngCache int width, int height, int bufferWidth, int bufferHeight, std::vector<char>& output, LibreOfficeKitTileMode mode, - TileBinaryHash hash, TileWireId wid, TileWireId /* oldWid */) + TileBinaryHash hash, TileWireId wid, TileWireId oldWid) { LOG_DBG("PNG cache with hash " << hash << " missed."); + if (_deltaGen.createDelta(pixmap, startX, startY, width, height, + bufferWidth, bufferHeight, + output, wid, oldWid)) + return true; + + LOG_DBG("Encode a new png for this tile."); CacheEntry newEntry(bufferWidth * bufferHeight * 1, wid); if (Png::encodeSubBufferToPNG(pixmap, startX, startY, width, height, bufferWidth, bufferHeight, diff --git a/loleaflet/src/core/Socket.js b/loleaflet/src/core/Socket.js index 1481f4af2..5caf832fe 100644 --- a/loleaflet/src/core/Socket.js +++ b/loleaflet/src/core/Socket.js @@ -583,6 +583,7 @@ L.Socket = L.Class.extend({ else if (!textMsg.startsWith('tile:') && !textMsg.startsWith('renderfont:') && !textMsg.startsWith('dialogpaint:') && !textMsg.startsWith('dialogchildpaint:')) { // log the tile msg separately as we need the tile coordinates L.Log.log(textMsg, L.INCOMING); + if (imgBytes !== undefined) { try { // if it's not a tile, parse the whole message @@ -600,12 +601,21 @@ L.Socket = L.Class.extend({ } else { var data = imgBytes.subarray(index + 1); - // read the tile data - var strBytes = ''; - for (var i = 0; i < data.length; i++) { - strBytes += String.fromCharCode(data[i]); + + if (data.length > 0 && data[0] == 68 /* D */) + { + console.log('Socket: got a delta !'); + var img = data; + } + else + { + // read the tile data + var strBytes = ''; + for (var i = 0; i < data.length; i++) { + strBytes += String.fromCharCode(data[i]); + } + var img = 'data:image/png;base64,' + window.btoa(strBytes); } - var img = 'data:image/png;base64,' + window.btoa(strBytes); } if (textMsg.startsWith('status:')) { diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js index 7acb28fb5..a33fd6581 100644 --- a/loleaflet/src/layer/tile/TileLayer.js +++ b/loleaflet/src/layer/tile/TileLayer.js @@ -1281,6 +1281,44 @@ L.TileLayer = L.GridLayer.extend({ docType: this._docType }); } + else if (tile && typeof(img) == 'object') { + // 'Uint8Array' delta + console.log('hit here with a delta'); + var canvas = document.createElement('canvas'); + canvas.width = 256; + canvas.height = 256; + var ctx = canvas.getContext('2d'); + + oldImg = new Image(); + oldImg.src = tile.el.src; + ctx.drawImage(oldImg, 0, 0); + + // FIXME; can we operate directly on the image ? + var imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); + var delta = img; + var pixSize = canvas.width * canvas.height * 4; + var offset = 0; + for (var i = 1; i < delta.length && + offset < pixSize; ++i) + { + var span = delta[i] & 127; + if (delta[i] & 128) { // changed run. + console.log('apply new span of size ' + span + ' at offset ' + i + ' into delta at byte: ' + offset); + while (span-- > 0) { + imgData.data[offset++] = delta[++i]; + } + } else { + offset += span; + } + } + ctx.putImageData(imgData, 0, 0); + + tile.oldWireId = tile.wireId; + tile.wireId = command.wireId; + tile.el.src = canvas.toDataURL('image/png'); + + console.log('set new image'); + } else if (tile) { if (command.wireId != undefined) { tile.oldWireId = command.wireId; commit 9e88e7f72768cc338a94a039ca1e38e0cc1fba02 Author: Jan Holesovsky <ke...@collabora.com> AuthorDate: Fri Oct 27 09:01:14 2017 +0200 Commit: Jan Holesovsky <ke...@collabora.com> CommitDate: Fri Oct 27 09:04:08 2017 +0200 Add the missing separator in the "Language for paragraph" submenu. Change-Id: Id389ef7539d69c96c0bbd7d80404bd435d54a8b4 Reviewed-on: https://gerrit.libreoffice.org/43932 Reviewed-by: Jan Holesovsky <ke...@collabora.com> Tested-by: Jan Holesovsky <ke...@collabora.com> diff --git a/loleaflet/src/control/Control.Menubar.js b/loleaflet/src/control/Control.Menubar.js index 9be729886..5cdfc69f2 100644 --- a/loleaflet/src/control/Control.Menubar.js +++ b/loleaflet/src/control/Control.Menubar.js @@ -384,7 +384,7 @@ L.Control.Menubar = L.Control.extend({ $menuDefault.append(this._createLangMenuItem(_(e.commandValues[lang]), encodeURIComponent('Default_' + e.commandValues[lang]))); } $menuSelection.append(this._createMenu([{type: 'separator'}])); - $menuParagraph.append(this._createMenu[{type: 'separator'}]); + $menuParagraph.append(this._createMenu([{type: 'separator'}])); $menuDefault.append(this._createMenu([{type: 'separator'}])); $menuSelection.append(this._createLangMenuItem(resetLang, 'Current_RESET_LANGUAGES')); $menuParagraph.append(this._createLangMenuItem(resetLang, 'Paragraph_RESET_LANGUAGES')); commit ea39bb2552f4f6ae0c633ae7ff43e255f15aa290 Author: Jan Holesovsky <ke...@collabora.com> AuthorDate: Fri Oct 27 08:44:44 2017 +0200 Commit: Jan Holesovsky <ke...@collabora.com> CommitDate: Fri Oct 27 08:45:42 2017 +0200 Use just "Language" in the Calc and Impress Tools menu. Change-Id: If5bcaf0e7e3aa8867682afaaee87645b05182143 Reviewed-on: https://gerrit.libreoffice.org/43930 Reviewed-by: Jan Holesovsky <ke...@collabora.com> Tested-by: Jan Holesovsky <ke...@collabora.com> diff --git a/loleaflet/src/control/Control.Menubar.js b/loleaflet/src/control/Control.Menubar.js index 120550a8e..9be729886 100644 --- a/loleaflet/src/control/Control.Menubar.js +++ b/loleaflet/src/control/Control.Menubar.js @@ -255,7 +255,7 @@ L.Control.Menubar = L.Control.extend({ }, {name: _('Tools'), id: 'tools', type: 'menu', menu: [ {name: _('Automatic spell checking'), type: 'unocommand', uno: '.uno:SpellOnline'}, - {name: _('Language for entire document'), type: 'menu', menu: [ + {name: _('Language'), type: 'menu', menu: [ {name: _('None (Do not check spelling)'), id: 'nonelanguage', type: 'unocommand', uno: '.uno:LanguageStatus?Language:string=Default_LANGUAGE_NONE'}]} ]}, {name: _('Help'), id: 'help', type: 'menu', menu: [ @@ -310,7 +310,7 @@ L.Control.Menubar = L.Control.extend({ }, {name: _('Tools'), id: 'tools', type: 'menu', menu: [ {name: _('Automatic spell checking'), type: 'unocommand', uno: '.uno:SpellOnline'}, - {name: _('Language for entire document'), type: 'menu', menu: [ + {name: _('Language'), type: 'menu', menu: [ {name: _('None (Do not check spelling)'), id: 'nonelanguage', type: 'unocommand', uno: '.uno:LanguageStatus?Language:string=Default_LANGUAGE_NONE'}]} ]}, {name: _('Help'), id: 'help', type: 'menu', menu: [ commit bf1aa3326fe652e14b727e9e16d7eab05cfa0576 Author: Jan Holesovsky <ke...@collabora.com> AuthorDate: Thu Oct 26 12:12:13 2017 +0200 Commit: Jan Holesovsky <ke...@collabora.com> CommitDate: Thu Oct 26 17:52:55 2017 +0200 Fix convert-to after the Save As work. Change-Id: I1871dd8331367798ee42b2ca35505847b43b639d Reviewed-on: https://gerrit.libreoffice.org/43881 Reviewed-by: Miklos Vajna <vmik...@collabora.co.uk> Tested-by: Miklos Vajna <vmik...@collabora.co.uk> diff --git a/test/integration-http-server.cpp b/test/integration-http-server.cpp index 717862053..f93874ffa 100644 --- a/test/integration-http-server.cpp +++ b/test/integration-http-server.cpp @@ -239,7 +239,11 @@ void HTTPServerTest::testConvertTo() form.set("format", "txt"); form.addPart("data", new Poco::Net::FilePartSource(srcPath)); form.prepareSubmit(request); - // If this results in a Poco::Net::ConnectionRefusedException, loolwsd is not running. + + // FIXME From some reason we are getting Poco::Net::ConnectionRefusedException + // What happens is that the file is just partially transferred - + // ConvertToPartHandler::handlePart() gets just some 3.6k bytes; no idea + // why yet form.write(session->sendRequest(request)); Poco::Net::HTTPResponse response; diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp index d3cf71067..be9f542f9 100644 --- a/wsd/ClientSession.cpp +++ b/wsd/ClientSession.cpp @@ -656,17 +656,26 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt } else if (tokens.size() == 3 && tokens[0] == "saveas:") { + bool isConvertTo = static_cast<bool>(_saveAsSocket); + std::string encodedURL; if (!getTokenString(tokens[1], "url", encodedURL)) { LOG_ERR("Bad syntax for: " << firstLine); - return false; + // we must not return early with convert-to so that we clean up + // the session + if (!isConvertTo) + { + sendTextFrame("error: cmd=saveas kind=syntax"); + return false; + } } std::string encodedWopiFilename; - if (!getTokenString(tokens[2], "filename", encodedWopiFilename)) + if (!isConvertTo && !getTokenString(tokens[2], "filename", encodedWopiFilename)) { LOG_ERR("Bad syntax for: " << firstLine); + sendTextFrame("error: cmd=saveas kind=syntax"); return false; } @@ -698,7 +707,7 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt LOG_TRC("Save-as URL: " << resultURL.toString()); - if (!_saveAsSocket) + if (!isConvertTo) { // Normal SaveAs - save to Storage and log result. if (resultURL.getScheme() == "file" && !resultURL.getPath().empty()) commit 396c1488b42a89a7b6dad06583b66db3476301f6 Author: Tor Lillqvist <t...@collabora.com> AuthorDate: Thu Oct 26 16:53:43 2017 +0300 Commit: Tor Lillqvist <t...@collabora.com> CommitDate: Thu Oct 26 16:56:25 2017 +0300 Avoid test: : integer expression expected Change-Id: I87f087a29ab4dc5cf94b0eee2a115b77c8283552 diff --git a/configure.ac b/configure.ac index 8c4057783..5d458fa46 100644 --- a/configure.ac +++ b/configure.ac @@ -146,7 +146,7 @@ fi AC_SUBST(LOOLWSD_LOGFILE) MAX_CONNECTIONS=20 -AS_IF([test -n "$with_max_connections" -a "$with_max_connections" -gt "0"], +AS_IF([test -n "$with_max_connections" && test "$with_max_connections" -gt "0"], [MAX_CONNECTIONS="$with_max_connections"]) AS_IF([test "$MAX_CONNECTIONS" -lt "3"], [MAX_CONNECTIONS="3"]) @@ -154,7 +154,7 @@ AC_DEFINE_UNQUOTED([MAX_CONNECTIONS],[$MAX_CONNECTIONS],[Limit the maximum numbe AC_SUBST(MAX_CONNECTIONS) MAX_DOCUMENTS=10 -AS_IF([test -n "$with_max_documents" -a "$with_max_documents" -gt "0"], +AS_IF([test -n "$with_max_documents" && test "$with_max_documents" -gt "0"], [MAX_DOCUMENTS="$with_max_documents"]) AS_IF([test "$MAX_DOCUMENTS" -gt "$MAX_CONNECTIONS"], [MAX_DOCUMENTS="$MAX_CONNECTIONS"]) commit 3dee70e3c4874abd6e7927b250d38d04d879c2bf Author: Marco Cecchetti <marco.cecche...@collabora.com> AuthorDate: Thu Oct 26 12:00:05 2017 +0200 Commit: Marco Cecchetti <mrcek...@gmail.com> CommitDate: Thu Oct 26 13:12:20 2017 +0200 loleaflet: now Calc headers are rendered throught Canvas 2d Change-Id: I5ab0d9a4af7a8bdba3eb0d07e89c811f7b1850b4 Reviewed-on: https://gerrit.libreoffice.org/43880 Reviewed-by: Marco Cecchetti <mrcek...@gmail.com> Tested-by: Marco Cecchetti <mrcek...@gmail.com> diff --git a/loleaflet/src/control/Control.ColumnHeader.js b/loleaflet/src/control/Control.ColumnHeader.js index a9571d255..6d86d7faf 100644 --- a/loleaflet/src/control/Control.ColumnHeader.js +++ b/loleaflet/src/control/Control.ColumnHeader.js @@ -25,50 +25,76 @@ L.Control.ColumnHeader = L.Control.Header.extend({ var cornerHeader = L.DomUtil.create('div', 'spreadsheet-header-corner', rowColumnFrame); L.DomEvent.on(cornerHeader, 'contextmenu', L.DomEvent.preventDefault); L.DomEvent.addListener(cornerHeader, 'click', this._onCornerHeaderClick, this); - var headersContainer = L.DomUtil.create('div', 'spreadsheet-header-columns-container', rowColumnFrame); - this._columns = L.DomUtil.create('div', 'spreadsheet-header-columns', headersContainer); + this._headersContainer = L.DomUtil.create('div', 'spreadsheet-header-columns-container', rowColumnFrame); + this._headerCanvas = L.DomUtil.create('canvas', 'spreadsheet-header-columns', this._headersContainer); + this._canvasContext = this._headerCanvas.getContext('2d'); + this._headerCanvas.width = parseInt(L.DomUtil.getStyle(this._headersContainer, 'width')); + this._headerCanvas.height = parseInt(L.DomUtil.getStyle(this._headersContainer, 'height')); + + L.DomUtil.setStyle(this._headerCanvas, 'cursor', this._cursor); + + L.DomEvent.on(this._headerCanvas, 'mousemove', this._onCanvasMouseMove, this); + L.DomEvent.on(this._headerCanvas, 'mouseout', this._onMouseOut, this); + L.DomEvent.on(this._headerCanvas, 'click', this._onHeaderClick, this); + + this._leftmostColumn = 0; this._leftOffset = 0; this._position = 0; var colHeaderObj = this; $.contextMenu({ - selector: '.spreadsheet-header-column-text', + selector: '.spreadsheet-header-columns', className: 'loleaflet-font', items: { 'insertcolbefore': { name: _('Insert column before'), callback: function(key, options) { - var colAlpha = options.$trigger.attr('rel').split('spreadsheet-column-')[1]; - colHeaderObj.insertColumn.call(colHeaderObj, colAlpha); + var index = colHeaderObj._lastMouseOverIndex; + if (index) { + var colAlpha = colHeaderObj._data[index].text; + colHeaderObj.insertColumn.call(colHeaderObj, colAlpha); + } } }, 'deleteselectedcol': { name: _('Delete column'), callback: function(key, options) { - var colAlpha = options.$trigger.attr('rel').split('spreadsheet-column-')[1]; - colHeaderObj.deleteColumn.call(colHeaderObj, colAlpha); + var index = colHeaderObj._lastMouseOverIndex; + if (index) { + var colAlpha = colHeaderObj._data[index].text; + colHeaderObj.deleteColumn.call(colHeaderObj, colAlpha); + } } }, 'optimalwidth': { name: _('Optimal Width') + '...', callback: function(key, options) { - var colAlpha = options.$trigger.attr('rel').split('spreadsheet-column-')[1]; - colHeaderObj.optimalWidth.call(colHeaderObj, colAlpha); + var index = colHeaderObj._lastMouseOverIndex; + if (index) { + var colAlpha = colHeaderObj._data[index].text; + colHeaderObj.optimalWidth.call(colHeaderObj, colAlpha); + } } }, 'hideColumn': { name: _('Hide Columns'), callback: function(key, options) { - var colAlpha = options.$trigger.attr('rel').split('spreadsheet-column-')[1]; - colHeaderObj.hideColumn.call(colHeaderObj, colAlpha); + var index = colHeaderObj._lastMouseOverIndex; + if (index) { + var colAlpha = colHeaderObj._data[index].text; + colHeaderObj.hideColumn.call(colHeaderObj, colAlpha); + } } }, 'showColumn': { name: _('Show Columns'), callback: function(key, options) { - var colAlpha = options.$trigger.attr('rel').split('spreadsheet-column-')[1]; - colHeaderObj.showColumn.call(colHeaderObj, colAlpha); + var index = colHeaderObj._lastMouseOverIndex; + if (index) { + var colAlpha = colHeaderObj._data[index].text; + colHeaderObj.showColumn.call(colHeaderObj, colAlpha); + } } } }, @@ -136,71 +162,145 @@ L.Control.ColumnHeader = L.Control.Header.extend({ }, _onClearSelection: function (e) { - this.clearSelection(this._columns); + this.clearSelection(this._data); }, _onUpdateSelection: function (e) { - this.updateSelection(this._columns, e.start.x, e.end.x); + var data = this._data; + if (!data) + return; + var start = e.start.x; + var end = e.end.x; + var twips; + if (start !== -1) { + twips = new L.Point(start, start); + start = Math.round(data.converter.call(data.context, twips).x); + } + if (end !== -1) { + twips = new L.Point(end, end); + end = Math.round(data.converter.call(data.context, twips).x); + } + this.updateSelection(data, start, end); }, _onUpdateCurrentColumn: function (e) { - this.updateCurrent(this._columns, e.x); + var data = this._data; + if (!data) + return; + var x = e.x; + if (x !== -1) { + var twips = new L.Point(x, x); + x = Math.round(data.converter.call(data.context, twips).x); + } + this.updateCurrent(data, x); }, _updateColumnHeader: function () { this._map.fire('updaterowcolumnheaders', {x: this._map._getTopLeftPoint().x, y: 0, offset: {x: undefined, y: 0}}); }, + drawHeaderEntry: function (index, isOver) { + if (!index || index <= 0 || index >= this._data.length) + return; + + var ctx = this._canvasContext; + var content = this._data[index].text; + var start = this._data[index - 1].pos - this._leftOffset; + var end = this._data[index].pos - this._leftOffset; + var width = end - start; + var height = this._headerCanvas.height; + var isHighlighted = this._data[index].selected; + + if (width <= 0) + return; + + ctx.save(); + ctx.translate(this._position + this._leftOffset, 0); + // background gradient + var selectionBackgroundGradient = null; + if (isHighlighted) { + selectionBackgroundGradient = ctx.createLinearGradient(start, 0, start, height); + selectionBackgroundGradient.addColorStop(0, this._selectionBackgroundGradient[0]); + selectionBackgroundGradient.addColorStop(0.5, this._selectionBackgroundGradient[1]); + selectionBackgroundGradient.addColorStop(1, this._selectionBackgroundGradient[2]); + } + // clip mask + ctx.beginPath(); + ctx.rect(start, 0, width, height); + ctx.clip(); + // draw background + ctx.fillStyle = isHighlighted ? selectionBackgroundGradient : isOver ? this._hoverColor : this._backgroundColor; + ctx.fillRect(start, 0, width, height); + // draw text content + ctx.fillStyle = isHighlighted ? this._selectionTextColor : this._textColor; + ctx.font = this._font; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(content, end - width / 2, height / 2); + // draw row separator + ctx.fillStyle = this._borderColor; + ctx.fillRect(end -1, 0, 1, height); + ctx.restore(); + }, + + getHeaderEntryBoundingClientRect: function (index) { + if (!index) + index = this._mouseOverIndex; // use last mouse over position + + if (!index || !this._data[index]) + return; + + var rect = this._headerCanvas.getBoundingClientRect(); + + var colStart = this._data[index - 1].pos + this._position; + var colEnd = this._data[index].pos + this._position; + + var left = rect.left + colStart; + var right = rect.left + colEnd; + var top = rect.top; + var bottom = rect.bottom; + return { left: left, right: right, top: top, bottom: bottom }; + }, + viewRowColumnHeaders: function (e) { if (e.data.columns && e.data.columns.length > 0) { this.fillColumns(e.data.columns, e.converter, e.context); - L.DomUtil.setStyle(this._columns, 'left', (this._position + this._leftOffset) + 'px'); } }, fillColumns: function (columns, converter, context) { - var iterator, twip, width, column, text, resize; + var iterator, twip, width; + + this._data = new Array(columns.length); + this._data.converter = converter; + this._data.context = context; + + var canvas = this._headerCanvas; + canvas.width = parseInt(L.DomUtil.getStyle(this._headersContainer, 'width')); + canvas.height = parseInt(L.DomUtil.getStyle(this._headersContainer, 'height')); + + this._canvasContext.clearRect(0, 0, canvas.width, canvas.height); + + var leftmostOffset = new L.Point(columns[0].size, columns[0].size); + this._leftmostColumn = parseInt(columns[0].text); + this._leftOffset = Math.round(converter.call(context, leftmostOffset).x); + + this._data[0] = { pos: this._leftOffset, text: '', selected: false }; - L.DomUtil.empty(this._columns); - var leftOffset = new L.Point(columns[0].size, columns[0].size); - // column[0] is a dummy column header whose text attribute is set to the column index - var leftmostCol = parseInt(columns[0].text); - this._leftOffset = Math.round(converter.call(context, leftOffset).x); for (iterator = 1; iterator < columns.length; iterator++) { - width = columns[iterator].size - columns[iterator - 1].size; - twip = new L.Point(width, width); - column = L.DomUtil.create('div', 'spreadsheet-header-column', this._columns); - text = L.DomUtil.create('div', 'spreadsheet-header-column-text', column); - resize = L.DomUtil.create('div', 'spreadsheet-header-column-resize', column); - L.DomEvent.on(resize, 'contextmenu', L.DomEvent.preventDefault); - column.size = columns[iterator].size; - var content = columns[iterator].text; - text.setAttribute('rel', 'spreadsheet-column-' + content); // for easy addressing - text.innerHTML = content; - width = Math.round(converter.call(context, twip).x) - 1; - if (width <= 0) { - L.DomUtil.setStyle(column, 'display', 'none'); - } else if (width < 10) { - text.column = iterator + leftmostCol; - text.width = width; - L.DomUtil.setStyle(column, 'width', width + 'px'); - L.DomUtil.setStyle(column, 'cursor', 'col-resize'); - L.DomUtil.setStyle(text, 'cursor', 'col-resize'); - L.DomUtil.setStyle(resize, 'display', 'none'); - this.mouseInit(text); - } else { - resize.column = iterator + leftmostCol; - resize.width = width; - L.DomUtil.setStyle(column, 'width', width + 'px'); - L.DomUtil.setStyle(text, 'width', width - 3 + 'px'); - L.DomUtil.setStyle(resize, 'width', '3px'); - this.mouseInit(resize); + twip = new L.Point(columns[iterator].size, columns[iterator].size); + this._data[iterator] = { pos: Math.round(converter.call(context, twip).x), text: columns[iterator].text, selected: false }; + width = this._data[iterator].pos - this._data[iterator - 1].pos; + if (width > 0) { + this.drawHeaderEntry(iterator, false); } - L.DomEvent.addListener(text, 'click', this._onColumnHeaderClick, this); } - if ($('.spreadsheet-header-column-text').length > 0) { - $('.spreadsheet-header-column-text').contextMenu(this._map._permission === 'edit'); + this.mouseInit(canvas); + + L.DomEvent.on(canvas, 'contextmenu', L.DomEvent.preventDefault); + if ($('.spreadsheet-header-columns').length > 0) { + $('.spreadsheet-header-columns').contextMenu(this._map._permission === 'edit'); } }, @@ -232,8 +332,11 @@ L.Control.ColumnHeader = L.Control.Header.extend({ this._map.sendUnoCommand('.uno:SelectColumn ', command); }, - _onColumnHeaderClick: function (e) { - var colAlpha = e.target.getAttribute('rel').split('spreadsheet-column-')[1]; + _onHeaderClick: function (e) { + if (!this._mouseOverIndex) + return; + + var colAlpha = this._data[this._mouseOverIndex].text; var modifier = 0; if (e.shiftKey) { @@ -295,21 +398,31 @@ L.Control.ColumnHeader = L.Control.Header.extend({ var end = new L.Point(e.clientX + offset.x, e.clientY); var distance = this._map._docLayer._pixelsToTwips(end.subtract(start)); - if (item.width != distance.x) { - var command = { - ColumnWidth: { - type: 'unsigned short', - value: this._map._docLayer.twipsToHMM(Math.max(distance.x, 0)) - }, - Column: { - type: 'unsigned short', - value: item.parentNode && item.parentNode.nextSibling && - L.DomUtil.getStyle(item.parentNode.nextSibling, 'display') === 'none' ? item.column + 1 : item.column - } - }; + if (this._mouseOverIndex) { + var clickedColumn = this._data[this._mouseOverIndex]; + var width = clickedColumn.pos - this._data[this._mouseOverIndex - 1]; + var column = this._mouseOverIndex + this._leftmostColumn; + + if (this._data[this._mouseOverIndex + 1] + && this._data[this._mouseOverIndex + 1].pos === clickedColumn.pos) { + column += 1; + } + + if (width !== distance.x) { + var command = { + ColumnWidth: { + type: 'unsigned short', + value: this._map._docLayer.twipsToHMM(Math.max(distance.x, 0)) + }, + Column: { + type: 'unsigned short', + value: column + } + }; - this._map.sendUnoCommand('.uno:ColumnWidth', command); - this._updateColumnHeader(); + this._map.sendUnoCommand('.uno:ColumnWidth', command); + this._updateColumnHeader(); + } } this._map.removeLayer(this._vertLine); @@ -318,11 +431,15 @@ L.Control.ColumnHeader = L.Control.Header.extend({ onDragClick: function (item, clicks, e) { this._map.removeLayer(this._vertLine); + if (!this._mouseOverIndex) + return; + if (clicks === 2) { + var column = this._mouseOverIndex + this._leftmostColumn; var command = { Col: { type: 'unsigned short', - value: item.column - 1 + value: column - 1 }, Modifier: { type: 'unsigned short', @@ -343,9 +460,13 @@ L.Control.ColumnHeader = L.Control.Header.extend({ if (!this._initialized) { this._initialize(); } - if ($('.spreadsheet-header-column-text').length > 0) { - $('.spreadsheet-header-column-text').contextMenu(e.perm === 'edit'); + if ($('.spreadsheet-header-columns').length > 0) { + $('.spreadsheet-header-columns').contextMenu(e.perm === 'edit'); } + }, + + _getPos: function (point) { + return point.x; } }); diff --git a/loleaflet/src/control/Control.Header.js b/loleaflet/src/control/Control.Header.js index 7b3093394..1c522cafe 100644 --- a/loleaflet/src/control/Control.Header.js +++ b/loleaflet/src/control/Control.Header.js @@ -8,60 +8,77 @@ L.Control.Header = L.Control.extend({ }, initialize: function () { + this._headerCanvas = null; this._clicks = 0; this._current = -1; this._selection = {start: -1, end: -1}; + this._mouseOverIndex = undefined; + this._lastMouseOverIndex = undefined; + this._hitResizeArea = false; + + // styles + this._backgroundColor = 'lightgray'; + this._hoverColor = '#DDD'; + this._borderColor = 'darkgray'; + this._textColor = 'black'; + this._font = '12px/1.5 "Segoe UI", Tahoma, Arial, Helvetica, sans-serif'; + this._cursor = 'pointer'; + this._selectionTextColor = 'white'; + this._selectionBackgroundGradient = [ '#3465A4', '#729FCF', '#004586' ]; }, mouseInit: function (element) { L.DomEvent.on(element, 'mousedown', this._onMouseDown, this); }, - select: function (item) { - if (item && !L.DomUtil.hasClass(item, 'spreadsheet-header-selected')) { - L.DomUtil.addClass(item, 'spreadsheet-header-selected'); - } + select: function (data, index) { + if (!data[index]) + return; + data[index].selected = true; + this.drawHeaderEntry(index, false); }, - unselect: function (item) { - if (item && L.DomUtil.hasClass(item, 'spreadsheet-header-selected')) { - L.DomUtil.removeClass(item, 'spreadsheet-header-selected'); - } + unselect: function (data, index) { + if (!data[index]) + return; + data[index].selected = false; + this.drawHeaderEntry(index, false); }, - clearSelection: function (element) { + clearSelection: function (data) { if (this._selection.start === -1 && this._selection.end === -1) return; - var childs = element.children; var start = (this._selection.start === -1) ? 0 : this._selection.start; var end = this._selection.end + 1; for (var iterator = start; iterator < end; iterator++) { - this.unselect(childs[iterator]); + this.unselect(data, iterator); } this._selection.start = this._selection.end = -1; // after clearing selection, we need to select the header entry for the current cursor position, // since we can't be sure that the selection clearing is due to click on a cell // different from the one where the cursor is already placed - this.select(childs[this._current]); + this.select(data, this._current); }, - updateSelection: function(element, start, end) { - var childs = element.children; + updateSelection: function(data, start, end) { + if (!data) + return; + var x0 = 0, x1 = 0; var itStart = -1, itEnd = -1; var selected = false; var iterator = 0; - for (var len = childs.length; iterator < len; iterator++) { - x0 = (iterator > 0 ? childs[iterator - 1].size : 0); - x1 = childs[iterator].size; + for (var len = data.length; iterator < len; iterator++) { + x0 = (iterator > 0 ? data[iterator - 1].pos : 0); + x1 = data[iterator].pos; // 'start < x1' not '<=' or we get highlighted also the `start-row - 1` and `start-column - 1` headers if (x0 <= start && start < x1) { selected = true; itStart = iterator; } if (selected) { - this.select(childs[iterator]); + this.select(data, iterator); } if (x0 <= end && end <= x1) { itEnd = iterator; @@ -72,7 +89,7 @@ L.Control.Header = L.Control.extend({ // if end is greater than the last fetched header position set itEnd to the max possible value // without this hack selecting a whole row and then a whole column (or viceversa) leads to an incorrect selection if (itStart !== -1 && itEnd === -1) { - itEnd = childs.length - 1; + itEnd = data.length - 1; } // we need to unselect the row (column) header entry for the current cell cursor position @@ -80,43 +97,44 @@ L.Control.Header = L.Control.extend({ // does not start by clicking on a cell if (this._current !== -1 && itStart !== -1 && itEnd !== -1) { if (this._current < itStart || this._current > itEnd) { - this.unselect(childs[this._current]); + this.unselect(data, this._current); } } if (this._selection.start !== -1 && itStart !== -1 && itStart > this._selection.start) { for (iterator = this._selection.start; iterator < itStart; iterator++) { - this.unselect(childs[iterator]); + this.unselect(data, iterator); } } if (this._selection.end !== -1 && itEnd !== -1 && itEnd < this._selection.end) { for (iterator = itEnd + 1; iterator <= this._selection.end; iterator++) { - this.unselect(childs[iterator]); + this.unselect(data, iterator); } } this._selection.start = itStart; this._selection.end = itEnd; }, - updateCurrent: function (element, start) { - var childs = element.children; + updateCurrent: function (data, start) { + if (!data) + return; if (start < 0) { - this.unselect(childs[this._current]); + this.unselect(data, this._current); this._current = -1; return; } var x0 = 0, x1 = 0; - for (var iterator = 0, len = childs.length; iterator < len; iterator++) { - x0 = (iterator > 0 ? childs[iterator - 1].size : 0); - x1 = childs[iterator].size; - if (x0 <= start && start <= x1) { + for (var iterator = 1, len = data.length; iterator < len; iterator++) { + x0 = (iterator > 0 ? data[iterator - 1].pos : 0); + x1 = data[iterator].pos; + if (x0 <= start && start < x1) { // when a whole row (column) is selected the cell cursor is moved to the first column (row) // but this action should not cause to select/unselect anything, on the contrary we end up // with all column (row) header entries selected but the one where the cell cursor was // previously placed if (this._selection.start === -1 && this._selection.end === -1) { - this.unselect(childs[this._current]); - this.select(childs[iterator]); + this.unselect(data, this._current); + this.select(data, iterator); } this._current = iterator; break; @@ -124,6 +142,71 @@ L.Control.Header = L.Control.extend({ } }, + _mouseEventToCanvasPos: function(canvas, evt) { + var rect = canvas.getBoundingClientRect(); + return { + x: evt.clientX - rect.left, + y: evt.clientY - rect.top + }; + }, + + _onMouseOut: function (e) { + if (this._mouseOverIndex) { + this.drawHeaderEntry(this._mouseOverIndex, false); + this._lastMouseOverIndex = this._mouseOverIndex; // used by context menu + this._mouseOverIndex = undefined; + } + this._hitResizeArea = false; + L.DomUtil.setStyle(this._headerCanvas, 'cursor', this._cursor); + }, + + _onCanvasMouseMove: function (e) { + var target = e.target || e.srcElement; + + if (!target || this._dragging) { + return false; + } + + var isMouseOverResizeArea = false; + var pos = this._getPos(this._mouseEventToCanvasPos(this._headerCanvas, e)); + pos = pos - this._position; + + var mouseOverIndex = this._mouseOverIndex; + for (var iterator = 1; iterator < this._data.length; ++iterator) { + var start = this._data[iterator - 1].pos; + var end = this._data[iterator].pos; + if (pos > start && pos <= end) { + mouseOverIndex = iterator; + var resizeAreaStart = Math.max(start, end - 3); + isMouseOverResizeArea = (pos > resizeAreaStart); + break; + } + } + + if (mouseOverIndex !== this._mouseOverIndex) { + if (this._mouseOverIndex) { + this.drawHeaderEntry(this._mouseOverIndex, false); + } + if (mouseOverIndex) { + this.drawHeaderEntry(mouseOverIndex, true); + } + } + + if (isMouseOverResizeArea !== this._hitResizeArea) { + if (isMouseOverResizeArea) { + L.DomEvent.off(this._headerCanvas, 'click', this._onHeaderClick, this); + } + else { + L.DomEvent.on(this._headerCanvas, 'click', this._onHeaderClick, this); + } + var cursor = isMouseOverResizeArea ? this.options.cursor : this._cursor; + L.DomUtil.setStyle(this._headerCanvas, 'cursor', cursor); + this._hitResizeArea = isMouseOverResizeArea; + } + + this._mouseOverIndex = mouseOverIndex; + }, + _onMouseDown: function (e) { var target = e.target || e.srcElement; @@ -131,14 +214,21 @@ L.Control.Header = L.Control.extend({ return false; } + if (!this._hitResizeArea) + return; + L.DomUtil.disableImageDrag(); L.DomUtil.disableTextSelection(); L.DomEvent.stopPropagation(e); + + L.DomEvent.off(target, 'mousemove', this._onCanvasMouseMove, this); + L.DomEvent.off(target, 'mouseout', this._onMouseOut, this); + L.DomEvent.on(document, 'mousemove', this._onMouseMove, this); L.DomEvent.on(document, 'mouseup', this._onMouseUp, this); - var rect = target.parentNode.getBoundingClientRect(); + var rect = this.getHeaderEntryBoundingClientRect(); this._start = new L.Point(rect.left, rect.top); this._offset = new L.Point(rect.right - e.clientX, rect.bottom - e.clientY); this._item = target; @@ -150,13 +240,6 @@ L.Control.Header = L.Control.extend({ this._dragging = true; L.DomEvent.preventDefault(e); - var target = e.target || e.srcElement; - if (target.style.cursor !== this.options.cursor && - (L.DomUtil.hasClass(target, 'spreadsheet-header-column-text') || - L.DomUtil.hasClass(target, 'spreadsheet-header-row-text'))) { - target.style.cursor = this.options.cursor; - } - this.onDragMove(this._item, this._start, this._offset, e); }, @@ -167,6 +250,9 @@ L.Control.Header = L.Control.extend({ L.DomUtil.enableImageDrag(); L.DomUtil.enableTextSelection(); + L.DomEvent.on(this._item, 'mousemove', this._onCanvasMouseMove, this); + L.DomEvent.on(this._item, 'mouseout', this._onMouseOut, this); + if (this._dragging) { this.onDragEnd(this._item, this._start, this._offset, e); this._clicks = 0; @@ -182,5 +268,8 @@ L.Control.Header = L.Control.extend({ onDragStart: function () {}, onDragMove: function () {}, onDragEnd: function () {}, - onDragClick: function () {} + onDragClick: function () {}, + getHeaderEntryBoundingClientRect: function () {}, + drawHeaderEntry: function () {}, + _getPos: function () {} }); diff --git a/loleaflet/src/control/Control.RowHeader.js b/loleaflet/src/control/Control.RowHeader.js index 744a99a66..99945f7d7 100644 --- a/loleaflet/src/control/Control.RowHeader.js +++ b/loleaflet/src/control/Control.RowHeader.js @@ -22,50 +22,76 @@ L.Control.RowHeader = L.Control.Header.extend({ this._map.on('clearselectionheader', this._onClearSelection, this); this._map.on('updatecurrentheader', this._onUpdateCurrentRow, this); var rowColumnFrame = L.DomUtil.get('spreadsheet-row-column-frame'); - var headersContainer = L.DomUtil.create('div', 'spreadsheet-header-rows-container', rowColumnFrame); - this._rows = L.DomUtil.create('div', 'spreadsheet-header-rows', headersContainer); + this._headersContainer = L.DomUtil.create('div', 'spreadsheet-header-rows-container', rowColumnFrame); + this._headerCanvas = L.DomUtil.create('canvas', 'spreadsheet-header-rows', this._headersContainer); + this._canvasContext = this._headerCanvas.getContext('2d'); + this._headerCanvas.width = parseInt(L.DomUtil.getStyle(this._headersContainer, 'width')); + this._headerCanvas.height = parseInt(L.DomUtil.getStyle(this._headersContainer, 'height')); + + L.DomUtil.setStyle(this._headerCanvas, 'cursor', this._cursor); + + L.DomEvent.on(this._headerCanvas, 'mousemove', this._onCanvasMouseMove, this); + L.DomEvent.on(this._headerCanvas, 'mouseout', this._onMouseOut, this); + L.DomEvent.on(this._headerCanvas, 'click', this._onHeaderClick, this); + + this._topRow = 0; this._topOffset = 0; this._position = 0; var rowHeaderObj = this; $.contextMenu({ - selector: '.spreadsheet-header-row-text', + selector: '.spreadsheet-header-rows', className: 'loleaflet-font', items: { 'insertrowabove': { name: _('Insert row above'), callback: function(key, options) { - var row = parseInt(options.$trigger.attr('rel').split('spreadsheet-row-')[1]); - rowHeaderObj.insertRow.call(rowHeaderObj, row); + var index = rowHeaderObj._lastMouseOverIndex; + if (index) { + var row = rowHeaderObj._data[index].text; + rowHeaderObj.insertRow.call(rowHeaderObj, row); + } } }, 'deleteselectedrow': { name: _('Delete row'), callback: function(key, options) { - var row = parseInt(options.$trigger.attr('rel').split('spreadsheet-row-')[1]); - rowHeaderObj.deleteRow.call(rowHeaderObj, row); + var index = rowHeaderObj._lastMouseOverIndex; + if (index) { + var row = rowHeaderObj._data[index].text; + rowHeaderObj.deleteRow.call(rowHeaderObj, row); + } } }, 'optimalheight': { name: _('Optimal Height') + '...', callback: function(key, options) { - var row = parseInt(options.$trigger.attr('rel').split('spreadsheet-row-')[1]); - rowHeaderObj.optimalHeight.call(rowHeaderObj, row); + var index = rowHeaderObj._lastMouseOverIndex; + if (index) { + var row = rowHeaderObj._data[index].text; + rowHeaderObj.optimalHeight.call(rowHeaderObj, row); + } } }, 'hideRow': { name: _('Hide Rows'), callback: function(key, options) { - var row = parseInt(options.$trigger.attr('rel').split('spreadsheet-row-')[1]); - rowHeaderObj.hideRow.call(rowHeaderObj, row); + var index = rowHeaderObj._lastMouseOverIndex; + if (index) { + var row = rowHeaderObj._data[index].text; + rowHeaderObj.hideRow.call(rowHeaderObj, row); + } } }, 'showRow': { name: _('Show Rows'), callback: function(key, options) { - var row = parseInt(options.$trigger.attr('rel').split('spreadsheet-row-')[1]); - rowHeaderObj.showRow.call(rowHeaderObj, row); + var index = rowHeaderObj._lastMouseOverIndex; + if (index) { + var row = rowHeaderObj._data[index].text; + rowHeaderObj.showRow.call(rowHeaderObj, row); + } } } }, @@ -127,72 +153,145 @@ L.Control.RowHeader = L.Control.Header.extend({ }, _onClearSelection: function (e) { - this.clearSelection(this._rows); + this.clearSelection(this._data); }, _onUpdateSelection: function (e) { - this.updateSelection(this._rows, e.start.y, e.end.y); + var data = this._data; + if (!data) + return; + var start = e.start.y; + var end = e.end.y; + var twips; + if (start !== -1) { + twips = new L.Point(start, start); + start = Math.round(data.converter.call(data.context, twips).y); + } + if (end !== -1) { + twips = new L.Point(end, end); + end = Math.round(data.converter.call(data.context, twips).y); + } + this.updateSelection(data, start, end); }, _onUpdateCurrentRow: function (e) { - this.updateCurrent(this._rows, e.y); + var data = this._data; + if (!data) + return; + var y = e.y; + if (y !== -1) { + var twips = new L.Point(y, y); + y = Math.round(data.converter.call(data.context, twips).y); + } + this.updateCurrent(data, y); }, _updateRowHeader: function () { this._map.fire('updaterowcolumnheaders', {x: 0, y: this._map._getTopLeftPoint().y, offset: {x: 0, y: undefined}}); }, + drawHeaderEntry: function (index, isOver) { + if (!index || index <= 0 || index >= this._data.length) + return; + + var ctx = this._canvasContext; + var content = this._data[index].text; + var start = this._data[index - 1].pos - this._topOffset; + var end = this._data[index].pos - this._topOffset; + var height = end - start; + var width = this._headerCanvas.width; + var isHighlighted = this._data[index].selected; + + if (height <= 0) + return; + + ctx.save(); + ctx.translate(0, this._position + this._topOffset); + // background gradient + var selectionBackgroundGradient = null; + if (isHighlighted) { + selectionBackgroundGradient = ctx.createLinearGradient(0, start, 0, start + height); + selectionBackgroundGradient.addColorStop(0, this._selectionBackgroundGradient[0]); + selectionBackgroundGradient.addColorStop(0.5, this._selectionBackgroundGradient[1]); + selectionBackgroundGradient.addColorStop(1, this._selectionBackgroundGradient[2]); + } + // clip mask + ctx.beginPath(); + ctx.rect(0, start, width, height); + ctx.clip(); + // draw background + ctx.fillStyle = isHighlighted ? selectionBackgroundGradient : isOver ? this._hoverColor : this._backgroundColor; + ctx.fillRect(0, start, width, height); + // draw text content + ctx.fillStyle = isHighlighted ? this._selectionTextColor : this._textColor; + ctx.font = this._font; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(content, width / 2, end - (height / 2)); + // draw row separator + ctx.fillStyle = this._borderColor; + ctx.fillRect(0, end -1, width, 1); + ctx.restore(); + }, + + getHeaderEntryBoundingClientRect: function (index) { + if (!index) + index = this._mouseOverIndex; // use last mouse over position + + if (!index || !this._data[index]) + return; + + var rect = this._headerCanvas.getBoundingClientRect(); + + var rowStart = this._data[index - 1].pos + this._position; + var rowEnd = this._data[index].pos + this._position; + + var left = rect.left; + var right = rect.right; + var top = rect.top + rowStart; + var bottom = rect.top + rowEnd; + return { left: left, right: right, top: top, bottom: bottom }; + }, + viewRowColumnHeaders: function (e) { if (e.data.rows && e.data.rows.length) { this.fillRows(e.data.rows, e.converter, e.context); - L.DomUtil.setStyle(this._rows, 'top', (this._position + this._topOffset) + 'px'); } }, fillRows: function (rows, converter, context) { - var iterator, twip, height, row, text, resize; + var iterator, twip, height; + + this._data = new Array(rows.length); + this._data.converter = converter; + this._data.context = context; + + var canvas = this._headerCanvas; + canvas.width = parseInt(L.DomUtil.getStyle(this._headersContainer, 'width')); + canvas.height = parseInt(L.DomUtil.getStyle(this._headersContainer, 'height')); + + this._canvasContext.clearRect(0, 0, canvas.width, canvas.height); - L.DomUtil.empty(this._rows); var topOffset = new L.Point(rows[0].size, rows[0].size); - var topRow = parseInt(rows[0].text); ... etc. - the rest is truncated _______________________________________________ Libreoffice-commits mailing list libreoffice-comm...@lists.freedesktop.org https://lists.freedesktop.org/mailman/listinfo/libreoffice-commits