[Libreoffice-commits] online.git: Branch 'private/mmeeks/deltas' - 180 commits - bundled/include common/Crypto.cpp common/Crypto.hpp common/Message.hpp common/Seccomp.cpp common/Session.cpp common/Session.hpp common/SpookyV2.cpp common/Unit.hpp configure.ac discovery.xml docker/Dockerfile docker/.dockerignore docker/l10n-docker-nightly.sh docker/scripts .gitignore kit/ChildSession.cpp kit/ChildSession.hpp kit/Delta.hpp kit/DummyLibreOfficeKit.cpp kit/ForKit.cpp kit/Kit.cpp kit/KitHelper.hpp loleaflet/build loleaflet/debug loleaflet/dist loleaflet/main.js loleaflet/Makefile.am loleaflet/po loleaflet/README loleaflet/reference.html loleaflet/src loleaflet/util loolkitconfig.xcu loolwsd-systemplate-setup loolwsd.xml.in Makefile.am net/Socket.hpp net/Ssl.cpp net/WebSocketHandler.hpp test/countloolkits.hpp test/data test/DeltaTests.cpp test/httpcrashtest.cpp test/integration-http-server.cpp test/Makefile.am test/run_unit.sh.in test/test.cpp test/test.hpp test/UnitClient.cpp test/UnitOAuth.cpp test/Uni tWOPI.cpp test/UnitWOPISaveAs.cpp test/WhiteBoxTests.cpp test/WopiTestServer.hpp tools/Config.cpp tools/KitClient.cpp tools/map.cpp wsd/Admin.cpp wsd/Admin.hpp wsd/AdminModel.hpp wsd/Auth.cpp wsd/Auth.hpp wsd/ClientSession.cpp wsd/ClientSession.hpp wsd/DocumentBroker.cpp wsd/DocumentBroker.hpp wsd/FileServer.cpp wsd/LOOLWSD.cpp wsd/LOOLWSD.hpp wsd/protocol.txt wsd/reference.txt wsd/Storage.cpp wsd/Storage.hpp wsd/TileDesc.hpp

Fri, 06 Dec 2019 17:51:16 -0800

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

Reply via email to