/* Copyright (C) 2019 Kasm Web * * This is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this software; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, * USA. */ #include #include #include #include #include #include #include #include #include #include #include #include using namespace rfb; static LogWriter vlog("WEBP"); static const PixelFormat pfRGBX(32, 24, false, true, 255, 255, 255, 0, 8, 16); static const PixelFormat pfBGRX(32, 24, false, true, 255, 255, 255, 16, 8, 0); struct TightWEBPConfiguration { uint8_t quality; uint8_t method; }; // Matt: I found that -q 40 -m 1 can compress an HD image in 78ms and the quality is // acceptable and similar to cjpeg -q 70 // Using a test image from a browser, I matched DSSIM levels with the below JPEG // targets. Qualities 6 and above couldn't match jpeg, presumably because // webp always subsamples. Each level is 50-70% of the corresponding JPEG // level's size, while approx matching its quality. // NOTE: The JPEG quality and subsampling levels below were obtained // experimentally by the VirtualGL Project. They represent the approximate // average compression ratios listed below, as measured across the set of // every 10th frame in the SPECviewperf 9 benchmark suite. // // 9 = JPEG quality 100, no subsampling (ratio ~= 10:1) // [this should be lossless, except for round-off error] // 8 = JPEG quality 92, no subsampling (ratio ~= 20:1) // [this should be perceptually lossless, based on current research] // 7 = JPEG quality 86, no subsampling (ratio ~= 25:1) // 6 = JPEG quality 79, no subsampling (ratio ~= 30:1) // 5 = JPEG quality 77, 4:2:2 subsampling (ratio ~= 40:1) // 4 = JPEG quality 62, 4:2:2 subsampling (ratio ~= 50:1) // 3 = JPEG quality 42, 4:2:2 subsampling (ratio ~= 60:1) // 2 = JPEG quality 41, 4:2:0 subsampling (ratio ~= 70:1) // 1 = JPEG quality 29, 4:2:0 subsampling (ratio ~= 80:1) // 0 = JPEG quality 15, 4:2:0 subsampling (ratio ~= 100:1) static const struct TightWEBPConfiguration conf[10] = { { 5, 0 }, // 0 { 24, 0 }, // 1 { 30, 0 }, // 2 { 37, 0 }, // 3 { 42, 0 }, // 4 { 65, 0 }, // 5 { 78, 0 }, // 6 { 85, 0 }, // 7 { 88, 0 }, // 8 { 100, 0 } // 9 }; TightWEBPEncoder::TightWEBPEncoder(SConnection* conn) : Encoder(conn, encodingTight, (EncoderFlags)(EncoderUseNativePF | EncoderLossy), -1), qualityLevel(-1) { } TightWEBPEncoder::~TightWEBPEncoder() { } bool TightWEBPEncoder::isSupported() { if (!conn->cp.supportsEncoding(encodingTight)) return false; if (conn->cp.supportsWEBP) return true; // Tight support, but not WEBP return false; } void TightWEBPEncoder::setQualityLevel(int level) { qualityLevel = level; } void TightWEBPEncoder::setFineQualityLevel(int quality, int subsampling) { // NOP } bool TightWEBPEncoder::treatLossless() { return qualityLevel >= rfb::Server::treatLossless; } void TightWEBPEncoder::compressOnly(const PixelBuffer* pb, const uint8_t qualityIn, std::vector &out, const bool lowVideoQuality) const { const rdr::U8* buffer; int stride; uint8_t quality, method; WebPConfig cfg; WebPPicture pic; WebPMemoryWriter wrt; buffer = pb->getBuffer(pb->getRect(), &stride); if (lowVideoQuality) { if (rfb::Server::webpVideoQuality == -1) { quality = 3; method = 0; } else { uint8_t num = rfb::Server::webpVideoQuality; quality = conf[num].quality; method = conf[num].method; } } else if (qualityIn <= 9) { quality = conf[qualityIn].quality; method = conf[qualityIn].method; } else { quality = 8; method = 0; } WebPConfigInit(&cfg); cfg.method = method; cfg.quality = quality; cfg.thread_level = 1; // Try to use multiple threads WebPPictureInit(&pic); pic.width = pb->getRect().width(); pic.height = pb->getRect().height(); if (pfRGBX.equal(pb->getPF())) { WebPPictureImportRGBX(&pic, buffer, stride * 4); } else if (pfBGRX.equal(pb->getPF())) { WebPPictureImportBGRX(&pic, buffer, stride * 4); } else { rdr::U8* tmpbuf = new rdr::U8[pic.width * pic.height * 3]; pb->getPF().rgbFromBuffer(tmpbuf, (const rdr::U8 *) buffer, pic.width, stride, pic.height); stride = pic.width * 3; WebPPictureImportRGB(&pic, tmpbuf, stride); delete [] tmpbuf; } WebPMemoryWriterInit(&wrt); pic.writer = WebPMemoryWrite; pic.custom_ptr = &wrt; if (!WebPEncode(&cfg, &pic)) { // Error vlog.error("WEBP error %u", pic.error_code); } out.resize(wrt.size); memcpy(&out[0], wrt.mem, wrt.size); WebPPictureFree(&pic); WebPMemoryWriterClear(&wrt); } void TightWEBPEncoder::writeOnly(const std::vector &out) const { rdr::OutStream* os; os = conn->getOutStream(conn->cp.supportsUdp); os->writeU8(tightWebp << 4); writeCompact(out.size(), os); os->writeBytes(&out[0], out.size()); } void TightWEBPEncoder::writeRect(const PixelBuffer* pb, const Palette& palette) { const rdr::U8* buffer; int stride; uint8_t quality, method; WebPConfig cfg; WebPPicture pic; WebPMemoryWriter wrt; rdr::OutStream* os; buffer = pb->getBuffer(pb->getRect(), &stride); if (qualityLevel >= 0 && qualityLevel <= 9) { quality = conf[qualityLevel].quality; method = conf[qualityLevel].method; } else { quality = 8; method = 0; } WebPConfigInit(&cfg); cfg.method = method; cfg.quality = quality; cfg.thread_level = 1; // Try to use multiple threads WebPPictureInit(&pic); pic.width = pb->getRect().width(); pic.height = pb->getRect().height(); if (pfRGBX.equal(pb->getPF())) { WebPPictureImportRGBX(&pic, buffer, stride * 4); } else if (pfBGRX.equal(pb->getPF())) { WebPPictureImportBGRX(&pic, buffer, stride * 4); } else { rdr::U8* tmpbuf = new rdr::U8[pic.width * pic.height * 3]; pb->getPF().rgbFromBuffer(tmpbuf, (const rdr::U8 *) buffer, pic.width, stride, pic.height); stride = pic.width * 3; WebPPictureImportRGB(&pic, tmpbuf, stride); delete [] tmpbuf; } WebPMemoryWriterInit(&wrt); pic.writer = WebPMemoryWrite; pic.custom_ptr = &wrt; if (!WebPEncode(&cfg, &pic)) { // Error vlog.error("WEBP error %u", pic.error_code); } os = conn->getOutStream(conn->cp.supportsUdp); os->writeU8(tightWebp << 4); writeCompact(wrt.size, os); os->writeBytes(wrt.mem, wrt.size); WebPPictureFree(&pic); WebPMemoryWriterClear(&wrt); } // How many milliseconds would it take to encode a 256x256 block at quality 8 rdr::U32 TightWEBPEncoder::benchmark() const { rdr::U8* buffer; struct timeval start; int stride, i; const uint8_t quality = 8, method = 2; WebPConfig cfg; WebPPicture pic; WebPMemoryWriter wrt; ManagedPixelBuffer pb(pfRGBX, 256, 256); buffer = pb.getBufferRW(pb.getRect(), &stride); // Fill it with random data for (i = 0; i < pb.getRect().width() * pb.getRect().height() * 4; i++) buffer[i] = random(); gettimeofday(&start, NULL); WebPConfigInit(&cfg); cfg.method = method; cfg.quality = quality; cfg.thread_level = 1; // Try to use multiple threads WebPPictureInit(&pic); pic.width = pb.getRect().width(); pic.height = pb.getRect().height(); WebPPictureImportRGBX(&pic, buffer, stride * 4); WebPMemoryWriterInit(&wrt); pic.writer = WebPMemoryWrite; pic.custom_ptr = &wrt; if (!WebPEncode(&cfg, &pic)) { // Error vlog.error("WEBP error %u", pic.error_code); } WebPPictureFree(&pic); WebPMemoryWriterClear(&wrt); return msSince(&start); } void TightWEBPEncoder::writeSolidRect(int width, int height, const PixelFormat& pf, const rdr::U8* colour) { // FIXME: Add a shortcut in the JPEG compressor to handle this case // without having to use the default fallback which is very slow. Encoder::writeSolidRect(width, height, pf, colour); } void TightWEBPEncoder::writeCompact(rdr::U32 value, rdr::OutStream* os) const { // Copied from TightEncoder as it's overkill to inherit just for this rdr::U8 b; b = value & 0x7F; if (value <= 0x7F) { os->writeU8(b); } else { os->writeU8(b | 0x80); b = value >> 7 & 0x7F; if (value <= 0x3FFF) { os->writeU8(b); } else { os->writeU8(b | 0x80); os->writeU8(value >> 14 & 0xFF); } } }