diff --git a/CMakeLists.txt b/CMakeLists.txt index b7dd91a15..5e5203782 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,13 +8,13 @@ set(CMAKE_AUTOMOC ON) ### generic info set ( WEBSITE "https://drawpile.net/" ) -set ( DRAWPILE_VERSION "2.0.11" ) +set ( DRAWPILE_VERSION "2.1.0" ) ### protocol versions # see doc/protocol.md for protocol version history set ( DRAWPILE_PROTO_SERVER_VERSION 4 ) -set ( DRAWPILE_PROTO_MAJOR_VERSION 20 ) -set ( DRAWPILE_PROTO_MINOR_VERSION 1 ) +set ( DRAWPILE_PROTO_MAJOR_VERSION 21 ) +set ( DRAWPILE_PROTO_MINOR_VERSION 2 ) set ( DRAWPILE_PROTO_DEFAULT_PORT 27750 ) ### diff --git a/ChangeLog b/ChangeLog index ebe4597a0..cc6295325 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,32 @@ +2019-02-13 Version 2.1.0 + * Changed internal pixel format to premultiplied 32 bit RGBA + * Added PutTile command and optimized layer initialization + * dprectool: added --msg-freq option + * Protocol change: replaced ToolChange and PenMove with DrawDabs commands + * Replaced "background layer" concept with a tiled canvas background. (Mostly compatible with MyPaint) + * Redesigned user list box and chat panel + * Autoreset threshold is now adjustable and includes reset image as base size + * Individual layers can now be censored + * Merged "Find sessions" dialog into the "Join" dialog + * Simplified "Host" dialog + * Redesigned "Login" dialog + * Moved layer hamburger menu to the main menu bar (and expanded it) + * Added support for user avatars + * Added controls for flipping, rotating and zooming to the navigator dock + * New user level: Trusted users + * Added tiered feature access controls (guest/registered/trusted/operator) + * Added zoom and rotation controls to navigator + * Removed sliders from status bar + * Added zoom tool + * Added zoom/rotation/flip toolbar + * Show keyboard shortcuts in toolbar tooltips + * Replaced recording status icon with toolbar button + * Replaced "new chat message" status icon with toolbar button (red dot indicates new message) + * Added template export feature + * Added square pixel brush + * Replaced Ffmpeg video export with native WebM export + * Added private messaging feature + 2018-08-14 Version 2.0.11 * Server: fixed OP status auto-restoration for users who used the "become operator" feature * Server AppImage: updated libmicrohttpd to version 0.9.59 diff --git a/README.md b/README.md index f8ea75e2f..77b1a5670 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Client specific dependencies: * KF5 KDNSSD (optional: local server discovery with Zeroconf) * GIFLIB (optional: animated GIF export) * MiniUPnP (optional: automatic port forwarding setup) +* LibVPX (optional: WebM video export) Server specific dependencies (you can also take a look at [Docker build](server/docker/Dockerfile) script): diff --git a/config/FindVpx.cmake b/config/FindVpx.cmake new file mode 100644 index 000000000..a30a7162f --- /dev/null +++ b/config/FindVpx.cmake @@ -0,0 +1,19 @@ +# Try to find libvpx +# +# This will define +# LIBVPX_FOUND +# LIBVPX_INCLUDE_DIRS +# LIBVPX_LIBRARIES + +find_path(LIBVPX_INCLUDE_DIR vpx_encoder.h + PATH_SUFFIXES vpx) +find_library(LIBVPX_LIBRARY NAMES vpx) + +set(LIBVPX_INCLUDE_DIRS ${LIBVPX_INCLUDE_DIR}) +set(LIBVPX_LIBRARIES ${LIBVPX_LIBRARY}) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(libvpx DEFAULT_MSG LIBVPX_LIBRARY LIBVPX_INCLUDE_DIR) + +mark_as_advanced(LIBVPX_INCLUDE_DIR LIBVPX_LIBRARY) + diff --git a/desktop/CMakeLists.txt b/desktop/CMakeLists.txt index bdc2d7793..c29e28ef2 100644 --- a/desktop/CMakeLists.txt +++ b/desktop/CMakeLists.txt @@ -20,7 +20,7 @@ endif() if( XDGMENU ) install(CODE " - execute_process(COMMAND ${XDGMENU} install --novendor ${CMAKE_CURRENT_SOURCE_DIR}/drawpile.desktop) + execute_process(COMMAND ${XDGMENU} install --novendor ${CMAKE_CURRENT_SOURCE_DIR}/net.drawpile.drawpile.desktop) ") endif() @@ -36,5 +36,5 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/palettes" DESTINATION "${DATADIR} install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/sounds" DESTINATION "${DATADIR}") install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/theme" DESTINATION "${DATADIR}") -install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/drawpile.appdata.xml" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/appdata/") +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/net.drawpile.drawpile.appdata.xml" DESTINATION "${CMAKE_INSTALL_PREFIX}/share/metainfo/") diff --git a/desktop/drawpile.appdata.xml b/desktop/net.drawpile.drawpile.appdata.xml similarity index 74% rename from desktop/drawpile.appdata.xml rename to desktop/net.drawpile.drawpile.appdata.xml index 8c0eb0633..59bd28c64 100644 --- a/desktop/drawpile.appdata.xml +++ b/desktop/net.drawpile.drawpile.appdata.xml @@ -1,8 +1,12 @@ - drawpile.desktop + net.drawpile.drawpile + net.drawpile.drawpile.desktop CC0-1.0 GPL-3.0+ + Calle Laakkonen + Drawpile + Collaborative Drawing

Drawpile is a drawing program that lets you share a canvas with other users in real time.

Feature highlights:

@@ -16,16 +20,18 @@
  • Discover drawing sessions via global announcement feature or locally with Zeroconf
  • - http://drawpile.net/ + https://drawpile.net/ https://github.com/callaa/Drawpile/issues - http://drawpile.net/static/screenshots/screenshot2.png + https://drawpile.net/media/d/images/screenshot2.png + + Graphics + drawpile - Calle Laakkonen
    diff --git a/desktop/drawpile.desktop b/desktop/net.drawpile.drawpile.desktop similarity index 88% rename from desktop/drawpile.desktop rename to desktop/net.drawpile.drawpile.desktop index f0768e1c6..4fde069eb 100644 --- a/desktop/drawpile.desktop +++ b/desktop/net.drawpile.drawpile.desktop @@ -8,6 +8,6 @@ MimeType=image/openraster;image/png;image/jpeg;application/x-drawpile-recording; Type=Application Icon=drawpile StartupNotify=true -Categories=Graphics;Network;RasterGraphics; +Categories=Graphics; Terminal=false diff --git a/desktop/theme/dark/zoom-select.svg b/desktop/theme/dark/zoom-select.svg new file mode 100644 index 000000000..2d78c59f7 --- /dev/null +++ b/desktop/theme/dark/zoom-select.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/desktop/theme/light/zoom-select.svg b/desktop/theme/light/zoom-select.svg new file mode 100644 index 000000000..d2c3d0daa --- /dev/null +++ b/desktop/theme/light/zoom-select.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/doc/example.dptpl.txt b/doc/example.dptpl.txt deleted file mode 100644 index 6cb89429d..000000000 --- a/doc/example.dptpl.txt +++ /dev/null @@ -1,16 +0,0 @@ -########################################################################## -# -# This is a minimal session template example. -# This template creates a session with a blank 1200x1200px canvas with -# a white background layer and a transparent foreground layer. -# -########################################################################## - -# A session must begin with a "resize" command to set the initial canvas size. -1 resize right=1200 bottom=1000 - -# Create default layers -1 newlayer id=0x0101 fill=#ffffffff title=Background -1 newlayer id=0x0102 title=Foreground - - diff --git a/doc/protocol.md b/doc/protocol.md index 523b339cf..e258af4a9 100644 --- a/doc/protocol.md +++ b/doc/protocol.md @@ -99,6 +99,21 @@ Protocol stability promises: * New server features may be added at any time, but they should not break older clients, nor should a missing feature break newer clients. +### Protocol dp:4.21.2 (2.1.0) + + * Changed PutImage pixel format to ARGB32_Premultiplied + * Added PutTile command + * Removed ToolChange and PenMove commands + * Added DrawDabsClassic and DrawDabsPixel commands + * Added CanvasBackground command + * Added TrustedUsers command + * General session lock is now applied with the LayerACL command by using layer ID 0 + * Removed "lock new users by default" feature + * Replaced SessionACL message with FeatureAccessLevels message + * Added sublayer field to LayerAttributes command + * Added flags field and "censored" flag to LayerAttributes command + * Added PrivateChat transparent meta message + ### Protocol dp:4.20.1 (2.0.9) * Added `Filtered` message type. Fully backward compatible. diff --git a/pkg/mac/.gitignore b/pkg/mac/.gitignore new file mode 100644 index 000000000..f521a3ed6 --- /dev/null +++ b/pkg/mac/.gitignore @@ -0,0 +1,3 @@ +deps/ +build/ + diff --git a/pkg/mac/deps.sha256 b/pkg/mac/deps.sha256 new file mode 100644 index 000000000..7a3a29f70 --- /dev/null +++ b/pkg/mac/deps.sha256 @@ -0,0 +1,6 @@ +91b7a9359f1bfe6f667a5a9c23f6b2178555df26ca2e4dd1bb5c38dc36c77144 *extra-cmake-modules.tar.xz +34a7377ba834397db019e8eb122e551a49c98f49df75ec3fcc92b9a794a4f6d1 *giflib.tar.gz +8f28ab8a8f7236ae5e9e6cf35263dbbb87a52ec938d35515f073bc33dbc33d90 *karchive.tar.xz +677ed3d572706cc896c1e05bb2f1fb1a6c50ffaa13d1c62de13e35eca1e85803 *kdnssd.tar.xz +e8577a6acf5a168b13fc6f64d829e8ea86e917bcddf75f452bd46c69d2a6445f *libvpx.zip +e19fb5e01ea5a707e2a8cb96f537fbd9f3a913d53d804a3265e3aeab3d2064c6 *miniupnpc.tar.gz diff --git a/pkg/mac/install-deps.sh b/pkg/mac/install-deps.sh new file mode 100755 index 000000000..d32de1416 --- /dev/null +++ b/pkg/mac/install-deps.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +set -e + +### VERSIONS TO DOWNLOAD +GIFLIB_URL=https://sourceforge.net/projects/giflib/files/giflib-5.1.4.tar.gz/download +MINIUPNPC_URL=http://miniupnp.free.fr/files/download.php?file=miniupnpc-2.1.tar.gz +LIBVPX_URL=https://github.com/webmproject/libvpx/archive/v1.8.0.zip +ECM_URL=https://download.kde.org/stable/frameworks/5.54/extra-cmake-modules-5.54.0.tar.xz +KARCHIVE_URL=https://download.kde.org/stable/frameworks/5.54/karchive-5.54.0.tar.xz +KDNSSD_URL=https://download.kde.org/stable/frameworks/5.54/kdnssd-5.54.0.tar.xz + +### Build flags +export CFLAGS=-mmacosx-version-min=10.7 +export CXXFLAGS=-mmacosx-version-min=10.7 + +### GENERIC FUNCTIONS +function download_package() { + URL="$1" + OUT="$2" + + if [ -f "$OUT" ] + then + echo "$OUT already downloaded. Skipping..." + else + curl -L "$URL" -o "$OUT" + fi +} + +function install_package() { + if [ -d $1-* ]; then + echo "Build directory for $1 already exists. Skipping..." + return + fi + if [ -f "$1.zip" ]; then + unzip -q "$1.zip" + elif [ -f "$1.tar.gz" ]; then + tar xfz "$1.tar.gz" + elif [ -f "$1.tar.xz" ]; then + tar xfJ "$1.tar.xz" + else + echo "BUG: Unhandled package archive format $1" + exit 1 + fi + pushd $1-* + build_$2 + popd +} + +### PACKAGE SPECIFIC BUILD SCRIPTS +function build_autoconf() { + ./configure "--prefix=$QTPATH" + make + make install +} + +function build_autoconf_libvpx() { + ./configure "--prefix=$QTPATH" --disable-vp8 --disable-vp9-decoder + make + make install +} + +function build_justmakeinstall() { + INSTALLPREFIX="$QTPATH" make install +} + +function build_cmake() { + mkdir build + cd build + cmake .. "-DCMAKE_PREFIX_PATH=$QTPATH" "-DCMAKE_INSTALL_PREFIX=$QTPATH" + make + make install +} + +### MAIN SCRIPT STARTS HERE +if [ -z "$QTPATH" ] +then + echo "QTPATH environment variable not set" + exit 1 +fi + +if [ ! -d "$QTPATH" ] +then + echo "$QTPATH is not a directory!" + exit 1 +fi + +echo "Dependencies will be downloaded to $(pwd)/deps and installed to $QTPATH." +echo "Write 'ok' to continue" + +read confirmation + +if [ "$confirmation" != "ok" ] +then + echo "Cancelled." + exit 0 +fi + +mkdir -p deps +cd deps + +# Download dependencies +download_package "$GIFLIB_URL" giflib.tar.gz +download_package "$MINIUPNPC_URL" miniupnpc.tar.gz +download_package "$LIBVPX_URL" libvpx.zip +download_package "$ECM_URL" extra-cmake-modules.tar.xz +download_package "$KARCHIVE_URL" karchive.tar.xz +download_package "$KDNSSD_URL" kdnssd.tar.xz + +# Make sure we have the right versions (and they haven't been tampered with) +shasum -a 256 -c ../deps.sha256 + +# Build and install +install_package giflib autoconf +install_package miniupnpc justmakeinstall +install_package libvpx autoconf_libvpx +install_package extra-cmake-modules cmake +install_package karchive cmake +install_package kdnssd cmake + diff --git a/pkg/mac/make-mac-dmg.sh b/pkg/mac/make-mac-dmg.sh index 70b050cac..ffb434615 100755 --- a/pkg/mac/make-mac-dmg.sh +++ b/pkg/mac/make-mac-dmg.sh @@ -1,24 +1,53 @@ #!/bin/bash -echo "NOTE: Run this script while inside of the root Drawpile directory" -echo "ANOTHER NOTE: This script is meant to be run after building drawpile, so do it after :)" -echo "Otherwise the universe might explode into popcorn..." -sleep 2 +set -e -DRAWPILE="$(pwd)" +if [ "${QTDIR+}" == "" ]; then + QTDIR="$HOME/Qt/5.9.7/clang_64" +fi + +VERSION=$(grep DRAWPILE_VERSION ../../CMakeLists.txt | cut -d \" -f 2) +TITLE="Drawpile $VERSION" + +if [ ! -d "$QTDIR" ]; then + echo "$QTDIR not found!" + exit 1 +fi + +if [ "$(which appdmg)" == "" ]; then + echo "Appdmg not found!" + echo "Run npm install -g appdmg" + exit 1 +fi + +if [ -d build ]; then + echo "Old build directory exists!" + echo "Run 'rm -rf build' and try again." + exit 1 +fi + +# Build +mkdir build +pushd build +cmake ../../../ \ + "-DCMAKE_PREFIX_PATH=$QTDIR" \ + -DSERVER=OFF \ + -DCMAKE_BUILD_TYPE=Release +make # Remove version string from the binary -cd "$DRAWPILE"/build/bin/Drawpile.app/Contents/MacOS +pushd bin +pushd Drawpile.app/Contents/MacOS BINFILE="$(readlink -n Drawpile)" rm Drawpile mv "$BINFILE" Drawpile +popd + +# Bundle frameworks +"$QTDIR/bin/macdeployqt" Drawpile.app +popd +popd # Package the app in a dmg archive -APPDMG="$(command -v appdmg)" -if [ "$APPDMG" = "/usr/local/bin/appdmg" ]; then - "$APPDMG" "$DRAWPILE"/pkg/Mac/spec.json "$DRAWPILE"/build/bin/Drawpile.dmg -else - echo "Ya done goofed, you don't have appdmg installed!; (or maybe it's not in your path, in which case sorry...)" -fi +appdmg spec.json build/bin/Drawpile.dmg -echo "The Drawpile.dmg archive is in the build directory" \ No newline at end of file diff --git a/pkg/mac/spec.json b/pkg/mac/spec.json index 9e9489f9b..a4dd98381 100644 --- a/pkg/mac/spec.json +++ b/pkg/mac/spec.json @@ -3,7 +3,7 @@ "background": "background.png", "icon": "Drawpile_Drive_Icon.icns", "contents": [ - { "x": 130, "y": 250, "type": "file", "path": "../../build/bin/Drawpile.app" }, + { "x": 130, "y": 250, "type": "file", "path": "build/bin/Drawpile.app" }, { "x": 380, "y": 250, "type": "link", "path": "/Applications" } ] } diff --git a/pkg/make-mac-pkg.sh b/pkg/make-mac-pkg.sh deleted file mode 100755 index 54343d9fe..000000000 --- a/pkg/make-mac-pkg.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -set -e - -QTDIR="$HOME/Qt/5.7/clang_64" -VERSION=$(grep DRAWPILE_VERSION ../CMakeLists.txt | cut -d \" -f 2) -TITLE="Drawpile $VERSION" - -# Create build directory -mkdir -p mac-deploy -cd mac-deploy - -# Build the app -cmake ../../ "-DCMAKE_PREFIX_PATH=$QTDIR" -DSERVER=off -DCMAKE_BUILD_TYPE=Release -make - -# Remove version string from the binary -cd bin -cd Drawpile.app/Contents/MacOS -BINFILE="$(readlink -n Drawpile)" -rm Drawpile -mv "$BINFILE" Drawpile -cd - - -"$QTDIR/bin/macdeployqt" Drawpile.app -dmg - -mv Drawpile.dmg "Drawpile $VERSION.dmg" - diff --git a/pkg/win/Dockerfile b/pkg/win/Dockerfile index 7fbd10395..8005a2e2a 100644 --- a/pkg/win/Dockerfile +++ b/pkg/win/Dockerfile @@ -1,26 +1,29 @@ FROM dockcross/windows-x64 -# Add MXE dependencies and customized settings.mk WORKDIR /usr/src/mxe -RUN git checkout master -RUN git pull + +# Get specific MXE version that is known to work (2019-01-10) +# Provides Qt 5.12.0 +RUN git fetch && git checkout 62329b26b94cf1eeb37a24f73228eda2078e44ea + +# Custom MXE settings ADD settings.mk /usr/src/mxe/ RUN make -j$(nproc) +# Download MXE deps RUN make download-qt5 -RUN make download-miniupnpc download-giflib download-libsodium +RUN make download-miniupnpc download-giflib download-libsodium download-libvpx # Patch Qt ADD qtbase-2-no-tabletevent.patch /usr/src/mxe/src/ # Build MXE dependencies -RUN make -j$(nproc) qt5 miniupnpc giflib libsodium +RUN make -j$(nproc) qt5 miniupnpc giflib libsodium libvpx # Add our own deps -ADD extra-cmake-modules.mk karchive.mk /usr/src/mxe/src/ -RUN make download-karchive download-extra-cmake-modules -ADD karchive-1-notests.patch /usr/src/mxe/src/ +ADD extra-cmake-modules.mk karchive.mk dnssd_shim.mk kdnssd.mk kdnssd-1-qtendian.patch kdnssd-2-shim.patch /usr/src/mxe/src/ +RUN make download-karchive download-extra-cmake-modules download-dnssd_shim download-kdnssd # Build our dependencies -RUN make extra-cmake-modules karchive +RUN make extra-cmake-modules karchive kdnssd diff --git a/pkg/win/dnssd_shim.mk b/pkg/win/dnssd_shim.mk new file mode 100644 index 000000000..19a7c1ea5 --- /dev/null +++ b/pkg/win/dnssd_shim.mk @@ -0,0 +1,14 @@ +PKG := dnssd_shim +$(PKG)_WEBSITE := https://github.com/callaa/dnssd_shim +$(PKG)_DESCR := dnssd.dll dynamic loader shim +$(PKG)_VERSION := 3abe0ee +$(PKG)_CHECKSUM := aaa58801ff3fcc541db06dfdede3537c9c33346bbb7a3d82912dc7069653cd32 +$(PKG)_GH_CONF := callaa/dnssd_shim/branches/master +$(PKG)_DEPS := cc + +define $(PKG)_BUILD + CC='$(PREFIX)/bin/$(TARGET)-gcc' AR='$(PREFIX)/bin/$(TARGET)-ar' $(MAKE) -C '$(1)' + cp '$(1)/libdnssd_shim.a' '$(PREFIX)/$(TARGET)/lib' + cp '$(1)/include/dns_sd.h' '$(PREFIX)/$(TARGET)/include' +endef + diff --git a/pkg/win/drawpile.iss b/pkg/win/drawpile.iss index 5d3e91805..0ccb716a0 100644 --- a/pkg/win/drawpile.iss +++ b/pkg/win/drawpile.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "Drawpile" -#define MyAppVersion "2.0.11" +#define MyAppVersion "2.1.0" #define MyAppURL "http://drawpile.net/" #define MyAppExeName "drawpile.exe" diff --git a/pkg/win/extra-cmake-modules.mk b/pkg/win/extra-cmake-modules.mk index 7130c99fb..a955cb653 100644 --- a/pkg/win/extra-cmake-modules.mk +++ b/pkg/win/extra-cmake-modules.mk @@ -1,11 +1,11 @@ PKG := extra-cmake-modules $(PKG)_WEBSITE := https://community.kde.org/Frameworks $(PKG)_DESCR := KDE Frameworks 5 Extra CMake Modules -$(PKG)_VERSION := 5.41.0 -$(PKG)_CHECKSUM := baaf60940b9ff883332629ba2800090bb86722ba49a85cc12782e4ee5b169f67 +$(PKG)_VERSION := 5.55.0 +$(PKG)_CHECKSUM := 649453922aef38a24af04258ab6661ddfd566aaba4d1773a9e1f4799344406f5 $(PKG)_SUBDIR := $(PKG)-$($(PKG)_VERSION) $(PKG)_FILE := $($(PKG)_SUBDIR).tar.xz -$(PKG)_URL := http://download.kde.org/stable/frameworks/5.41/$($(PKG)_FILE) +$(PKG)_URL := http://download.kde.org/stable/frameworks/5.55/$($(PKG)_FILE) $(PKG)_DEPS := gcc qtbase define $(PKG)_BUILD diff --git a/pkg/win/karchive-1-notests.patch b/pkg/win/karchive-1-notests.patch deleted file mode 100644 index 0e284bf41..000000000 --- a/pkg/win/karchive-1-notests.patch +++ /dev/null @@ -1,12 +0,0 @@ ---- karchive-5.31.0/CMakeLists.txt.orig 2017-02-18 20:23:35.537259060 +0000 -+++ karchive-5.31.0/CMakeLists.txt 2017-02-18 20:23:39.663914927 +0000 -@@ -58,8 +58,6 @@ - SOVERSION 5) - - add_subdirectory(src) --add_subdirectory(autotests) --add_subdirectory(tests) - - - # create a Config.cmake and a ConfigVersion.cmake file and install them - diff --git a/pkg/win/karchive.mk b/pkg/win/karchive.mk index 103225bc0..c6d00e73d 100644 --- a/pkg/win/karchive.mk +++ b/pkg/win/karchive.mk @@ -1,16 +1,16 @@ PKG := karchive $(PKG)_WEBSITE := https://community.kde.org/Frameworks $(PKG)_DESCR := KDE Frameworks 5 KArchive -$(PKG)_VERSION := 5.41.0 -$(PKG)_CHECKSUM := 43c40f06e8a5e3198e5363a82748b3a7cb79526489e6bb651ca59e509297a741 +$(PKG)_VERSION := 5.55.0 +$(PKG)_CHECKSUM := 8475efa46cdc054d9fb6336e42c6075fb037921a9147d4e5aa564a5e58b79fd2 $(PKG)_SUBDIR := $(PKG)-$($(PKG)_VERSION) $(PKG)_FILE := $($(PKG)_SUBDIR).tar.xz -$(PKG)_URL := http://download.kde.org/stable/frameworks/5.41/$($(PKG)_FILE) +$(PKG)_URL := http://download.kde.org/stable/frameworks/5.55/$($(PKG)_FILE) $(PKG)_DEPS := gcc qtbase define $(PKG)_BUILD mkdir '$(1)/build' - cd '$(1)/build' && '$(TARGET)-cmake' .. -DTESTS=off + cd '$(1)/build' && '$(TARGET)-cmake' .. -DBUILD_TESTING=off $(MAKE) -C '$(1)/build' install endef diff --git a/pkg/win/kdnssd-1-qtendian.patch b/pkg/win/kdnssd-1-qtendian.patch new file mode 100644 index 000000000..891aa69fb --- /dev/null +++ b/pkg/win/kdnssd-1-qtendian.patch @@ -0,0 +1,42 @@ +diff -ru kdnssd-5.54.0.orig/src/mdnsd-publicservice.cpp kdnssd-5.54.0/src/mdnsd-publicservice.cpp +--- kdnssd-5.54.0.orig/src/mdnsd-publicservice.cpp 2019-01-20 17:23:54.407549801 +0000 ++++ kdnssd-5.54.0/src/mdnsd-publicservice.cpp 2019-01-20 17:24:50.534010980 +0000 +@@ -20,7 +20,7 @@ + + #include + #include +-#include ++#include + #include "publicservice.h" + #include "servicebase_p.h" + #include "mdnsd-sdevent.h" +@@ -172,7 +172,7 @@ + fullType += ',' + subtype; + } + if (DNSServiceRegister(&ref, 0, 0, d->m_serviceName.toUtf8().constData(), fullType.toLatin1().constData(), domainToDNS(d->m_domain).constData(), NULL, +- htons(d->m_port), TXTRecordGetLength(&txt), TXTRecordGetBytesPtr(&txt), publish_callback, ++ qToBigEndian(d->m_port), TXTRecordGetLength(&txt), TXTRecordGetBytesPtr(&txt), publish_callback, + reinterpret_cast(d)) == kDNSServiceErr_NoError) { + d->setRef(ref); + } +diff -ru kdnssd-5.54.0.orig/src/mdnsd-remoteservice.cpp kdnssd-5.54.0/src/mdnsd-remoteservice.cpp +--- kdnssd-5.54.0.orig/src/mdnsd-remoteservice.cpp 2019-01-20 17:23:54.407549801 +0000 ++++ kdnssd-5.54.0/src/mdnsd-remoteservice.cpp 2019-01-20 17:24:32.840742420 +0000 +@@ -18,7 +18,7 @@ + * Boston, MA 02110-1301, USA. + */ + +-#include ++#include + #include + #include + #include +@@ -152,7 +152,7 @@ + map[QString::fromUtf8(key)].clear(); + } + } +- ResolveEvent rev(DNSToDomain(hosttarget), ntohs(port), map); ++ ResolveEvent rev(DNSToDomain(hosttarget), qFromBigEndian(port), map); + QCoreApplication::sendEvent(obj, &rev); + } + diff --git a/pkg/win/kdnssd-2-shim.patch b/pkg/win/kdnssd-2-shim.patch new file mode 100644 index 000000000..a707495c1 --- /dev/null +++ b/pkg/win/kdnssd-2-shim.patch @@ -0,0 +1,25 @@ +diff -ru kdnssd-5.54.0.orig/cmake/FindDNSSD.cmake kdnssd-5.54.0/cmake/FindDNSSD.cmake +--- kdnssd-5.54.0.orig/cmake/FindDNSSD.cmake 2019-01-20 17:23:54.407549801 +0000 ++++ kdnssd-5.54.0/cmake/FindDNSSD.cmake 2019-01-20 17:26:15.030368312 +0000 +@@ -45,18 +45,17 @@ + if (APPLE) + set(DNSSD_LIBRARIES "/usr/lib/libSystem.dylib") + else (APPLE) +- FIND_LIBRARY(DNSSD_LIBRARIES NAMES dns_sd ) ++ FIND_LIBRARY(DNSSD_LIBRARIES NAMES dnssd_shim ) + endif (APPLE) + + cmake_push_check_state() + set(CMAKE_REQUIRED_INCLUDES ${DNSSD_INCLUDE_DIR}) + set(CMAKE_REQUIRED_LIBRARIES ${DNSSD_LIBRARIES}) +- CHECK_FUNCTION_EXISTS(DNSServiceRefDeallocate DNSSD_FUNCTION_FOUND) + cmake_pop_check_state() + +- if (DNSSD_INCLUDE_DIR AND DNSSD_LIBRARIES AND DNSSD_FUNCTION_FOUND) ++ if (DNSSD_INCLUDE_DIR AND DNSSD_LIBRARIES) + set(DNSSD_FOUND TRUE) +- endif (DNSSD_INCLUDE_DIR AND DNSSD_LIBRARIES AND DNSSD_FUNCTION_FOUND) ++ endif (DNSSD_INCLUDE_DIR AND DNSSD_LIBRARIES) + endif (DNSSD_INCLUDE_DIR) + + if (DNSSD_FOUND) diff --git a/pkg/win/kdnssd.mk b/pkg/win/kdnssd.mk new file mode 100644 index 000000000..73962b117 --- /dev/null +++ b/pkg/win/kdnssd.mk @@ -0,0 +1,16 @@ +PKG := kdnssd +$(PKG)_WEBSITE := https://community.kde.org/Frameworks +$(PKG)_DESCR := KDE Frameworks 5 KDNSSD +$(PKG)_VERSION := 5.55.0 +$(PKG)_CHECKSUM := ec9bf96ea760061d8b3a6efaf70f74de5554fe59ff8cfcdd04a6e2daeb919252 +$(PKG)_SUBDIR := $(PKG)-$($(PKG)_VERSION) +$(PKG)_FILE := $($(PKG)_SUBDIR).tar.xz +$(PKG)_URL := http://download.kde.org/stable/frameworks/5.55/$($(PKG)_FILE) +$(PKG)_DEPS := cc qtbase dnssd_shim + +define $(PKG)_BUILD + mkdir '$(1)/build' + cd '$(1)/build' && '$(TARGET)-cmake' .. -DBUILD_TESTING=off + $(MAKE) -C '$(1)/build' install +endef + diff --git a/pkg/win/make-pkg.sh b/pkg/win/make-pkg.sh index 242ca2fb2..2571d1178 100755 --- a/pkg/win/make-pkg.sh +++ b/pkg/win/make-pkg.sh @@ -29,6 +29,7 @@ cd $PKGNAME # Copy DLLs MBIN="$MXEROOT/bin" +cp "$MBIN/libwinpthread-1.dll" . cp "$MBIN/libgcc_s_seh-1.dll" . cp "$MBIN/libstdc++-6.dll" . cp "$MBIN/libstdc++-6.dll" . @@ -44,12 +45,13 @@ cp "$MBIN/libfreetype-6.dll" . cp "$MBIN/libglib-2.0-0.dll" . cp "$MBIN/libintl-8.dll" . cp "$MBIN/libiconv-2.dll" . -cp "$MBIN/libeay32.dll" . -cp "$MBIN/ssleay32.dll" . +cp "$MBIN/libcrypto-1_1-x64.dll" . +cp "$MBIN/libssl-1_1-x64.dll" . cp "$MBIN/libgif-7.dll" . cp "$MBIN/libminiupnpc.dll" . cp "$MBIN/libsqlite3-0.dll" . cp "$MBIN/libKF5Archive.dll" . +cp "$MBIN/libKF5DNSSD.dll" . cp "$MBIN/libsodium-23.dll" . QROOT="$MXEROOT/qt5" diff --git a/server/autoban.py b/server/autoban.py deleted file mode 100755 index dad64ff4f..000000000 --- a/server/autoban.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -"""Autoban script for Drawpile dedicated server. -This script looks through the server's configuration database and -adds serverwide bans for users who have been repeatedly banned from sessions. - -The serverwide ban duration depends on how many sessions the user was initially -banned from. - -Basic usage: autoban.py config.db -Run "autoban.py -h" for list of supported command line arguments. -""" - -import argparse -import sqlite3 -import sys -import os - -def autoban(conn, sessions, days, penalty, verbose=False): - """Scan the server log for users who have been banned from at least the - given number of different sessions in the last number of days and add - them to the serverwide banlist. - - Returns the number of new bans added. - - Arguments: - conn -- the database connection - sessions -- ban count treshold (must have been banned from this many sessions) - days -- timespan treshold (look for bans this many days in the past) - penalty -- serverwide ban will expire after this many days, multiplied per offense - verbose -- print extra info about discovered bans - - Returns: - The number of new bans added - """ - - if sessions < 1: - raise ValueError("Session count must be at least one.") - if days < 1: - raise ValueError("Must look back at least one day.") - if penalty < 1: - raise ValueError("Penalty time must be at least one day.") - - c = conn.cursor() - - # Get all in-session bans in the last days - rows = c.execute("""SELECT user, session - FROM serverlog - WHERE timestamp>=datetime('now', ?) - AND topic='Ban'""", - ['-' + str(days) + ' days']) - - # Group bans by IP address - users = {} - for row in rows: - try: - userid, userip, username = row[0].split(';', 2) - except ValueError: - print("Invalid user (ID;IP;Name) triplet: " + row[0], file=sys.stderr) - continue - - if userip not in users: - users[userip] = {'names': set(), 'sessions': set()} - users[userip]['names'].add(username) - users[userip]['sessions'].add(row[1]) - - # Gather a list of potential serverwide bans - bans = [] - for ip, info in users.items(): - if verbose: - print ('{banning}{ip} banned from {count} sessions using names "{names}"'.format( - banning="PLONK! " if (len(info['sessions']) >= sessions) else " ", - ip=ip, - names=', '.join(info['names']), - count=len(info['sessions']) - )) - - if len(info['sessions']) >= sessions: - bans.append({ - 'ip': ip, - 'expiration': '+{} days'.format(penalty * len(info['sessions'])), - 'comment': 'Banned from {count} sessions. Names used: {names}'.format( - names=', '.join(info['names']), - count=len(info['sessions']) - ) - }) - - # Filter out existing bans from the list - c.execute("SELECT ip FROM ipbans WHERE subnet=0 AND expires>datetime('now')") - active_bans = set(x[0] for x in c.fetchall()) - bans = [b for b in bans if b['ip'] not in active_bans] - - # Insert new bans - for b in bans: - c.execute("INSERT INTO ipbans VALUES (:ip, 0, datetime('now', :expiration), :comment, datetime('now'))", b) - conn.commit() - - return len(bans) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument("database", help="configuration database") - parser.add_argument("--sessions", type=int, default=3, help="number of sessions a user must be banned from") - parser.add_argument("--days", type=int, default=2, help="number of days to look back") - parser.add_argument("--penalty", type=int, default=5, help="serverwide ban duration per individual ban (days)") - parser.add_argument('--verbose', '-v', default=False, action='store_true') - args = parser.parse_args() - - if not os.path.isfile(args.database): - print("{}: file not found!".format(args.database), file=sys.stderr) - sys.exit(1) - - conn = sqlite3.connect(args.database) - try: - newbans = autoban( - conn, - sessions=args.sessions, - days=args.days, - penalty=args.penalty, - verbose=args.verbose) - - except ValueError as e: - print (str(e), file=sys.stderr) - sys.exit(1) - - finally: - conn.close() - - if newbans>0 or args.verbose: - print ("{} new ban{} added".format(newbans, 's' if newbans!=1 else '')) - diff --git a/server/bantool.py b/server/bantool.py new file mode 100755 index 000000000..f05a80359 --- /dev/null +++ b/server/bantool.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +"""Ban list analysis & autoban tool for Drawpile Dedicted server. + +List bans: + bantool.py list [--expired] [--format table|json|csv] [--ip IP] + +Autoban users who have been banned often from individual sessions: + bantool.py autoban [--dry-run] [--days 2] [--sessions 3] [--penalty 5] + + The --days parameter sets the number of days to look back in the log. + The --session parameter sets the number of sessions a user must have been + banned from to earn a serverwide ban. + The --penalty parameter sets the ban duration in days per infraction. + Use the --dry-run option to see what would happen without actually banning + anyone. + +Get info about a specific IP's ban status: + bantool.py info + +Add a ban manually: + bantool.py ban + +Remove a ban manually: + bantool.py unban + + Note: autoban will restore all bans that match its criteria! + Using the special target 'expired' will delete all expired ban entries + from the database. + +Clear all active bans: + bantool.py unban all +""" + +import argparse +import itertools +import sqlite3 +import ipaddress +import json +import csv +import sys +import os + +class BadArg(Exception): + pass + +# List bans +def command_list(db, *, format, expired=False, ip=None): + """Show ban list + + Arguments: + db -- the database connection + format -- output format (table, json, csv) + expired -- show expired bans too + ip -- show only entries matching this IP + """ + + sql_filter = ["1=1"] + params = [] + + if not expired: + sql_filter.append("expires > current_timestamp") + + if ip: + sql_filter.append("in_subnet(?, ip, subnet)") + params.append(ip) + + query = "SELECT expires, added, ip, subnet, comment FROM ipbans " \ + "WHERE {filter} " \ + "ORDER BY expires DESC".format( + filter=' AND '.join(sql_filter) + ) + + rows = db.execute(query, params) + + FORMATS[format]([r[0] for r in rows.description], rows) + + +def command_autoban(db, *, sessions, days, penalty, verbose=False, dryrun=False): + """Scan the server log for users who have been banned from at least the + given number of different sessions in the last number of days and add + them to the serverwide banlist. + + Arguments: + db -- the database connection + sessions -- ban count treshold (must have been banned from this many sessions) + days -- timespan treshold (look for bans this many days in the past) + penalty -- serverwide ban will expire after this many days, multiplied per offense + verbose -- print extra info about discovered bans + dryrun -- don't actually add any new bans (setting this implies verbose=True) + """ + + if sessions < 1: + raise BadArg("Session count must be at least one.") + if days < 1: + raise BadArg("Must look back at least one day.") + if penalty < 1: + raise BadArg("Penalty time must be at least one day.") + verbose = verbose or dryrun + + # Get all in-session bans in the last days + rows = db.execute("""SELECT user, session + FROM serverlog + WHERE timestamp>=datetime('now', ?) + AND topic='Ban'""", + ['-' + str(days) + ' days'] + ) + + # Group bans by IP address + users = {} + for row in rows: + try: + userid, userip, username = row[0].split(';', 2) + except ValueError: + print("Invalid user (ID;IP;Name) triplet: " + row[0], file=sys.stderr) + continue + + if userip not in users: + users[userip] = {'names': set(), 'sessions': set()} + users[userip]['names'].add(username) + users[userip]['sessions'].add(row[1]) + + # Gather a list of potential serverwide bans + verbose_output = [] + bans = [] + for ip, info in users.items(): + if verbose: + verbose_output.append(( + ' X' if (len(info['sessions']) >= sessions) else '', + ip, + len(info['sessions']), + ', '.join(info['names']) + )) + + if len(info['sessions']) >= sessions: + bans.append({ + 'ip': ip, + 'expiration': '+{} days'.format(penalty * len(info['sessions'])), + 'comment': 'Banned from {count} sessions. Names used: {names}'.format( + names=', '.join(info['names']), + count=len(info['sessions']) + ) + }) + + # Filter out existing bans from the list + active_bans = db.execute("SELECT ip FROM ipbans WHERE subnet=0 AND expires>current_timestamp") + active_bans = set(x[0] for x in active_bans) + bans = [b for b in bans if b['ip'] not in active_bans] + + if verbose: + print_table(('Ban', 'IP', 'Sessions', 'Names used'), verbose_output) + print("---") + print("Adding %d new serverwide bans." % len(bans)) + + # Insert new bans + if not dryrun: + for b in bans: + db.execute("INSERT INTO ipbans VALUES (:ip, 0, datetime('now', :expiration), :comment, datetime('now'))", b) + db.commit() + + +def command_info(db, *, ip): + """Get ban related information about the given IP address. + """ + + + # Serverwide bans: + banned_until = db.execute( + "SELECT julianday(expires)-julianday('now'), comment FROM ipbans WHERE expires>current_timestamp AND in_subnet(?, ip, subnet)", + (ip,) + ).fetchone() + + print("IP address:", ip) + if banned_until: + print("Banned serverwide for %.1f more days" % banned_until[0]) + print("Reason:", banned_until[1]) + else: + print("No serverwide ban") + + # Session bans + rows = db.execute("""SELECT timestamp, user, session, message + FROM serverlog + WHERE topic='Ban' AND user LIKE ?""", + ['%;' + ip + ';%'] + ).fetchall() + + if rows: + print("Banned from %d sessions:" % len(rows)) + rows = map(lambda row: (row[0], row[1][row[1].rindex(';')+1:], *row[2:]), rows) + print_table(("Timestamp", "Username", "Session ID", "Message"), rows) + else: + print("Not banned from any session recently") + + +def command_ban(db, *, ip, days, message=""): + """Add a ban entry manually. + """ + if '/' in ip: + ip, subnet = ip.split('/') + subnet = int(subnet) + else: + subnet = 0 + + if subnet < 0 or subnet > 128: + raise BadArg("Invalid subnet mask") + + if days < 1: + raise BadArg("Ban duration must be at least one day") + + db.execute( + "INSERT INTO ipbans (ip, subnet, expires, comment, added) " \ + "VALUES (:ip, :subnet, datetime('now', :expiration), :msg, current_timestamp)", + { + 'ip': ip, + 'subnet': subnet, + 'expiration': '+%d days' % days, + 'msg': message, + } + ) + db.commit() + + +def command_unban(db, *, ip): + """Remove all active ban entries for the given IP""" + if ip == 'all': + c = db.execute("DELETE FROM ipbans WHERE expires > current_timestamp") + + elif ip == 'expired': + c = db.execute("DELETE FROM ipbans WHERE expires < current_timestamp") + + else: + if '/' in ip: + ip, subnet = ip.split('/') + subnet = int(subnet) + else: + subnet = 0 + + c = db.execute( + "DELETE FROM ipbans WHERE expires > current_timestamp AND ip=? AND subnet=?", + (ip, subnet) + ) + db.commit() + + print("Removed %d entries" % c.rowcount) + + +# Output formatters +def print_table(header, rows): + rows = list(rows) + columns = [0] * len(header) + + for row in itertools.chain((header,), rows): + for i, col in enumerate(row[:-1]): + columns[i] = max(columns[i], len(str(col))) + + for row in itertools.chain((header,), rows): + for width, col in zip(columns, row): + val = str(col) + sys.stdout.write(val) + sys.stdout.write(' ' * (width - len(val) + 1)) + sys.stdout.write('\n') + +def print_json(header, rows): + json.dump( + list(dict(zip(header, row)) for row in rows), + sys.stdout, + indent=0 + ) + +def print_csv(header, rows): + writer = csv.writer(sys.stdout) + writer.writerow(header) + writer.writerows(rows) + +FORMATS = { + 'table': print_table, + 'json': print_json, + 'csv': print_csv, +} + + +def _sqlite_in_subnet(ip, subnet, mask): + try: + ipaddr = ipaddress.ip_address(ip) + if mask == 0: + return ipaddr == ipaddress.ip_address(subnet) + + return ipaddr in ipaddress.ip_network(subnet + "/" + str(mask)) + except Exception as e: + print(e) + raise + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("database", + help="configuration database") + + subparsers = parser.add_subparsers(help="sub-command help") + + # List bans + cmd_list = subparsers.add_parser("list", help="List bans") + cmd_list.add_argument("--format", type=str, default='table', + help="Output format (%s)" % ', '.join(FORMATS.keys())) + cmd_list.add_argument("--expired", dest="expired", action="store_true", + help="Show expired bans") + cmd_list.add_argument("--ip", type=str, default='', + help="Show ban rules matching this IP") + cmd_list.set_defaults(func=command_list) + + # Info about a specific IPs ban status + cmd_info = subparsers.add_parser("info", help="Info about a specific IP address") + cmd_info.add_argument("ip", type=str, help="IP address") + cmd_info.set_defaults(func=command_info) + + # Autoban + cmd_autoban = subparsers.add_parser("autoban", help="Autoban repeat offenders") + cmd_autoban.add_argument("--sessions", type=int, default=3, help="number of sessions a user must be banned from") + cmd_autoban.add_argument("--days", type=int, default=2, help="number of days to look back") + cmd_autoban.add_argument("--penalty", type=int, default=5, help="serverwide ban duration per individual ban (days)") + cmd_autoban.add_argument('--verbose', '-v', default=False, action='store_true') + cmd_autoban.add_argument('--dry-run', dest="dryrun", default=False, action='store_true') + cmd_autoban.set_defaults(func=command_autoban) + + # Manually add a ban + cmd_ban = subparsers.add_parser("ban", + help="Add a ban") + cmd_ban.add_argument("ip", type=str, + help="IP address (with optional subnet)") + cmd_ban.add_argument("--days", type=int, default=365, + help="Number of days the ban lasts for (default is 1 year)") + cmd_ban.add_argument("--message", "-m", dest="message", type=str, + default="Banned manually", + help="Banlist comment") + cmd_ban.set_defaults(func=command_ban) + + # Remove a ban + cmd_unban = subparsers.add_parser("unban", + help="Remove bans") + cmd_unban.add_argument("ip", type=str, + help="IP[/subnet] | all | expired") + cmd_unban.set_defaults(func=command_unban) + + # Parse arguments + args = vars(parser.parse_args()) + + if 'func' not in args: + parser.print_help() + sys.exit(0) + + cmd = args.pop('func') + dbfile = args.pop('database') + + try: + if not os.path.isfile(dbfile): + raise BadArg(dbfile + ": file not found!") + + conn = sqlite3.connect(dbfile) + try: + conn.create_function("in_subnet", 3, _sqlite_in_subnet) + cmd(conn, **args) + finally: + conn.close() + + except BadArg as e: + print(str(e), file=sys.stderr) + sys.exit(1) + diff --git a/server/docker/Dockerfile b/server/docker/Dockerfile index 28d7e3e1f..95becd0fa 100644 --- a/server/docker/Dockerfile +++ b/server/docker/Dockerfile @@ -1,5 +1,5 @@ ## Common base -FROM alpine:3.8 as common +FROM alpine:3.9 as common RUN apk add --no-cache qt5-qtbase qt5-qtbase-sqlite libmicrohttpd libbz2 libsodium ## Build container diff --git a/server/docker/build-deps.sh b/server/docker/build-deps.sh index d7537a927..6eb1e8d03 100644 --- a/server/docker/build-deps.sh +++ b/server/docker/build-deps.sh @@ -1,5 +1,5 @@ -wget https://github.com/KDE/extra-cmake-modules/archive/v5.49.0.zip -O ecm.zip -wget https://github.com/KDE/karchive/archive/v5.49.0.zip -O karchive.zip +wget https://github.com/KDE/extra-cmake-modules/archive/v5.55.0.zip -O ecm.zip +wget https://github.com/KDE/karchive/archive/v5.55.0.zip -O karchive.zip unzip ecm.zip unzip karchive.zip diff --git a/server/logtool.py b/server/logtool.py new file mode 100755 index 000000000..fac913ce4 --- /dev/null +++ b/server/logtool.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +"""Log analysis tool for Drawpile dedicated server. + +Usage: + + logtool.py [--columns col1,col2,...] [--output ] [filters] +""" + +import argparse +import itertools +import sqlite3 +import json +import csv +import sys +import os + +COLUMNS = ("timestamp", "level", "topic", "ip", "username", "session", "message") + +class BadArg(Exception): + pass + +def logtool(db, *, columns, output_fn, ips=None, names=None, sessions=None, + topics=None, after=None, before=None): + # Select columns + for i, col in enumerate(columns): + if col not in COLUMNS: + raise BadArg("Invalid column '%s'. Must be one of (%s)" % (col, ','.join(COLUMNS))) + + if col == 'username': + columns[i] = "extract_username(user) as username" + elif col == 'ip': + columns[i] = "extract_ip(user) as ip" + + sql_filters = ["1=1"] # just so we can always have a WHERE clause + params = [] + + # Filter by IP + if ips: + sql_filters.append("extract_ip(user) IN (%s)" % (','.join('?' * len(ips)))) + params += ips + + # Filter by user name + if names: + sql_filters.append("lower(extract_username(user)) IN (%s)" % (','.join('?' * len(names)))) + params += names + + # Filter by session ID + if sessions: + sql_filters.append("session IN (%s)" % (','.join('?' * len(sessions)))) + params += sessions + + # Filter by log topic + if topics: + sql_filters.append("topic IN (%s)" % (','.join('?' * len(topics)))) + params += topics + + # Add time range filtering + if after: + sql_filters.append("datetime(timestamp) > ?") + params.append(after) + + if before: + sql_filters.append("datetime(timestamp) < ?") + params.append(before) + + # Execute query and show results + query = "SELECT {columns} FROM serverlog WHERE {where} ORDER BY timestamp".format( + columns=','.join(columns), + where=' AND '.join(sql_filters), + ) + + rows = db.execute(query, params) + output_fn([r[0] for r in rows.description], rows) + + +# Output formatters +def print_table(header, rows): + rows = list(rows) + columns = [0] * len(header) + + for row in itertools.chain((header,), rows): + for i, col in enumerate(row[:-1]): + columns[i] = max(columns[i], len(str(col))) + + for row in itertools.chain((header,), rows): + for width, col in zip(columns, row): + val = str(col) + sys.stdout.write(val) + sys.stdout.write(' ' * (width - len(val) + 1)) + sys.stdout.write('\n') + +def print_json(header, rows): + encoder = json.JSONEncoder( + ensure_ascii=False, + check_circular=False, + ) + sys.stdout.write('[') + first = True + for row in rows: + if first: + first = False + else: + sys.stdout.write(',\n') + + sys.stdout.write(encoder.encode(dict(zip(header, row)))) + sys.stdout.write(']\n') + +def print_csv(header, rows): + writer = csv.writer(sys.stdout) + writer.writerow(header) + writer.writerows(rows) + +FORMATS = { + 'table': print_table, + 'json': print_json, + 'csv': print_csv, +} + + +def _sqlite_extract_ip(value): + if not isinstance(value, str): + return '' + + idx1 = value.index(';') + if idx1 < 0: + return '' + idx2 = value.index(';', idx1+1) + + return value[idx1+1:idx2] + +def _sqlite_extract_username(value): + if not isinstance(value, str): + return '' + + idx = value.rindex(';') + if idx < 0: + return '' + return value[idx+1:] + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("database", + help="configuration database") + parser.add_argument("--columns", type=str, default='', + help="List only selected columns") + parser.add_argument("--format", type=str, default='table', + help="Output format (%s)" % ', '.join(FORMATS.keys())) + + parser.add_argument("--ip", type=str, default='', + help="Filter by IP address (comma separated list)") + parser.add_argument("--name", type=str, default='', + help="Filter by username (case insensitive comma separated list)") + parser.add_argument("--session", type=str, default='', + help="Filter by session ID (comma separated list)") + parser.add_argument("--topic", type=str, default='', + help="Filter by topic (comma separated list)") + parser.add_argument("--after", type=str, default='', + help="Show entries after this timestamp") + parser.add_argument("--before", type=str, default='', + help="Show entries before this timestamp") + + args = parser.parse_args() + + try: + if not os.path.isfile(args.database): + raise BadArg(args.database + ": file not found!") + + if args.columns: + columns = args.columns.split(',') + else: + columns = list(COLUMNS) + + if args.format not in FORMATS: + raise BadArg("Unknown output format: '%s'" % args.format) + + conn = sqlite3.connect(args.database) + try: + conn.create_function("extract_ip", 1, _sqlite_extract_ip) + conn.create_function("extract_username", 1, _sqlite_extract_username) + + logtool(conn, + columns=columns, + output_fn=FORMATS[args.format], + ips=args.ip.split(',') if args.ip else None, + names=[name.lower() for name in args.name.split(',')] if args.name else None, + sessions=args.session.split(',') if args.session else None, + topics=args.topic.split(',') if args.topic else None, + after=args.after, + before=args.before, + ) + except BrokenPipeError: + pass + finally: + conn.close() + + except BadArg as e: + print(str(e), file=sys.stderr) + sys.exit(1) + diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 7fc41b443..f93b37608 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -1,6 +1,7 @@ find_package(Qt5Gui REQUIRED) find_package(Qt5Svg REQUIRED) find_package(Qt5LinguistTools) +find_package(Vpx) set ( SOURCES @@ -9,7 +10,7 @@ set ( tools/toolproperties.cpp tools/utils.cpp tools/annotation.cpp - tools/brushes.cpp + tools/freehand.cpp tools/colorpicker.cpp tools/laser.cpp tools/selection.cpp @@ -17,6 +18,7 @@ set ( tools/beziertool.cpp tools/floodfill.cpp tools/strokesmoother.cpp + tools/zoom.cpp canvas/statetracker.cpp canvas/canvasmodel.cpp canvas/selection.cpp @@ -57,16 +59,25 @@ set ( utils/icon.cpp utils/logging.cpp utils/passwordstore.cpp + utils/identicon.cpp + utils/avatarlistmodel.cpp + utils/sessionfilterproxymodel.cpp core/annotationmodel.cpp core/tile.cpp core/layer.cpp core/layerstack.cpp - core/brush.cpp core/brushmask.cpp core/blendmodes.cpp core/rasterop.cpp - core/shapes.cpp core/floodfill.cpp + core/tilevector.cpp + brushes/brushengine.cpp + brushes/brushpainter.cpp + brushes/classicbrushstate.cpp + brushes/classicbrushpainter.cpp + brushes/pixelbrushstate.cpp + brushes/pixelbrushpainter.cpp + brushes/shapes.cpp ora/orawriter.cpp ora/orareader.cpp recording/index.cpp @@ -77,10 +88,11 @@ set ( export/animation.cpp export/videoexporter.cpp export/imageseriesexporter.cpp - export/ffmpegexporter.cpp parentalcontrols/parentalcontrols.cpp ) +include_directories(bundled) + if(WIN32) set(SOURCES ${SOURCES} parentalcontrols/parentalcontrols_win.cpp) else() @@ -106,6 +118,16 @@ if(KF5DNSSD_FOUND) add_definitions(-DHAVE_DNSSD) endif() +if(LIBVPX_FOUND) + set( + SOURCES ${SOURCES} + bundled/mkvmuxer/mkvmuxer.cc + bundled/mkvmuxer/mkvmuxerutil.cc + export/webmexporter.cpp + export/webmencoder.cpp + ) +endif(LIBVPX_FOUND) + if( Qt5LinguistTools_FOUND) set(TRANSLATIONS i18n/drawpile_fi.ts @@ -143,6 +165,10 @@ if(LIBMINIUPNPC_FOUND) endif ( WIN32 ) endif() +if(LIBVPX_FOUND) + target_link_libraries(${DPCLIENTLIB} ${LIBVPX_LIBRARIES}) +endif(LIBVPX_FOUND) + if ( WIN32 ) target_link_libraries (${DPCLIENTLIB} ws2_32) endif () diff --git a/src/client/brushes/brush.h b/src/client/brushes/brush.h new file mode 100644 index 000000000..d4b7de40f --- /dev/null +++ b/src/client/brushes/brush.h @@ -0,0 +1,148 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2006-2019 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ +#ifndef DP_BRUSHES_BRUSH_H +#define DP_BRUSHES_BRUSH_H + +#include "../core/blendmodes.h" + +#include + +namespace brushes { + +/** + * @brief The parameters for Drawpile's classic soft and pixel brushes + * + * Pressure sensitive brush parameters come in pairs: x1 and x2, + * where 1 is the value at full pressure and 2 is the value at zero pressure. + * + * Pressure sensitive brush parameters are: + * + * - Size - the brush diameter + * - Hardness - hardness of the brush shape + * - Opacity - brush opacity + * - Smudge - smudging factor (how much color is picked up) + * + * Constant parameters are: + * + * - Resmudge - how often to sample smudge color (every resmudge dabs) + * - Spacing - dab spacing as a percentage of brush diameter + * - Subpixel mode (boolean) + * - Incremental mode (boolean) + * - Blending mode + * - Shape selection (round/square) + */ +class ClassicBrush +{ +public: + //! Construct a brush + ClassicBrush(int size=1, qreal hardness=1.0, qreal opacity=1.0, + const QColor& color=Qt::black, int spacing=25) + : m_size1(size), m_size2(size), + m_hardness1(hardness), m_hardness2(hardness), + m_opacity1(opacity), m_opacity2(opacity), + m_smudge1(0), m_smudge2(0), + m_color(color), m_spacing(spacing), m_resmudge(0), + m_blend(paintcore::BlendMode::MODE_NORMAL), + m_subpixel(false), m_incremental(true), + m_square(false) + { + } + + void setSize(int size) { Q_ASSERT(size>0); m_size1 = size; } + void setSize2(int size) { Q_ASSERT(size>0); m_size2 = size; } + + int size1() const { return m_size1; } + int size2() const { return m_size2; } + + void setHardness(qreal hardness) { Q_ASSERT(hardness>=0 && hardness<=1); m_hardness1 = hardness; } + void setHardness2(qreal hardness) { Q_ASSERT(hardness>=0 && hardness<=1); m_hardness2 = hardness; } + + qreal hardness1() const { return m_hardness1; } + qreal hardness2() const { return m_hardness2; } + + void setOpacity(qreal opacity) { Q_ASSERT(opacity>=0 && opacity<=1); m_opacity1 = opacity; } + void setOpacity2(qreal opacity) { Q_ASSERT(opacity>=0 && opacity<=1); m_opacity2 = opacity; } + + qreal opacity1() const { return m_opacity1; } + qreal opacity2() const { return m_opacity2; } + + void setColor(const QColor& color) { m_color = color; } + const QColor &color() const { return m_color; } + + void setSmudge(qreal smudge) { Q_ASSERT(smudge>=0 && smudge<=1); m_smudge1 = smudge; } + void setSmudge2(qreal smudge) { Q_ASSERT(smudge>=0 && smudge<=1); m_smudge2 = smudge; } + + qreal smudge1() const { return m_smudge1; } + qreal smudge2() const { return m_smudge2; } + + void setSpacing(int spacing) { Q_ASSERT(spacing >= 0 && spacing <= 100); m_spacing = spacing; } + int spacing() const { return m_spacing; } + + //! Set smudge colir resampling frequency (0 resamples on every dab) + void setResmudge(int resmudge) { Q_ASSERT(resmudge >= 0); m_resmudge = resmudge; } + int resmudge() const { return m_resmudge; } + + void setSubpixel(bool sp) { m_subpixel = sp; } + bool subpixel() const { return m_subpixel; } + + void setIncremental(bool incremental) { m_incremental = incremental; } + bool incremental() const { return m_incremental; } + + void setBlendingMode(paintcore::BlendMode::Mode mode) { m_blend = mode; } + paintcore::BlendMode::Mode blendingMode() const { return m_blend; } + bool isEraser() const { return m_blend == paintcore::BlendMode::MODE_ERASE; } + + void setSquare(bool square) { m_square = square; } + bool isSquare() const { return m_square; } + + qreal size(qreal pressure) const { return lerp(size1(), size2(), pressure); } + qreal hardness(qreal pressure) const { return lerp(hardness1(), hardness2(), pressure); } + qreal opacity(qreal pressure) const { return lerp(opacity1(), opacity2(), pressure); } + qreal smudge(qreal pressure) const { return lerp(smudge1(), smudge2(), pressure); } + qreal spacingDist(qreal pressure) const { return spacing() / 100.0 * size(pressure); } + + //! Does opacity vary with pressure? + bool isOpacityVariable() const { return qAbs(opacity1() - opacity2()) > (1/256.0); } + +private: + static inline qreal lerp(qreal a, qreal b, qreal alpha) { + Q_ASSERT(alpha >=0 && alpha<=1); + return (a-b) * alpha + b; + } + + int m_size1, m_size2; + qreal m_hardness1, m_hardness2; + qreal m_opacity1, m_opacity2; + qreal m_smudge1, m_smudge2; + QColor m_color; + int m_spacing; + int m_resmudge; + paintcore::BlendMode::Mode m_blend; + bool m_subpixel; + bool m_incremental; + bool m_square; +}; + +} + +Q_DECLARE_TYPEINFO(brushes::ClassicBrush, Q_MOVABLE_TYPE); + +#endif + + diff --git a/src/client/brushes/brushengine.cpp b/src/client/brushes/brushengine.cpp new file mode 100644 index 000000000..7ba743f22 --- /dev/null +++ b/src/client/brushes/brushengine.cpp @@ -0,0 +1,48 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#include "brushengine.h" + +#include "classicbrushstate.h" +#include "pixelbrushstate.h" + +namespace brushes { + +BrushEngine::BrushEngine() +{ +} + +void BrushEngine::setBrush(int contextId, int layerId, const ClassicBrush &brush) +{ + // Select brush engine to use + if(brush.subpixel()) { + m_activeEngine = &m_classic; + m_classic.setBrush(brush); + m_classic.setContextId(contextId); + m_classic.setLayer(layerId); + + } else { + m_activeEngine = &m_pixel; + m_pixel.setBrush(brush); + m_pixel.setContextId(contextId); + m_pixel.setLayer(layerId); + } +} + +} diff --git a/src/client/brushes/brushengine.h b/src/client/brushes/brushengine.h new file mode 100644 index 000000000..45101cca8 --- /dev/null +++ b/src/client/brushes/brushengine.h @@ -0,0 +1,53 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#ifndef DP_BRUSHENGINE_H +#define DP_BRUSHENGINE_H + +#include "brushstate.h" +#include "classicbrushstate.h" +#include "pixelbrushstate.h" +#include "../shared/net/message.h" + +namespace brushes { + +/** + * @brief An abstraction layer for brush engines + */ +class BrushEngine : public BrushState +{ +public: + BrushEngine(); + + void setBrush(int contextId, int layerId, const ClassicBrush &brush); + + void strokeTo(const paintcore::Point &p, const paintcore::Layer *sourceLayer) override { m_activeEngine->strokeTo(p, sourceLayer); } + void endStroke() override { m_activeEngine->endStroke(); } + QList takeDabs() override { return m_activeEngine->takeDabs(); } + +private: + BrushState *m_activeEngine; + + ClassicBrushState m_classic; + PixelBrushState m_pixel; +}; + +} + +#endif diff --git a/src/client/brushes/brushpainter.cpp b/src/client/brushes/brushpainter.cpp new file mode 100644 index 000000000..18301e74f --- /dev/null +++ b/src/client/brushes/brushpainter.cpp @@ -0,0 +1,68 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#include "brushpainter.h" +#include "classicbrushpainter.h" +#include "pixelbrushpainter.h" + +#include "core/layerstack.h" +#include "core/layer.h" +#include "../shared/net/brushes.h" + +namespace brushes { + +void drawBrushDabs(const protocol::Message &msg, paintcore::EditableLayerStack &layers) +{ + auto layer = layers.getEditableLayer(msg.layer()); + if(layer.isNull()) { + qWarning("drawBrushDabs(ctx=%d, layer=%d): no such layer", msg.contextId(), msg.layer()); + return; + } + + drawBrushDabsDirect(msg, layer); +} + +/** + * @brief Draw brush dabs onto a specific layer + * + * Typically, you should call drawBrushDabs instead. However, this function + * must be called directly when you're drawing onto a preview sublayer. + * + * @param msg brush dab message + * @param layer the layer to draw onto + */ +void drawBrushDabsDirect(const protocol::Message &msg, paintcore::EditableLayer layer, int sublayer) +{ + Q_ASSERT(!layer.isNull()); + + switch(msg.type()) { + case protocol::MSG_DRAWDABS_CLASSIC: + drawClassicBrushDabs(static_cast(msg), layer, sublayer); + break; + case protocol::MSG_DRAWDABS_PIXEL: + case protocol::MSG_DRAWDABS_PIXEL_SQUARE: + drawPixelBrushDabs(static_cast(msg), layer, sublayer); + break; + default: + qWarning("Unhandled dab type: %s", qPrintable(msg.messageName())); + } +} + +} + diff --git a/src/client/brushes/brushpainter.h b/src/client/brushes/brushpainter.h new file mode 100644 index 000000000..2414b8f10 --- /dev/null +++ b/src/client/brushes/brushpainter.h @@ -0,0 +1,58 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#ifndef BRUSHES_BRUSHPAINTER_H +#define BRUSHES_BRUSHPAINTER_H + +namespace protocol { + class Message; +} + +namespace paintcore { + class EditableLayerStack; + class EditableLayer; +} + +namespace brushes { + +/** + * @brief Draw brush dabs onto the canvas + * + * The layer is selected automatically + * + * @param msg brush dab message + * @param layers layer stack + */ +void drawBrushDabs(const protocol::Message &msg, paintcore::EditableLayerStack &layers); + +/** + * @brief Draw brush dabs onto a specific layer + * + * Typically, you should call drawBrushDabs instead. However, this function + * must be called directly when you're drawing onto a preview sublayer. + * + * @param msg brush dab message + * @param layer the layer to draw onto + * @param sublayer sublayer override + */ +void drawBrushDabsDirect(const protocol::Message &msg, paintcore::EditableLayer layer, int sublayer=0); + +} + +#endif diff --git a/src/client/brushes/brushstate.h b/src/client/brushes/brushstate.h new file mode 100644 index 000000000..a26b994d2 --- /dev/null +++ b/src/client/brushes/brushstate.h @@ -0,0 +1,62 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#ifndef DP_BRUSHSTATE_H +#define DP_BRUSHSTATE_H + +#include "../shared/net/message.h" + +namespace paintcore { + class Point; + class Layer; +} + +namespace brushes { + +/** + * @brief An abstract base class for brush engines + */ +class BrushState { +public: + virtual ~BrushState() { } + + /** + * @brief Start or continue a stroke + * @param sourceLayer layer to pick up color from (when smudging.) May be nullptr + */ + virtual void strokeTo(const paintcore::Point &p, const paintcore::Layer *sourceLayer) = 0; + + /** + * @brief End the active stroke + */ + virtual void endStroke() = 0; + + /** + * @brief Take the list of DrawDab* commands accumulated so far. + * + * This clears the dab buffer but does not end the stroke. + * + * @return + */ + virtual QList takeDabs() = 0; +}; + +} + +#endif diff --git a/src/client/brushes/classicbrushpainter.cpp b/src/client/brushes/classicbrushpainter.cpp new file mode 100644 index 000000000..5447d7aad --- /dev/null +++ b/src/client/brushes/classicbrushpainter.cpp @@ -0,0 +1,297 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2013-2019 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#include "../shared/net/brushes.h" +#include "core/brushmask.h" +#include "core/layer.h" + +#include + +#include + +namespace brushes { + +namespace { + +template T square(T x) { return x*x; } + +typedef QVector LUT; +static const int LUT_RADIUS = 128; +static QCache LUT_CACHE; + +// Generate a lookup table for Gimp style exponential brush shape +// The value at r² (where r is distance from brush center, scaled to LUT_RADIUS) is +// the opaqueness of the pixel. +static LUT makeGimpStyleBrushLUT(float hardness) +{ + qreal exponent; + if ((1.0 - hardness) < 0.0000004) + exponent = 1000000.0; + else + exponent = 0.4 / (1.0 - hardness); + + LUT lut(square(LUT_RADIUS)); + for(int i=0;i=0 && h<=100); + if(!LUT_CACHE.contains(h)) + LUT_CACHE.insert(h, new LUT(makeGimpStyleBrushLUT(hardness))); + + return *LUT_CACHE[h]; +} + +static paintcore::BrushStamp makeMask(qreal r, qreal hardness, qreal opacity) +{ + r /= 2.0; + opacity = opacity * 255; + + // generate mask + QVector data; + int diameter; + int stampOffset; + + if(r<1) { + // special case for single pixel brush + diameter=3; + stampOffset = -1; + data.resize(3*3); + data.fill(0); + data[4] = opacity; + + } else { + const LUT lut = cachedGimpStyleBrushLUT(hardness); + const float lut_scale = square((LUT_RADIUS-1) / r); + + float offset; + float fudge=1; + diameter = ceil(r*2) + 2; + + if(diameter%2==0) { + ++diameter; + offset = -1.0; + + if(r<8) + fudge = 0.9; + } else { + offset = -0.5; + } + stampOffset = -diameter/2; + + // empirically determined fudge factors to make small brushes look nice + if(r<2.5) + fudge=0.8; + + else if(r<4) + fudge=0.8; + + data.resize(square(diameter)); + uchar *ptr = data.data(); + + for(int y=0;y data(square(diameter)); + uchar *ptr = data.data(); + + for(int y=0;y1 || yfrac<0 || yfrac>1) + qWarning("offsetMask(mask, %f, %f): offset out of bounds!", xfrac, yfrac); +#endif + + const int diameter = mask.diameter(); + + const qreal kernel[] = { + xfrac*yfrac, + (1.0-xfrac)*yfrac, + xfrac*(1.0-yfrac), + (1.0-xfrac)*(1.0-yfrac) + }; +#ifndef NDEBUG + const qreal kernelsum = fabs(kernel[0]+kernel[1]+kernel[2]+kernel[3]-1.0); + if(kernelsum>0.001) + qWarning("offset kernel sum error=%f", kernelsum); +#endif + + const uchar *src = mask.data(); + + QVector data(square(diameter)); + uchar *ptr = data.data(); + +#if 0 + for(int y=-1;y0) + sublayer = dabs.contextId(); + + if(sublayer != 0) { + layer = layer.getEditableSubLayer(sublayer, blendmode, color.alpha() > 0 ? color.alpha() : 255); + layer.updateChangeBounds(dabs.bounds()); + blendmode = paintcore::BlendMode::MODE_NORMAL; + } + + int lastX = dabs.originX(); + int lastY = dabs.originY(); + for(const protocol::ClassicBrushDab &d : dabs.dabs()) { + const int nextX = lastX + d.x; + const int nextY = lastY + d.y; + const paintcore::BrushStamp bs = makeGimpStyleBrushStamp( + QPointF(nextX/4.0, nextY/4.0), + d.size/256.0, + d.hardness/255.0, + d.opacity/255.0 + ); + layer.putBrushStamp(bs, color, blendmode); + lastX = nextX; + lastY = nextY; + } +} + +} diff --git a/src/client/brushes/classicbrushpainter.h b/src/client/brushes/classicbrushpainter.h new file mode 100644 index 000000000..b3bd17528 --- /dev/null +++ b/src/client/brushes/classicbrushpainter.h @@ -0,0 +1,46 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ +#ifndef BRUSHES_CLASSICBRUSHPAINTER_H +#define BRUSHES_CLASSICBRUSHPAINTER_H + +#include + +class QPointF; + +namespace paintcore { + class EditableLayer; + struct BrushStamp; +} + +namespace protocol { + class DrawDabsClassic; +} + +namespace brushes { + +/** + * Draw brush drabs on the canvas + */ +void drawClassicBrushDabs(const protocol::DrawDabsClassic &dabs, paintcore::EditableLayer layer, int sublayer=0); + +paintcore::BrushStamp makeGimpStyleBrushStamp(const QPointF &point, qreal radius, qreal hardness, qreal opacity); + +} + +#endif diff --git a/src/client/brushes/classicbrushstate.cpp b/src/client/brushes/classicbrushstate.cpp new file mode 100644 index 000000000..f87e3e8fd --- /dev/null +++ b/src/client/brushes/classicbrushstate.cpp @@ -0,0 +1,160 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#include "classicbrushstate.h" +#include "core/layerstack.h" +#include "core/brushmask.h" +#include "core/layer.h" + +namespace brushes { + +ClassicBrushState::ClassicBrushState() + : m_contextId(0), m_layerId(0), + m_length(0), m_smudgeDistance(0), m_pendown(false), + m_lastDab(nullptr), m_lastDabX(0), m_lastDabY(0) +{ +} + +void ClassicBrushState::setBrush(const ClassicBrush &brush) +{ + m_brush = brush; + + QColor c = m_brush.color(); + if(brush.incremental()) { + c.setAlpha(0); + + } else { + // If brush alpha is nonzero, indirect drawing mode + // is used and the alpha is used as the overall transparency + // of the entire stroke. + c.setAlphaF(m_brush.opacity1()); + + m_brush.setOpacity(1.0); + m_brush.setOpacity2(brush.isOpacityVariable() ? 0.0 : 1.0); + } + m_brush.setColor(c); + m_smudgedColor = c; + + if(m_pendown) + qWarning("Brush changed mid-stroke!"); +} + +void ClassicBrushState::strokeTo(const paintcore::Point &to, const paintcore::Layer *sourceLayer) +{ + if(m_pendown) { + // Stroke in progress: draw a line + qreal dx = to.x() - m_lastPoint.x(); + qreal dy = to.y() - m_lastPoint.y(); + const qreal dist = hypot(dx, dy); + dx = dx / dist; + dy = dy / dist; + const qreal dp = (to.pressure() - m_lastPoint.pressure()) / dist; + + const qreal spacing0 = qMax(1.0, m_brush.spacingDist(m_lastPoint.pressure())); + qreal i; + if(m_length>=spacing0) + i = 0; + else if(m_length==0) + i = spacing0; + else + i = m_length; + + paintcore::Point p(m_lastPoint.x() + dx*i, m_lastPoint.y() + dy*i, qBound(0.0, m_lastPoint.pressure() + dp*i, 1.0)); + + while(i<=dist) { + const qreal spacing = qMax(1.0, m_brush.spacingDist(p.pressure())); + const qreal smudge = m_brush.smudge(p.pressure()); + + if(++m_smudgeDistance > m_brush.resmudge() && smudge>0 && sourceLayer) { + const QColor sampled = sourceLayer->colorAt(p.x(), p.y(), qRound(m_brush.size(p.pressure()))); + + const qreal a = sampled.alphaF() * smudge; + + m_smudgedColor = QColor::fromRgbF( + m_smudgedColor.redF() * (1-a) + sampled.redF() * a, + m_smudgedColor.greenF() * (1-a) + sampled.greenF() * a, + m_smudgedColor.blueF() * (1-a) + sampled.blueF() * a, + 0 + ); + m_smudgeDistance = 0; + } + + addDab(p, m_smudgedColor.rgba()); + + p.rx() += dx * spacing; + p.ry() += dy * spacing; + p.setPressure(qBound(0.0, p.pressure() + dp * spacing, 1.0)); + i += spacing; + } + m_length = i-dist; + + } else { + // Start a new stroke + m_pendown = true; + addDab(to, m_smudgedColor.rgba()); + } + + m_lastPoint = to; +} + +void ClassicBrushState::addDab(const paintcore::Point &point, quint32 color) +{ + const int x = point.x() * 4; + const int y = point.y() * 4; + + if(!m_lastDab + || m_lastDab->color() != color + || qAbs(x - m_lastDabX) > protocol::ClassicBrushDab::MAX_XY_DELTA + || qAbs(y - m_lastDabY) > protocol::ClassicBrushDab::MAX_XY_DELTA + || m_lastDab->dabs().size() >= protocol::DrawDabsClassic::MAX_DABS + ) { + m_lastDab = new protocol::DrawDabsClassic( + m_contextId, + m_layerId, + x, + y, + color, + m_brush.blendingMode() + ); + m_dabs << protocol::MessagePtr(m_lastDab); + m_lastDabX = x; + m_lastDabY = y; + } + + m_lastDab->dabs() << protocol::ClassicBrushDab { + static_cast(x - m_lastDabX), + static_cast(y - m_lastDabY), + static_cast(m_brush.size(point.pressure()) * 256), + static_cast(m_brush.hardness(point.pressure()) * 255), + static_cast(m_brush.opacity(point.pressure()) * 255) + }; + + m_lastDabX = x; + m_lastDabY = y; +} + +void ClassicBrushState::endStroke() +{ + m_pendown = false; + m_length = 0; + m_smudgeDistance = 0; + m_smudgedColor = m_brush.color(); +} + +} diff --git a/src/client/brushes/classicbrushstate.h b/src/client/brushes/classicbrushstate.h new file mode 100644 index 000000000..8543c58ea --- /dev/null +++ b/src/client/brushes/classicbrushstate.h @@ -0,0 +1,111 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ +#ifndef BRUSHES_BRUSHSTATE_H +#define BRUSHES_BRUSHSTATE_H + +#include "brush.h" +#include "brushstate.h" +#include "core/point.h" +#include "../shared/net/brushes.h" + +namespace paintcore { + class LayerStack; + class Layer; + struct BrushStamp; +} + +namespace brushes { + +/** + * @brief Drawpile's classic brush engine + * + * This class keeps track of a brush stroke's state and generates + * DrawDabs commands. + */ +class ClassicBrushState : public BrushState { +public: + ClassicBrushState(); + + /** + * @brief Set the context (user) ID + * @param id + */ + void setContextId(int id) { m_contextId = id; } + + /** + * @brief Set the brush parameters + */ + void setBrush(const brushes::ClassicBrush &brush); + + /** + * @brief Set the target layer + * @param id layer ID + */ + void setLayer(int id) { m_layerId = id; } + + /** + * @brief Start or continue a stroke + * @param sourceLayer layer to pick up color from (when smudging) + */ + void strokeTo(const paintcore::Point &p, const paintcore::Layer *sourceLayer) override; + + /** + * @brief End the active stroke + */ + void endStroke() override; + + /** + * @brief Take the current DrawDab commands + * + * This clears the dab buffer but does not end the + * stroke. + * + * @return list of DrawDab commands generated so far + */ + QList takeDabs() override { + auto dabs = m_dabs; + m_dabs = QList(); + m_lastDab = nullptr; + return dabs; + } + +private: + void addDab(const paintcore::Point &point, quint32 color); + + ClassicBrush m_brush; // the current brush + int m_contextId; // user context ID + int m_layerId; // target layer ID + qreal m_length; // current length of active brush stroke + int m_smudgeDistance; // dabs since last smudge color sampling + QColor m_smudgedColor; // effective color (nonzero alpha indicates indirect drawing mode) + bool m_pendown; // brush stroke in progress? + paintcore::Point m_lastPoint; + + QList m_dabs; + protocol::DrawDabsClassic *m_lastDab; + int m_lastDabX; + int m_lastDabY; +}; + +} + +#endif + + + diff --git a/src/client/brushes/pixelbrushpainter.cpp b/src/client/brushes/pixelbrushpainter.cpp new file mode 100644 index 000000000..52f07683b --- /dev/null +++ b/src/client/brushes/pixelbrushpainter.cpp @@ -0,0 +1,101 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2013-2019 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#include "../shared/net/brushes.h" +#include "core/brushmask.h" +#include "core/layer.h" + +namespace brushes { + +template static T square(T x) { return x*x; } + +static paintcore::BrushMask makeRoundPixelBrushMask(int diameter, uchar opacity) +{ + const qreal radius = diameter/2.0; + const qreal rr = square(radius); + + QVector data(square(diameter), 0); + uchar *ptr = data.data(); + + const qreal offset = 0.5; + for(int y=0;y(square(diameter), opacity)); +} + +void drawPixelBrushDabs(const protocol::DrawDabsPixel &dabs, paintcore::EditableLayer layer, int sublayer) +{ + if(dabs.dabs().isEmpty()) { + qWarning("drawPixelBrushDabs(ctx=%d, layer=%d): empty dab vector!", dabs.contextId(), dabs.layer()); + return; + } + + auto blendmode = paintcore::BlendMode::Mode(dabs.mode()); + const QColor color = QColor::fromRgba(dabs.color()); + + if(sublayer == 0 && color.alpha()>0) + sublayer = dabs.contextId(); + + if(sublayer != 0) { + layer = layer.getEditableSubLayer(sublayer, blendmode, color.alpha() > 0 ? color.alpha() : 255); + layer.updateChangeBounds(dabs.bounds()); + blendmode = paintcore::BlendMode::MODE_NORMAL; + } + + paintcore::BrushMask mask; + int lastSize = -1, lastOpacity = 0; + + int lastX = dabs.originX(); + int lastY = dabs.originY(); + for(const protocol::PixelBrushDab &d : dabs.dabs()) { + const int nextX = lastX + d.x; + const int nextY = lastY + d.y; + + if(d.size != lastSize|| d.opacity != lastOpacity) { + // The mask is often reusable + mask = dabs.isSquare() ? makeSquarePixelBrushMask(d.size, d.opacity) : makeRoundPixelBrushMask(d.size, d.opacity); + lastSize = d.size; + lastOpacity = d.opacity; + } + + const int offset = d.size/2; + layer.putBrushStamp( + paintcore::BrushStamp { nextX-offset, nextY-offset, mask }, + color, + blendmode + ); + + lastX = nextX; + lastY = nextY; + } +} + +} diff --git a/src/client/brushes/pixelbrushpainter.h b/src/client/brushes/pixelbrushpainter.h new file mode 100644 index 000000000..fc41627fc --- /dev/null +++ b/src/client/brushes/pixelbrushpainter.h @@ -0,0 +1,39 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ +#ifndef BRUSHES_PIXELBRUSHPAINTER_H +#define BRUSHES_PIXELBRUSHPAINTER_H + +namespace paintcore { + class Layer; +} + +namespace protocol { + class DrawDabsPixel; +} + +namespace brushes { + +/** + * Draw brush drabs on the canvas + */ +void drawPixelBrushDabs(const protocol::DrawDabsPixel &dabs, paintcore::EditableLayer layer, int sublayer=0); + +} + +#endif diff --git a/src/client/brushes/pixelbrushstate.cpp b/src/client/brushes/pixelbrushstate.cpp new file mode 100644 index 000000000..768044a7b --- /dev/null +++ b/src/client/brushes/pixelbrushstate.cpp @@ -0,0 +1,176 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#include "pixelbrushstate.h" +#include "core/layerstack.h" +#include "core/brushmask.h" +#include "core/layer.h" + +#include + +namespace brushes { + +PixelBrushState::PixelBrushState() + : m_contextId(0), m_layerId(0), + m_length(0), m_pendown(false), + m_lastDab(nullptr), m_lastDabX(0), m_lastDabY(0) +{ +} + +void PixelBrushState::setBrush(const ClassicBrush &brush) +{ + m_brush = brush; + + QColor c = m_brush.color(); + if(brush.incremental()) { + c.setAlpha(0); + + } else { + // If brush alpha is nonzero, indirect drawing mode + // is used and the alpha is used as the overall transparency + // of the entire stroke. + c.setAlphaF(m_brush.opacity1()); + + m_brush.setOpacity(1.0); + m_brush.setOpacity2(brush.isOpacityVariable() ? 0.0 : 1.0); + } + m_brush.setColor(c); + + if(m_pendown) + qWarning("Brush changed mid-stroke!"); +} + +void PixelBrushState::strokeTo(const paintcore::Point &to, const paintcore::Layer *) +{ + if(m_pendown) { + // Stroke in progress: draw a line + + const qreal dp = (to.pressure()-m_lastPoint.pressure()) / hypot(to.x()-m_lastPoint.x(), to.y()-m_lastPoint.y()); + + int x0 = qFloor(m_lastPoint.x()); + int y0 = qFloor(m_lastPoint.y()); + qreal p = m_lastPoint.pressure(); + int x1 = qFloor(to.x()); + int y1 = qFloor(to.y()); + int dy = y1 - y0; + int dx = x1 - x0; + int stepx, stepy; + + if (dy < 0) { + dy = -dy; + stepy = -1; + } else { + stepy = 1; + } + if (dx < 0) { + dx = -dx; + stepx = -1; + } else { + stepx = 1; + } + + dy *= 2; + dx *= 2; + + qreal distance = m_length; + + if (dx > dy) { + int fraction = dy - (dx >> 1); + while (x0 != x1) { + const qreal spacing = m_brush.spacingDist(p); + if (fraction >= 0) { + y0 += stepy; + fraction -= dx; + } + x0 += stepx; + fraction += dy; + if(++distance >= spacing) { + addDab(x0, y0, p); + distance = 0; + } + p += dp; + } + } else { + int fraction = dx - (dy >> 1); + while (y0 != y1) { + const qreal spacing = m_brush.spacingDist(p); + if (fraction >= 0) { + x0 += stepx; + fraction -= dy; + } + y0 += stepy; + fraction += dx; + if(++distance >= spacing) { + addDab(x0, y0, p); + distance = 0; + } + p += dp; + } + } + + m_length = distance; + + } else { + // Start a new stroke + m_pendown = true; + addDab(to.x(), to.y(), to.pressure()); + } + + m_lastPoint = to; +} + +void PixelBrushState::addDab(int x, int y, qreal pressure) +{ + if(!m_lastDab + || qAbs(x - m_lastDabX) > protocol::PixelBrushDab::MAX_XY_DELTA + || qAbs(y - m_lastDabY) > protocol::PixelBrushDab::MAX_XY_DELTA + || m_lastDab->dabs().size() >= protocol::DrawDabsPixel::MAX_DABS + ) { + m_lastDab = new protocol::DrawDabsPixel( + m_brush.isSquare() ? protocol::DabShape::Square : protocol::DabShape::Round, + m_contextId, + m_layerId, + x, + y, + m_brush.color().rgba(), + m_brush.blendingMode() + ); + m_dabs << protocol::MessagePtr(m_lastDab); + m_lastDabX = x; + m_lastDabY = y; + } + + m_lastDab->dabs() << protocol::PixelBrushDab { + static_cast(x - m_lastDabX), + static_cast(y - m_lastDabY), + static_cast(m_brush.size(pressure)), + static_cast(m_brush.opacity(pressure) * 255) + }; + + m_lastDabX = x; + m_lastDabY = y; +} + +void PixelBrushState::endStroke() +{ + m_pendown = false; + m_length = 0; +} + +} diff --git a/src/client/brushes/pixelbrushstate.h b/src/client/brushes/pixelbrushstate.h new file mode 100644 index 000000000..b41341812 --- /dev/null +++ b/src/client/brushes/pixelbrushstate.h @@ -0,0 +1,106 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ +#ifndef BRUSHES_PIXELBRUSHSTATE_H +#define BRUSHES_PIXELBRUSHSTATE_H + +#include "brush.h" +#include "brushstate.h" +#include "core/point.h" +#include "../shared/net/brushes.h" + +namespace paintcore { + class LayerStack; + class Layer; + struct BrushStamp; +} + +namespace brushes { + +/** + * @brief Pixel brush engine + * + * This class keeps track of a brush stroke's state and generates + * DrawDabs commands. + */ +class PixelBrushState : public BrushState { +public: + PixelBrushState(); + + /** + * @brief Set the context (user) ID + * @param id + */ + void setContextId(int id) { m_contextId = id; } + + /** + * @brief Set the brush parameters + */ + void setBrush(const ClassicBrush &brush); + + /** + * @brief Set the target layer + * @param id layer ID + */ + void setLayer(int id) { m_layerId = id; } + + /** + * @brief Start or continue a stroke + * @param sourceLayer unused + */ + void strokeTo(const paintcore::Point &p, const paintcore::Layer *) override; + + /** + * @brief End the active stroke + */ + void endStroke() override; + + /** + * @brief Take the current DrawDab commands + * + * This clears the dab buffer but does not end the + * stroke. + * + * @return list of DrawDab commands generated so far + */ + QList takeDabs() override { + auto dabs = m_dabs; + m_dabs = QList(); + m_lastDab = nullptr; + return dabs; + } + +private: + void addDab(int x, int y, qreal pressure); + + ClassicBrush m_brush; // the current brush + int m_contextId; // user context ID + int m_layerId; // target layer ID + qreal m_length; // current length of active brush stroke + bool m_pendown; // brush stroke in progress? + paintcore::Point m_lastPoint; + + QList m_dabs; + protocol::DrawDabsPixel *m_lastDab; + int m_lastDabX; + int m_lastDabY; +}; + +} + +#endif diff --git a/src/client/core/shapes.cpp b/src/client/brushes/shapes.cpp similarity index 93% rename from src/client/core/shapes.cpp rename to src/client/brushes/shapes.cpp index 3555d3113..a5e840227 100644 --- a/src/client/core/shapes.cpp +++ b/src/client/brushes/shapes.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2014 Calle Laakkonen + Copyright (C) 2014-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,13 +19,15 @@ #include "shapes.h" -#include -#include +#include #include -namespace paintcore{ +namespace brushes { namespace shapes { +using paintcore::Point; +using paintcore::PointVector; + PointVector rectangle(const QRectF &rect) { const QPointF p1 = rect.topLeft(); @@ -53,7 +55,7 @@ PointVector ellipse(const QRectF &rect) // TODO smart step size selection for(qreal t=0;t<2*M_PI;t+=M_PI/20) { - pv << Point(cx + a*qCos(t), cy + b*qSin(t), 1.0); + pv << Point(cx + a*cos(t), cy + b*sin(t), 1.0); } pv << Point(cx+a, cy, 1); return pv; @@ -102,7 +104,7 @@ PointVector sampleStroke(const QRectF &rect) const qreal fx = x/qreal(strokew); const qreal pressure = qBound(0.0, ((fx*fx) - (fx*fx*fx))*6.756, 1.0); - const qreal y = qSin(phase) * strokeh; + const qreal y = sin(phase) * strokeh; pointvector << Point(rect.left()+x, offy+y, pressure); } return pointvector; diff --git a/src/client/core/shapes.h b/src/client/brushes/shapes.h similarity index 64% rename from src/client/core/shapes.h rename to src/client/brushes/shapes.h index 707b22fec..3750ca963 100644 --- a/src/client/core/shapes.h +++ b/src/client/brushes/shapes.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2014 Calle Laakkonen + Copyright (C) 2014-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,23 +17,24 @@ along with Drawpile. If not, see . */ -#ifndef DP_PAINTCORE_SHAPES_H -#define DP_PAINTCORE_SHAPES_H +#ifndef DP_BRUSHES_SHAPES_H +#define DP_BRUSHES_SHAPES_H + +#include "core/point.h" #include -#include "point.h" -namespace paintcore { +namespace brushes { namespace shapes { -PointVector rectangle(const QRectF &rect); -PointVector ellipse(const QRectF &rect); +paintcore::PointVector rectangle(const QRectF &rect); +paintcore::PointVector ellipse(const QRectF &rect); -PointVector cubicBezierCurve(const QPointF p[4]); +paintcore::PointVector cubicBezierCurve(const QPointF p[4]); // These are used for brush previews -PointVector sampleStroke(const QRectF &rect); -PointVector sampleBlob(const QRectF &rect); +paintcore::PointVector sampleStroke(const QRectF &rect); +paintcore::PointVector sampleBlob(const QRectF &rect); } } diff --git a/src/client/bundled/LICENSE.mkvmuxer b/src/client/bundled/LICENSE.mkvmuxer new file mode 100644 index 000000000..7a6f99547 --- /dev/null +++ b/src/client/bundled/LICENSE.mkvmuxer @@ -0,0 +1,30 @@ +Copyright (c) 2010, Google Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Google nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/src/client/bundled/README b/src/client/bundled/README new file mode 100644 index 000000000..ec4139e81 --- /dev/null +++ b/src/client/bundled/README @@ -0,0 +1,11 @@ +# mkvmuxer + +Extracted from libwebm version 1.0.0.27 +If a prepackaged libwebm is available, it can be substituted for this bundled version. + +Modifications: + + - Removed chunking (unused feature, removes dependency on mkvparser and MkvWriter) + - Removed Segmet::CopyAndMoveCuesBeforeClusters (unused feature, removed dependency on mkvparser) + - Removed unneeded `#include "mkvwriter.h"` from mkvmuxerutil.cc + diff --git a/src/client/bundled/mkvmuxer/common/webmids.h b/src/client/bundled/mkvmuxer/common/webmids.h new file mode 100644 index 000000000..32a0c5fb9 --- /dev/null +++ b/src/client/bundled/mkvmuxer/common/webmids.h @@ -0,0 +1,184 @@ +// Copyright (c) 2012 The WebM project authors. All Rights Reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. An additional intellectual property rights grant can be found +// in the file PATENTS. All contributing project authors may +// be found in the AUTHORS file in the root of the source tree. + +#ifndef COMMON_WEBMIDS_H_ +#define COMMON_WEBMIDS_H_ + +namespace libwebm { + +enum MkvId { + kMkvEBML = 0x1A45DFA3, + kMkvEBMLVersion = 0x4286, + kMkvEBMLReadVersion = 0x42F7, + kMkvEBMLMaxIDLength = 0x42F2, + kMkvEBMLMaxSizeLength = 0x42F3, + kMkvDocType = 0x4282, + kMkvDocTypeVersion = 0x4287, + kMkvDocTypeReadVersion = 0x4285, + kMkvVoid = 0xEC, + kMkvSignatureSlot = 0x1B538667, + kMkvSignatureAlgo = 0x7E8A, + kMkvSignatureHash = 0x7E9A, + kMkvSignaturePublicKey = 0x7EA5, + kMkvSignature = 0x7EB5, + kMkvSignatureElements = 0x7E5B, + kMkvSignatureElementList = 0x7E7B, + kMkvSignedElement = 0x6532, + // segment + kMkvSegment = 0x18538067, + // Meta Seek Information + kMkvSeekHead = 0x114D9B74, + kMkvSeek = 0x4DBB, + kMkvSeekID = 0x53AB, + kMkvSeekPosition = 0x53AC, + // Segment Information + kMkvInfo = 0x1549A966, + kMkvTimecodeScale = 0x2AD7B1, + kMkvDuration = 0x4489, + kMkvDateUTC = 0x4461, + kMkvTitle = 0x7BA9, + kMkvMuxingApp = 0x4D80, + kMkvWritingApp = 0x5741, + // Cluster + kMkvCluster = 0x1F43B675, + kMkvTimecode = 0xE7, + kMkvPrevSize = 0xAB, + kMkvBlockGroup = 0xA0, + kMkvBlock = 0xA1, + kMkvBlockDuration = 0x9B, + kMkvReferenceBlock = 0xFB, + kMkvLaceNumber = 0xCC, + kMkvSimpleBlock = 0xA3, + kMkvBlockAdditions = 0x75A1, + kMkvBlockMore = 0xA6, + kMkvBlockAddID = 0xEE, + kMkvBlockAdditional = 0xA5, + kMkvDiscardPadding = 0x75A2, + // Track + kMkvTracks = 0x1654AE6B, + kMkvTrackEntry = 0xAE, + kMkvTrackNumber = 0xD7, + kMkvTrackUID = 0x73C5, + kMkvTrackType = 0x83, + kMkvFlagEnabled = 0xB9, + kMkvFlagDefault = 0x88, + kMkvFlagForced = 0x55AA, + kMkvFlagLacing = 0x9C, + kMkvDefaultDuration = 0x23E383, + kMkvMaxBlockAdditionID = 0x55EE, + kMkvName = 0x536E, + kMkvLanguage = 0x22B59C, + kMkvCodecID = 0x86, + kMkvCodecPrivate = 0x63A2, + kMkvCodecName = 0x258688, + kMkvCodecDelay = 0x56AA, + kMkvSeekPreRoll = 0x56BB, + // video + kMkvVideo = 0xE0, + kMkvFlagInterlaced = 0x9A, + kMkvStereoMode = 0x53B8, + kMkvAlphaMode = 0x53C0, + kMkvPixelWidth = 0xB0, + kMkvPixelHeight = 0xBA, + kMkvPixelCropBottom = 0x54AA, + kMkvPixelCropTop = 0x54BB, + kMkvPixelCropLeft = 0x54CC, + kMkvPixelCropRight = 0x54DD, + kMkvDisplayWidth = 0x54B0, + kMkvDisplayHeight = 0x54BA, + kMkvDisplayUnit = 0x54B2, + kMkvAspectRatioType = 0x54B3, + kMkvFrameRate = 0x2383E3, + // end video + // colour + kMkvColour = 0x55B0, + kMkvMatrixCoefficients = 0x55B1, + kMkvBitsPerChannel = 0x55B2, + kMkvChromaSubsamplingHorz = 0x55B3, + kMkvChromaSubsamplingVert = 0x55B4, + kMkvCbSubsamplingHorz = 0x55B5, + kMkvCbSubsamplingVert = 0x55B6, + kMkvChromaSitingHorz = 0x55B7, + kMkvChromaSitingVert = 0x55B8, + kMkvRange = 0x55B9, + kMkvTransferCharacteristics = 0x55BA, + kMkvPrimaries = 0x55BB, + kMkvMaxCLL = 0x55BC, + kMkvMaxFALL = 0x55BD, + // mastering metadata + kMkvMasteringMetadata = 0x55D0, + kMkvPrimaryRChromaticityX = 0x55D1, + kMkvPrimaryRChromaticityY = 0x55D2, + kMkvPrimaryGChromaticityX = 0x55D3, + kMkvPrimaryGChromaticityY = 0x55D4, + kMkvPrimaryBChromaticityX = 0x55D5, + kMkvPrimaryBChromaticityY = 0x55D6, + kMkvWhitePointChromaticityX = 0x55D7, + kMkvWhitePointChromaticityY = 0x55D8, + kMkvLuminanceMax = 0x55D9, + kMkvLuminanceMin = 0x55DA, + // end mastering metadata + // end colour + // audio + kMkvAudio = 0xE1, + kMkvSamplingFrequency = 0xB5, + kMkvOutputSamplingFrequency = 0x78B5, + kMkvChannels = 0x9F, + kMkvBitDepth = 0x6264, + // end audio + // ContentEncodings + kMkvContentEncodings = 0x6D80, + kMkvContentEncoding = 0x6240, + kMkvContentEncodingOrder = 0x5031, + kMkvContentEncodingScope = 0x5032, + kMkvContentEncodingType = 0x5033, + kMkvContentCompression = 0x5034, + kMkvContentCompAlgo = 0x4254, + kMkvContentCompSettings = 0x4255, + kMkvContentEncryption = 0x5035, + kMkvContentEncAlgo = 0x47E1, + kMkvContentEncKeyID = 0x47E2, + kMkvContentSignature = 0x47E3, + kMkvContentSigKeyID = 0x47E4, + kMkvContentSigAlgo = 0x47E5, + kMkvContentSigHashAlgo = 0x47E6, + kMkvContentEncAESSettings = 0x47E7, + kMkvAESSettingsCipherMode = 0x47E8, + kMkvAESSettingsCipherInitData = 0x47E9, + // end ContentEncodings + // Cueing Data + kMkvCues = 0x1C53BB6B, + kMkvCuePoint = 0xBB, + kMkvCueTime = 0xB3, + kMkvCueTrackPositions = 0xB7, + kMkvCueTrack = 0xF7, + kMkvCueClusterPosition = 0xF1, + kMkvCueBlockNumber = 0x5378, + // Chapters + kMkvChapters = 0x1043A770, + kMkvEditionEntry = 0x45B9, + kMkvChapterAtom = 0xB6, + kMkvChapterUID = 0x73C4, + kMkvChapterStringUID = 0x5654, + kMkvChapterTimeStart = 0x91, + kMkvChapterTimeEnd = 0x92, + kMkvChapterDisplay = 0x80, + kMkvChapString = 0x85, + kMkvChapLanguage = 0x437C, + kMkvChapCountry = 0x437E, + // Tags + kMkvTags = 0x1254C367, + kMkvTag = 0x7373, + kMkvSimpleTag = 0x67C8, + kMkvTagName = 0x45A3, + kMkvTagString = 0x4487 +}; + +} // namespace libwebm + +#endif // COMMON_WEBMIDS_H_ diff --git a/src/client/bundled/mkvmuxer/mkvmuxer.cc b/src/client/bundled/mkvmuxer/mkvmuxer.cc new file mode 100644 index 000000000..06c890727 --- /dev/null +++ b/src/client/bundled/mkvmuxer/mkvmuxer.cc @@ -0,0 +1,3682 @@ +// Copyright (c) 2012 The WebM project authors. All Rights Reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. An additional intellectual property rights grant can be found +// in the file PATENTS. All contributing project authors may +// be found in the AUTHORS file in the root of the source tree. + +#include "mkvmuxer/mkvmuxer.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/webmids.h" +#include "mkvmuxer/mkvmuxerutil.h" + +namespace mkvmuxer { + +const float MasteringMetadata::kValueNotPresent = FLT_MAX; +const uint64_t Colour::kValueNotPresent = UINT64_MAX; + +namespace { + +const char kDocTypeWebm[] = "webm"; +const char kDocTypeMatroska[] = "matroska"; + +// Deallocate the string designated by |dst|, and then copy the |src| +// string to |dst|. The caller owns both the |src| string and the +// |dst| copy (hence the caller is responsible for eventually +// deallocating the strings, either directly, or indirectly via +// StrCpy). Returns true if the source string was successfully copied +// to the destination. +bool StrCpy(const char* src, char** dst_ptr) { + if (dst_ptr == NULL) + return false; + + char*& dst = *dst_ptr; + + delete[] dst; + dst = NULL; + + if (src == NULL) + return true; + + const size_t size = strlen(src) + 1; + + dst = new (std::nothrow) char[size]; // NOLINT + if (dst == NULL) + return false; + + strcpy(dst, src); // NOLINT + return true; +} + +typedef std::auto_ptr PrimaryChromaticityPtr; +bool CopyChromaticity(const PrimaryChromaticity* src, + PrimaryChromaticityPtr* dst) { + if (!dst) + return false; + + dst->reset(new (std::nothrow) PrimaryChromaticity(src->x, src->y)); + if (!dst->get()) + return false; + + return true; +} + +} // namespace + +/////////////////////////////////////////////////////////////// +// +// IMkvWriter Class + +IMkvWriter::IMkvWriter() {} + +IMkvWriter::~IMkvWriter() {} + +bool WriteEbmlHeader(IMkvWriter* writer, uint64_t doc_type_version, + const char* const doc_type) { + // Level 0 + uint64_t size = + EbmlElementSize(libwebm::kMkvEBMLVersion, static_cast(1)); + size += EbmlElementSize(libwebm::kMkvEBMLReadVersion, static_cast(1)); + size += EbmlElementSize(libwebm::kMkvEBMLMaxIDLength, static_cast(4)); + size += + EbmlElementSize(libwebm::kMkvEBMLMaxSizeLength, static_cast(8)); + size += EbmlElementSize(libwebm::kMkvDocType, doc_type); + size += EbmlElementSize(libwebm::kMkvDocTypeVersion, + static_cast(doc_type_version)); + size += + EbmlElementSize(libwebm::kMkvDocTypeReadVersion, static_cast(2)); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvEBML, size)) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvEBMLVersion, + static_cast(1))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvEBMLReadVersion, + static_cast(1))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvEBMLMaxIDLength, + static_cast(4))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvEBMLMaxSizeLength, + static_cast(8))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvDocType, doc_type)) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvDocTypeVersion, + static_cast(doc_type_version))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvDocTypeReadVersion, + static_cast(2))) { + return false; + } + + return true; +} + +bool WriteEbmlHeader(IMkvWriter* writer, uint64_t doc_type_version) { + return WriteEbmlHeader(writer, doc_type_version, kDocTypeWebm); +} + +bool WriteEbmlHeader(IMkvWriter* writer) { + return WriteEbmlHeader(writer, mkvmuxer::Segment::kDefaultDocTypeVersion); +} + +/////////////////////////////////////////////////////////////// +// +// Frame Class + +Frame::Frame() + : add_id_(0), + additional_(NULL), + additional_length_(0), + duration_(0), + duration_set_(false), + frame_(NULL), + is_key_(false), + length_(0), + track_number_(0), + timestamp_(0), + discard_padding_(0), + reference_block_timestamp_(0), + reference_block_timestamp_set_(false) {} + +Frame::~Frame() { + delete[] frame_; + delete[] additional_; +} + +bool Frame::CopyFrom(const Frame& frame) { + delete[] frame_; + frame_ = NULL; + length_ = 0; + if (frame.length() > 0 && frame.frame() != NULL && + !Init(frame.frame(), frame.length())) { + return false; + } + add_id_ = 0; + delete[] additional_; + additional_ = NULL; + additional_length_ = 0; + if (frame.additional_length() > 0 && frame.additional() != NULL && + !AddAdditionalData(frame.additional(), frame.additional_length(), + frame.add_id())) { + return false; + } + duration_ = frame.duration(); + duration_set_ = frame.duration_set(); + is_key_ = frame.is_key(); + track_number_ = frame.track_number(); + timestamp_ = frame.timestamp(); + discard_padding_ = frame.discard_padding(); + reference_block_timestamp_ = frame.reference_block_timestamp(); + reference_block_timestamp_set_ = frame.reference_block_timestamp_set(); + return true; +} + +bool Frame::Init(const uint8_t* frame, uint64_t length) { + uint8_t* const data = + new (std::nothrow) uint8_t[static_cast(length)]; // NOLINT + if (!data) + return false; + + delete[] frame_; + frame_ = data; + length_ = length; + + memcpy(frame_, frame, static_cast(length_)); + return true; +} + +bool Frame::AddAdditionalData(const uint8_t* additional, uint64_t length, + uint64_t add_id) { + uint8_t* const data = + new (std::nothrow) uint8_t[static_cast(length)]; // NOLINT + if (!data) + return false; + + delete[] additional_; + additional_ = data; + additional_length_ = length; + add_id_ = add_id; + + memcpy(additional_, additional, static_cast(additional_length_)); + return true; +} + +bool Frame::IsValid() const { + if (length_ == 0 || !frame_) { + return false; + } + if ((additional_length_ != 0 && !additional_) || + (additional_ != NULL && additional_length_ == 0)) { + return false; + } + if (track_number_ == 0 || track_number_ > kMaxTrackNumber) { + return false; + } + if (!CanBeSimpleBlock() && !is_key_ && !reference_block_timestamp_set_) { + return false; + } + return true; +} + +bool Frame::CanBeSimpleBlock() const { + return additional_ == NULL && discard_padding_ == 0 && duration_ == 0; +} + +void Frame::set_duration(uint64_t duration) { + duration_ = duration; + duration_set_ = true; +} + +void Frame::set_reference_block_timestamp(int64_t reference_block_timestamp) { + reference_block_timestamp_ = reference_block_timestamp; + reference_block_timestamp_set_ = true; +} + +/////////////////////////////////////////////////////////////// +// +// CuePoint Class + +CuePoint::CuePoint() + : time_(0), + track_(0), + cluster_pos_(0), + block_number_(1), + output_block_number_(true) {} + +CuePoint::~CuePoint() {} + +bool CuePoint::Write(IMkvWriter* writer) const { + if (!writer || track_ < 1 || cluster_pos_ < 1) + return false; + + uint64_t size = EbmlElementSize(libwebm::kMkvCueClusterPosition, + static_cast(cluster_pos_)); + size += EbmlElementSize(libwebm::kMkvCueTrack, static_cast(track_)); + if (output_block_number_ && block_number_ > 1) + size += EbmlElementSize(libwebm::kMkvCueBlockNumber, + static_cast(block_number_)); + const uint64_t track_pos_size = + EbmlMasterElementSize(libwebm::kMkvCueTrackPositions, size) + size; + const uint64_t payload_size = + EbmlElementSize(libwebm::kMkvCueTime, static_cast(time_)) + + track_pos_size; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvCuePoint, payload_size)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvCueTime, + static_cast(time_))) { + return false; + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvCueTrackPositions, size)) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvCueTrack, + static_cast(track_))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvCueClusterPosition, + static_cast(cluster_pos_))) { + return false; + } + if (output_block_number_ && block_number_ > 1) { + if (!WriteEbmlElement(writer, libwebm::kMkvCueBlockNumber, + static_cast(block_number_))) { + return false; + } + } + + const int64_t stop_position = writer->Position(); + if (stop_position < 0) + return false; + + if (stop_position - payload_position != static_cast(payload_size)) + return false; + + return true; +} + +uint64_t CuePoint::PayloadSize() const { + uint64_t size = EbmlElementSize(libwebm::kMkvCueClusterPosition, + static_cast(cluster_pos_)); + size += EbmlElementSize(libwebm::kMkvCueTrack, static_cast(track_)); + if (output_block_number_ && block_number_ > 1) + size += EbmlElementSize(libwebm::kMkvCueBlockNumber, + static_cast(block_number_)); + const uint64_t track_pos_size = + EbmlMasterElementSize(libwebm::kMkvCueTrackPositions, size) + size; + const uint64_t payload_size = + EbmlElementSize(libwebm::kMkvCueTime, static_cast(time_)) + + track_pos_size; + + return payload_size; +} + +uint64_t CuePoint::Size() const { + const uint64_t payload_size = PayloadSize(); + return EbmlMasterElementSize(libwebm::kMkvCuePoint, payload_size) + + payload_size; +} + +/////////////////////////////////////////////////////////////// +// +// Cues Class + +Cues::Cues() + : cue_entries_capacity_(0), + cue_entries_size_(0), + cue_entries_(NULL), + output_block_number_(true) {} + +Cues::~Cues() { + if (cue_entries_) { + for (int32_t i = 0; i < cue_entries_size_; ++i) { + CuePoint* const cue = cue_entries_[i]; + delete cue; + } + delete[] cue_entries_; + } +} + +bool Cues::AddCue(CuePoint* cue) { + if (!cue) + return false; + + if ((cue_entries_size_ + 1) > cue_entries_capacity_) { + // Add more CuePoints. + const int32_t new_capacity = + (!cue_entries_capacity_) ? 2 : cue_entries_capacity_ * 2; + + if (new_capacity < 1) + return false; + + CuePoint** const cues = + new (std::nothrow) CuePoint*[new_capacity]; // NOLINT + if (!cues) + return false; + + for (int32_t i = 0; i < cue_entries_size_; ++i) { + cues[i] = cue_entries_[i]; + } + + delete[] cue_entries_; + + cue_entries_ = cues; + cue_entries_capacity_ = new_capacity; + } + + cue->set_output_block_number(output_block_number_); + cue_entries_[cue_entries_size_++] = cue; + return true; +} + +CuePoint* Cues::GetCueByIndex(int32_t index) const { + if (cue_entries_ == NULL) + return NULL; + + if (index >= cue_entries_size_) + return NULL; + + return cue_entries_[index]; +} + +uint64_t Cues::Size() { + uint64_t size = 0; + for (int32_t i = 0; i < cue_entries_size_; ++i) + size += GetCueByIndex(i)->Size(); + size += EbmlMasterElementSize(libwebm::kMkvCues, size); + return size; +} + +bool Cues::Write(IMkvWriter* writer) const { + if (!writer) + return false; + + uint64_t size = 0; + for (int32_t i = 0; i < cue_entries_size_; ++i) { + const CuePoint* const cue = GetCueByIndex(i); + + if (!cue) + return false; + + size += cue->Size(); + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvCues, size)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + for (int32_t i = 0; i < cue_entries_size_; ++i) { + const CuePoint* const cue = GetCueByIndex(i); + + if (!cue->Write(writer)) + return false; + } + + const int64_t stop_position = writer->Position(); + if (stop_position < 0) + return false; + + if (stop_position - payload_position != static_cast(size)) + return false; + + return true; +} + +/////////////////////////////////////////////////////////////// +// +// ContentEncAESSettings Class + +ContentEncAESSettings::ContentEncAESSettings() : cipher_mode_(kCTR) {} + +uint64_t ContentEncAESSettings::Size() const { + const uint64_t payload = PayloadSize(); + const uint64_t size = + EbmlMasterElementSize(libwebm::kMkvContentEncAESSettings, payload) + + payload; + return size; +} + +bool ContentEncAESSettings::Write(IMkvWriter* writer) const { + const uint64_t payload = PayloadSize(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvContentEncAESSettings, + payload)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvAESSettingsCipherMode, + static_cast(cipher_mode_))) { + return false; + } + + const int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(payload)) + return false; + + return true; +} + +uint64_t ContentEncAESSettings::PayloadSize() const { + uint64_t size = EbmlElementSize(libwebm::kMkvAESSettingsCipherMode, + static_cast(cipher_mode_)); + return size; +} + +/////////////////////////////////////////////////////////////// +// +// ContentEncoding Class + +ContentEncoding::ContentEncoding() + : enc_algo_(5), + enc_key_id_(NULL), + encoding_order_(0), + encoding_scope_(1), + encoding_type_(1), + enc_key_id_length_(0) {} + +ContentEncoding::~ContentEncoding() { delete[] enc_key_id_; } + +bool ContentEncoding::SetEncryptionID(const uint8_t* id, uint64_t length) { + if (!id || length < 1) + return false; + + delete[] enc_key_id_; + + enc_key_id_ = + new (std::nothrow) uint8_t[static_cast(length)]; // NOLINT + if (!enc_key_id_) + return false; + + memcpy(enc_key_id_, id, static_cast(length)); + enc_key_id_length_ = length; + + return true; +} + +uint64_t ContentEncoding::Size() const { + const uint64_t encryption_size = EncryptionSize(); + const uint64_t encoding_size = EncodingSize(0, encryption_size); + const uint64_t encodings_size = + EbmlMasterElementSize(libwebm::kMkvContentEncoding, encoding_size) + + encoding_size; + + return encodings_size; +} + +bool ContentEncoding::Write(IMkvWriter* writer) const { + const uint64_t encryption_size = EncryptionSize(); + const uint64_t encoding_size = EncodingSize(0, encryption_size); + const uint64_t size = + EbmlMasterElementSize(libwebm::kMkvContentEncoding, encoding_size) + + encoding_size; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvContentEncoding, + encoding_size)) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvContentEncodingOrder, + static_cast(encoding_order_))) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvContentEncodingScope, + static_cast(encoding_scope_))) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvContentEncodingType, + static_cast(encoding_type_))) + return false; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvContentEncryption, + encryption_size)) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvContentEncAlgo, + static_cast(enc_algo_))) { + return false; + } + if (!WriteEbmlElement(writer, libwebm::kMkvContentEncKeyID, enc_key_id_, + enc_key_id_length_)) + return false; + + if (!enc_aes_settings_.Write(writer)) + return false; + + const int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(size)) + return false; + + return true; +} + +uint64_t ContentEncoding::EncodingSize(uint64_t compresion_size, + uint64_t encryption_size) const { + // TODO(fgalligan): Add support for compression settings. + if (compresion_size != 0) + return 0; + + uint64_t encoding_size = 0; + + if (encryption_size > 0) { + encoding_size += + EbmlMasterElementSize(libwebm::kMkvContentEncryption, encryption_size) + + encryption_size; + } + encoding_size += EbmlElementSize(libwebm::kMkvContentEncodingType, + static_cast(encoding_type_)); + encoding_size += EbmlElementSize(libwebm::kMkvContentEncodingScope, + static_cast(encoding_scope_)); + encoding_size += EbmlElementSize(libwebm::kMkvContentEncodingOrder, + static_cast(encoding_order_)); + + return encoding_size; +} + +uint64_t ContentEncoding::EncryptionSize() const { + const uint64_t aes_size = enc_aes_settings_.Size(); + + uint64_t encryption_size = EbmlElementSize(libwebm::kMkvContentEncKeyID, + enc_key_id_, enc_key_id_length_); + encryption_size += EbmlElementSize(libwebm::kMkvContentEncAlgo, + static_cast(enc_algo_)); + + return encryption_size + aes_size; +} + +/////////////////////////////////////////////////////////////// +// +// Track Class + +Track::Track(unsigned int* seed) + : codec_id_(NULL), + codec_private_(NULL), + language_(NULL), + max_block_additional_id_(0), + name_(NULL), + number_(0), + type_(0), + uid_(MakeUID(seed)), + codec_delay_(0), + seek_pre_roll_(0), + default_duration_(0), + codec_private_length_(0), + content_encoding_entries_(NULL), + content_encoding_entries_size_(0) {} + +Track::~Track() { + delete[] codec_id_; + delete[] codec_private_; + delete[] language_; + delete[] name_; + + if (content_encoding_entries_) { + for (uint32_t i = 0; i < content_encoding_entries_size_; ++i) { + ContentEncoding* const encoding = content_encoding_entries_[i]; + delete encoding; + } + delete[] content_encoding_entries_; + } +} + +bool Track::AddContentEncoding() { + const uint32_t count = content_encoding_entries_size_ + 1; + + ContentEncoding** const content_encoding_entries = + new (std::nothrow) ContentEncoding*[count]; // NOLINT + if (!content_encoding_entries) + return false; + + ContentEncoding* const content_encoding = + new (std::nothrow) ContentEncoding(); // NOLINT + if (!content_encoding) { + delete[] content_encoding_entries; + return false; + } + + for (uint32_t i = 0; i < content_encoding_entries_size_; ++i) { + content_encoding_entries[i] = content_encoding_entries_[i]; + } + + delete[] content_encoding_entries_; + + content_encoding_entries_ = content_encoding_entries; + content_encoding_entries_[content_encoding_entries_size_] = content_encoding; + content_encoding_entries_size_ = count; + return true; +} + +ContentEncoding* Track::GetContentEncodingByIndex(uint32_t index) const { + if (content_encoding_entries_ == NULL) + return NULL; + + if (index >= content_encoding_entries_size_) + return NULL; + + return content_encoding_entries_[index]; +} + +uint64_t Track::PayloadSize() const { + uint64_t size = + EbmlElementSize(libwebm::kMkvTrackNumber, static_cast(number_)); + size += EbmlElementSize(libwebm::kMkvTrackUID, static_cast(uid_)); + size += EbmlElementSize(libwebm::kMkvTrackType, static_cast(type_)); + if (codec_id_) + size += EbmlElementSize(libwebm::kMkvCodecID, codec_id_); + if (codec_private_) + size += EbmlElementSize(libwebm::kMkvCodecPrivate, codec_private_, + codec_private_length_); + if (language_) + size += EbmlElementSize(libwebm::kMkvLanguage, language_); + if (name_) + size += EbmlElementSize(libwebm::kMkvName, name_); + if (max_block_additional_id_) { + size += EbmlElementSize(libwebm::kMkvMaxBlockAdditionID, + static_cast(max_block_additional_id_)); + } + if (codec_delay_) { + size += EbmlElementSize(libwebm::kMkvCodecDelay, + static_cast(codec_delay_)); + } + if (seek_pre_roll_) { + size += EbmlElementSize(libwebm::kMkvSeekPreRoll, + static_cast(seek_pre_roll_)); + } + if (default_duration_) { + size += EbmlElementSize(libwebm::kMkvDefaultDuration, + static_cast(default_duration_)); + } + + if (content_encoding_entries_size_ > 0) { + uint64_t content_encodings_size = 0; + for (uint32_t i = 0; i < content_encoding_entries_size_; ++i) { + ContentEncoding* const encoding = content_encoding_entries_[i]; + content_encodings_size += encoding->Size(); + } + + size += EbmlMasterElementSize(libwebm::kMkvContentEncodings, + content_encodings_size) + + content_encodings_size; + } + + return size; +} + +uint64_t Track::Size() const { + uint64_t size = PayloadSize(); + size += EbmlMasterElementSize(libwebm::kMkvTrackEntry, size); + return size; +} + +bool Track::Write(IMkvWriter* writer) const { + if (!writer) + return false; + + // mandatory elements without a default value. + if (!type_ || !codec_id_) + return false; + + // |size| may be bigger than what is written out in this function because + // derived classes may write out more data in the Track element. + const uint64_t payload_size = PayloadSize(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvTrackEntry, payload_size)) + return false; + + uint64_t size = + EbmlElementSize(libwebm::kMkvTrackNumber, static_cast(number_)); + size += EbmlElementSize(libwebm::kMkvTrackUID, static_cast(uid_)); + size += EbmlElementSize(libwebm::kMkvTrackType, static_cast(type_)); + if (codec_id_) + size += EbmlElementSize(libwebm::kMkvCodecID, codec_id_); + if (codec_private_) + size += EbmlElementSize(libwebm::kMkvCodecPrivate, codec_private_, + static_cast(codec_private_length_)); + if (language_) + size += EbmlElementSize(libwebm::kMkvLanguage, language_); + if (name_) + size += EbmlElementSize(libwebm::kMkvName, name_); + if (max_block_additional_id_) + size += EbmlElementSize(libwebm::kMkvMaxBlockAdditionID, + static_cast(max_block_additional_id_)); + if (codec_delay_) + size += EbmlElementSize(libwebm::kMkvCodecDelay, + static_cast(codec_delay_)); + if (seek_pre_roll_) + size += EbmlElementSize(libwebm::kMkvSeekPreRoll, + static_cast(seek_pre_roll_)); + if (default_duration_) + size += EbmlElementSize(libwebm::kMkvDefaultDuration, + static_cast(default_duration_)); + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvTrackNumber, + static_cast(number_))) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvTrackUID, + static_cast(uid_))) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvTrackType, + static_cast(type_))) + return false; + if (max_block_additional_id_) { + if (!WriteEbmlElement(writer, libwebm::kMkvMaxBlockAdditionID, + static_cast(max_block_additional_id_))) { + return false; + } + } + if (codec_delay_) { + if (!WriteEbmlElement(writer, libwebm::kMkvCodecDelay, + static_cast(codec_delay_))) + return false; + } + if (seek_pre_roll_) { + if (!WriteEbmlElement(writer, libwebm::kMkvSeekPreRoll, + static_cast(seek_pre_roll_))) + return false; + } + if (default_duration_) { + if (!WriteEbmlElement(writer, libwebm::kMkvDefaultDuration, + static_cast(default_duration_))) + return false; + } + if (codec_id_) { + if (!WriteEbmlElement(writer, libwebm::kMkvCodecID, codec_id_)) + return false; + } + if (codec_private_) { + if (!WriteEbmlElement(writer, libwebm::kMkvCodecPrivate, codec_private_, + static_cast(codec_private_length_))) + return false; + } + if (language_) { + if (!WriteEbmlElement(writer, libwebm::kMkvLanguage, language_)) + return false; + } + if (name_) { + if (!WriteEbmlElement(writer, libwebm::kMkvName, name_)) + return false; + } + + int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(size)) + return false; + + if (content_encoding_entries_size_ > 0) { + uint64_t content_encodings_size = 0; + for (uint32_t i = 0; i < content_encoding_entries_size_; ++i) { + ContentEncoding* const encoding = content_encoding_entries_[i]; + content_encodings_size += encoding->Size(); + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvContentEncodings, + content_encodings_size)) + return false; + + for (uint32_t i = 0; i < content_encoding_entries_size_; ++i) { + ContentEncoding* const encoding = content_encoding_entries_[i]; + if (!encoding->Write(writer)) + return false; + } + } + + stop_position = writer->Position(); + if (stop_position < 0) + return false; + return true; +} + +bool Track::SetCodecPrivate(const uint8_t* codec_private, uint64_t length) { + if (!codec_private || length < 1) + return false; + + delete[] codec_private_; + + codec_private_ = + new (std::nothrow) uint8_t[static_cast(length)]; // NOLINT + if (!codec_private_) + return false; + + memcpy(codec_private_, codec_private, static_cast(length)); + codec_private_length_ = length; + + return true; +} + +void Track::set_codec_id(const char* codec_id) { + if (codec_id) { + delete[] codec_id_; + + const size_t length = strlen(codec_id) + 1; + codec_id_ = new (std::nothrow) char[length]; // NOLINT + if (codec_id_) { +#ifdef _MSC_VER + strcpy_s(codec_id_, length, codec_id); +#else + strcpy(codec_id_, codec_id); +#endif + } + } +} + +// TODO(fgalligan): Vet the language parameter. +void Track::set_language(const char* language) { + if (language) { + delete[] language_; + + const size_t length = strlen(language) + 1; + language_ = new (std::nothrow) char[length]; // NOLINT + if (language_) { +#ifdef _MSC_VER + strcpy_s(language_, length, language); +#else + strcpy(language_, language); +#endif + } + } +} + +void Track::set_name(const char* name) { + if (name) { + delete[] name_; + + const size_t length = strlen(name) + 1; + name_ = new (std::nothrow) char[length]; // NOLINT + if (name_) { +#ifdef _MSC_VER + strcpy_s(name_, length, name); +#else + strcpy(name_, name); +#endif + } + } +} + +/////////////////////////////////////////////////////////////// +// +// Colour and its child elements + +uint64_t PrimaryChromaticity::PrimaryChromaticityPayloadSize( + libwebm::MkvId x_id, libwebm::MkvId y_id) const { + return EbmlElementSize(x_id, x) + EbmlElementSize(y_id, y); +} + +bool PrimaryChromaticity::Write(IMkvWriter* writer, libwebm::MkvId x_id, + libwebm::MkvId y_id) const { + return WriteEbmlElement(writer, x_id, x) && WriteEbmlElement(writer, y_id, y); +} + +uint64_t MasteringMetadata::MasteringMetadataSize() const { + uint64_t size = PayloadSize(); + + if (size > 0) + size += EbmlMasterElementSize(libwebm::kMkvMasteringMetadata, size); + + return size; +} + +bool MasteringMetadata::Write(IMkvWriter* writer) const { + const uint64_t size = PayloadSize(); + + // Don't write an empty element. + if (size == 0) + return true; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvMasteringMetadata, size)) + return false; + if (luminance_max != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvLuminanceMax, luminance_max)) { + return false; + } + if (luminance_min != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvLuminanceMin, luminance_min)) { + return false; + } + if (r_ && + !r_->Write(writer, libwebm::kMkvPrimaryRChromaticityX, + libwebm::kMkvPrimaryRChromaticityY)) { + return false; + } + if (g_ && + !g_->Write(writer, libwebm::kMkvPrimaryGChromaticityX, + libwebm::kMkvPrimaryGChromaticityY)) { + return false; + } + if (b_ && + !b_->Write(writer, libwebm::kMkvPrimaryBChromaticityX, + libwebm::kMkvPrimaryBChromaticityY)) { + return false; + } + if (white_point_ && + !white_point_->Write(writer, libwebm::kMkvWhitePointChromaticityX, + libwebm::kMkvWhitePointChromaticityY)) { + return false; + } + + return true; +} + +bool MasteringMetadata::SetChromaticity( + const PrimaryChromaticity* r, const PrimaryChromaticity* g, + const PrimaryChromaticity* b, const PrimaryChromaticity* white_point) { + PrimaryChromaticityPtr r_ptr(NULL); + if (r) { + if (!CopyChromaticity(r, &r_ptr)) + return false; + } + PrimaryChromaticityPtr g_ptr(NULL); + if (g) { + if (!CopyChromaticity(g, &g_ptr)) + return false; + } + PrimaryChromaticityPtr b_ptr(NULL); + if (b) { + if (!CopyChromaticity(b, &b_ptr)) + return false; + } + PrimaryChromaticityPtr wp_ptr(NULL); + if (white_point) { + if (!CopyChromaticity(white_point, &wp_ptr)) + return false; + } + + r_ = r_ptr.release(); + g_ = g_ptr.release(); + b_ = b_ptr.release(); + white_point_ = wp_ptr.release(); + return true; +} + +uint64_t MasteringMetadata::PayloadSize() const { + uint64_t size = 0; + + if (luminance_max != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvLuminanceMax, luminance_max); + if (luminance_min != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvLuminanceMin, luminance_min); + + if (r_) { + size += r_->PrimaryChromaticityPayloadSize( + libwebm::kMkvPrimaryRChromaticityX, libwebm::kMkvPrimaryRChromaticityY); + } + if (g_) { + size += g_->PrimaryChromaticityPayloadSize( + libwebm::kMkvPrimaryGChromaticityX, libwebm::kMkvPrimaryGChromaticityY); + } + if (b_) { + size += b_->PrimaryChromaticityPayloadSize( + libwebm::kMkvPrimaryBChromaticityX, libwebm::kMkvPrimaryBChromaticityY); + } + if (white_point_) { + size += white_point_->PrimaryChromaticityPayloadSize( + libwebm::kMkvWhitePointChromaticityX, + libwebm::kMkvWhitePointChromaticityY); + } + + return size; +} + +uint64_t Colour::ColourSize() const { + uint64_t size = PayloadSize(); + + if (size > 0) + size += EbmlMasterElementSize(libwebm::kMkvColour, size); + + return size; +} + +bool Colour::Write(IMkvWriter* writer) const { + const uint64_t size = PayloadSize(); + + // Don't write an empty element. + if (size == 0) + return true; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvColour, size)) + return false; + + if (matrix_coefficients != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvMatrixCoefficients, + static_cast(matrix_coefficients))) { + return false; + } + if (bits_per_channel != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvBitsPerChannel, + static_cast(bits_per_channel))) { + return false; + } + if (chroma_subsampling_horz != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvChromaSubsamplingHorz, + static_cast(chroma_subsampling_horz))) { + return false; + } + if (chroma_subsampling_vert != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvChromaSubsamplingVert, + static_cast(chroma_subsampling_vert))) { + return false; + } + + if (cb_subsampling_horz != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvCbSubsamplingHorz, + static_cast(cb_subsampling_horz))) { + return false; + } + if (cb_subsampling_vert != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvCbSubsamplingVert, + static_cast(cb_subsampling_vert))) { + return false; + } + if (chroma_siting_horz != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvChromaSitingHorz, + static_cast(chroma_siting_horz))) { + return false; + } + if (chroma_siting_vert != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvChromaSitingVert, + static_cast(chroma_siting_vert))) { + return false; + } + if (range != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvRange, + static_cast(range))) { + return false; + } + if (transfer_characteristics != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvTransferCharacteristics, + static_cast(transfer_characteristics))) { + return false; + } + if (primaries != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvPrimaries, + static_cast(primaries))) { + return false; + } + if (max_cll != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvMaxCLL, + static_cast(max_cll))) { + return false; + } + if (max_fall != kValueNotPresent && + !WriteEbmlElement(writer, libwebm::kMkvMaxFALL, + static_cast(max_fall))) { + return false; + } + + if (mastering_metadata_ && !mastering_metadata_->Write(writer)) + return false; + + return true; +} + +bool Colour::SetMasteringMetadata(const MasteringMetadata& mastering_metadata) { + std::unique_ptr mm_ptr(new MasteringMetadata()); + if (!mm_ptr.get()) + return false; + + mm_ptr->luminance_max = mastering_metadata.luminance_max; + mm_ptr->luminance_min = mastering_metadata.luminance_min; + + if (!mm_ptr->SetChromaticity(mastering_metadata.r(), mastering_metadata.g(), + mastering_metadata.b(), + mastering_metadata.white_point())) { + return false; + } + + delete mastering_metadata_; + mastering_metadata_ = mm_ptr.release(); + return true; +} + +uint64_t Colour::PayloadSize() const { + uint64_t size = 0; + + if (matrix_coefficients != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvMatrixCoefficients, + static_cast(matrix_coefficients)); + if (bits_per_channel != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvBitsPerChannel, + static_cast(bits_per_channel)); + if (chroma_subsampling_horz != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvChromaSubsamplingHorz, + static_cast(chroma_subsampling_horz)); + if (chroma_subsampling_vert != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvChromaSubsamplingVert, + static_cast(chroma_subsampling_vert)); + if (cb_subsampling_horz != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvCbSubsamplingHorz, + static_cast(cb_subsampling_horz)); + if (cb_subsampling_vert != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvCbSubsamplingVert, + static_cast(cb_subsampling_vert)); + if (chroma_siting_horz != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvChromaSitingHorz, + static_cast(chroma_siting_horz)); + if (chroma_siting_vert != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvChromaSitingVert, + static_cast(chroma_siting_vert)); + if (range != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvRange, static_cast(range)); + if (transfer_characteristics != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvTransferCharacteristics, + static_cast(transfer_characteristics)); + if (primaries != kValueNotPresent) + size += + EbmlElementSize(libwebm::kMkvPrimaries, static_cast(primaries)); + if (max_cll != kValueNotPresent) + size += EbmlElementSize(libwebm::kMkvMaxCLL, static_cast(max_cll)); + if (max_fall != kValueNotPresent) + size += + EbmlElementSize(libwebm::kMkvMaxFALL, static_cast(max_fall)); + + if (mastering_metadata_) + size += mastering_metadata_->MasteringMetadataSize(); + + return size; +} + +/////////////////////////////////////////////////////////////// +// +// VideoTrack Class + +VideoTrack::VideoTrack(unsigned int* seed) + : Track(seed), + display_height_(0), + display_width_(0), + crop_left_(0), + crop_right_(0), + crop_top_(0), + crop_bottom_(0), + frame_rate_(0.0), + height_(0), + stereo_mode_(0), + alpha_mode_(0), + width_(0), + colour_(NULL) {} + +VideoTrack::~VideoTrack() { delete colour_; } + +bool VideoTrack::SetStereoMode(uint64_t stereo_mode) { + if (stereo_mode != kMono && stereo_mode != kSideBySideLeftIsFirst && + stereo_mode != kTopBottomRightIsFirst && + stereo_mode != kTopBottomLeftIsFirst && + stereo_mode != kSideBySideRightIsFirst) + return false; + + stereo_mode_ = stereo_mode; + return true; +} + +bool VideoTrack::SetAlphaMode(uint64_t alpha_mode) { + if (alpha_mode != kNoAlpha && alpha_mode != kAlpha) + return false; + + alpha_mode_ = alpha_mode; + return true; +} + +uint64_t VideoTrack::PayloadSize() const { + const uint64_t parent_size = Track::PayloadSize(); + + uint64_t size = VideoPayloadSize(); + size += EbmlMasterElementSize(libwebm::kMkvVideo, size); + + return parent_size + size; +} + +bool VideoTrack::Write(IMkvWriter* writer) const { + if (!Track::Write(writer)) + return false; + + const uint64_t size = VideoPayloadSize(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvVideo, size)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvPixelWidth, + static_cast(width_))) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvPixelHeight, + static_cast(height_))) + return false; + if (display_width_ > 0) { + if (!WriteEbmlElement(writer, libwebm::kMkvDisplayWidth, + static_cast(display_width_))) + return false; + } + if (display_height_ > 0) { + if (!WriteEbmlElement(writer, libwebm::kMkvDisplayHeight, + static_cast(display_height_))) + return false; + } + if (crop_left_ > 0) { + if (!WriteEbmlElement(writer, libwebm::kMkvPixelCropLeft, + static_cast(crop_left_))) + return false; + } + if (crop_right_ > 0) { + if (!WriteEbmlElement(writer, libwebm::kMkvPixelCropRight, + static_cast(crop_right_))) + return false; + } + if (crop_top_ > 0) { + if (!WriteEbmlElement(writer, libwebm::kMkvPixelCropTop, + static_cast(crop_top_))) + return false; + } + if (crop_bottom_ > 0) { + if (!WriteEbmlElement(writer, libwebm::kMkvPixelCropBottom, + static_cast(crop_bottom_))) + return false; + } + if (stereo_mode_ > kMono) { + if (!WriteEbmlElement(writer, libwebm::kMkvStereoMode, + static_cast(stereo_mode_))) + return false; + } + if (alpha_mode_ > kNoAlpha) { + if (!WriteEbmlElement(writer, libwebm::kMkvAlphaMode, + static_cast(alpha_mode_))) + return false; + } + if (frame_rate_ > 0.0) { + if (!WriteEbmlElement(writer, libwebm::kMkvFrameRate, + static_cast(frame_rate_))) { + return false; + } + } + if (colour_) { + if (!colour_->Write(writer)) + return false; + } + + const int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(size)) { + return false; + } + + return true; +} + +bool VideoTrack::SetColour(const Colour& colour) { + std::unique_ptr colour_ptr(new Colour()); + if (!colour_ptr.get()) + return false; + + if (colour.mastering_metadata()) { + if (!colour_ptr->SetMasteringMetadata(*colour.mastering_metadata())) + return false; + } + + colour_ptr->matrix_coefficients = colour.matrix_coefficients; + colour_ptr->bits_per_channel = colour.bits_per_channel; + colour_ptr->chroma_subsampling_horz = colour.chroma_subsampling_horz; + colour_ptr->chroma_subsampling_vert = colour.chroma_subsampling_vert; + colour_ptr->cb_subsampling_horz = colour.cb_subsampling_horz; + colour_ptr->cb_subsampling_vert = colour.cb_subsampling_vert; + colour_ptr->chroma_siting_horz = colour.chroma_siting_horz; + colour_ptr->chroma_siting_vert = colour.chroma_siting_vert; + colour_ptr->range = colour.range; + colour_ptr->transfer_characteristics = colour.transfer_characteristics; + colour_ptr->primaries = colour.primaries; + colour_ptr->max_cll = colour.max_cll; + colour_ptr->max_fall = colour.max_fall; + colour_ = colour_ptr.release(); + return true; +} + +uint64_t VideoTrack::VideoPayloadSize() const { + uint64_t size = + EbmlElementSize(libwebm::kMkvPixelWidth, static_cast(width_)); + size += + EbmlElementSize(libwebm::kMkvPixelHeight, static_cast(height_)); + if (display_width_ > 0) + size += EbmlElementSize(libwebm::kMkvDisplayWidth, + static_cast(display_width_)); + if (display_height_ > 0) + size += EbmlElementSize(libwebm::kMkvDisplayHeight, + static_cast(display_height_)); + if (crop_left_ > 0) + size += EbmlElementSize(libwebm::kMkvPixelCropLeft, + static_cast(crop_left_)); + if (crop_right_ > 0) + size += EbmlElementSize(libwebm::kMkvPixelCropRight, + static_cast(crop_right_)); + if (crop_top_ > 0) + size += EbmlElementSize(libwebm::kMkvPixelCropTop, + static_cast(crop_top_)); + if (crop_bottom_ > 0) + size += EbmlElementSize(libwebm::kMkvPixelCropBottom, + static_cast(crop_bottom_)); + if (stereo_mode_ > kMono) + size += EbmlElementSize(libwebm::kMkvStereoMode, + static_cast(stereo_mode_)); + if (alpha_mode_ > kNoAlpha) + size += EbmlElementSize(libwebm::kMkvAlphaMode, + static_cast(alpha_mode_)); + if (frame_rate_ > 0.0) + size += EbmlElementSize(libwebm::kMkvFrameRate, + static_cast(frame_rate_)); + if (colour_) + size += colour_->ColourSize(); + + return size; +} + +/////////////////////////////////////////////////////////////// +// +// AudioTrack Class + +AudioTrack::AudioTrack(unsigned int* seed) + : Track(seed), bit_depth_(0), channels_(1), sample_rate_(0.0) {} + +AudioTrack::~AudioTrack() {} + +uint64_t AudioTrack::PayloadSize() const { + const uint64_t parent_size = Track::PayloadSize(); + + uint64_t size = EbmlElementSize(libwebm::kMkvSamplingFrequency, + static_cast(sample_rate_)); + size += + EbmlElementSize(libwebm::kMkvChannels, static_cast(channels_)); + if (bit_depth_ > 0) + size += + EbmlElementSize(libwebm::kMkvBitDepth, static_cast(bit_depth_)); + size += EbmlMasterElementSize(libwebm::kMkvAudio, size); + + return parent_size + size; +} + +bool AudioTrack::Write(IMkvWriter* writer) const { + if (!Track::Write(writer)) + return false; + + // Calculate AudioSettings size. + uint64_t size = EbmlElementSize(libwebm::kMkvSamplingFrequency, + static_cast(sample_rate_)); + size += + EbmlElementSize(libwebm::kMkvChannels, static_cast(channels_)); + if (bit_depth_ > 0) + size += + EbmlElementSize(libwebm::kMkvBitDepth, static_cast(bit_depth_)); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvAudio, size)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvSamplingFrequency, + static_cast(sample_rate_))) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvChannels, + static_cast(channels_))) + return false; + if (bit_depth_ > 0) + if (!WriteEbmlElement(writer, libwebm::kMkvBitDepth, + static_cast(bit_depth_))) + return false; + + const int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(size)) + return false; + + return true; +} + +/////////////////////////////////////////////////////////////// +// +// Tracks Class + +const char Tracks::kOpusCodecId[] = "A_OPUS"; +const char Tracks::kVorbisCodecId[] = "A_VORBIS"; +const char Tracks::kVp8CodecId[] = "V_VP8"; +const char Tracks::kVp9CodecId[] = "V_VP9"; +const char Tracks::kVp10CodecId[] = "V_VP10"; +const char Tracks::kWebVttCaptionsId[] = "D_WEBVTT/CAPTIONS"; +const char Tracks::kWebVttDescriptionsId[] = "D_WEBVTT/DESCRIPTIONS"; +const char Tracks::kWebVttMetadataId[] = "D_WEBVTT/METADATA"; +const char Tracks::kWebVttSubtitlesId[] = "D_WEBVTT/SUBTITLES"; + +Tracks::Tracks() + : track_entries_(NULL), track_entries_size_(0), wrote_tracks_(false) {} + +Tracks::~Tracks() { + if (track_entries_) { + for (uint32_t i = 0; i < track_entries_size_; ++i) { + Track* const track = track_entries_[i]; + delete track; + } + delete[] track_entries_; + } +} + +bool Tracks::AddTrack(Track* track, int32_t number) { + if (number < 0 || wrote_tracks_) + return false; + + // This muxer only supports track numbers in the range [1, 126], in + // order to be able (to use Matroska integer representation) to + // serialize the block header (of which the track number is a part) + // for a frame using exactly 4 bytes. + + if (number > 0x7E) + return false; + + uint32_t track_num = number; + + if (track_num > 0) { + // Check to make sure a track does not already have |track_num|. + for (uint32_t i = 0; i < track_entries_size_; ++i) { + if (track_entries_[i]->number() == track_num) + return false; + } + } + + const uint32_t count = track_entries_size_ + 1; + + Track** const track_entries = new (std::nothrow) Track*[count]; // NOLINT + if (!track_entries) + return false; + + for (uint32_t i = 0; i < track_entries_size_; ++i) { + track_entries[i] = track_entries_[i]; + } + + delete[] track_entries_; + + // Find the lowest availible track number > 0. + if (track_num == 0) { + track_num = count; + + // Check to make sure a track does not already have |track_num|. + bool exit = false; + do { + exit = true; + for (uint32_t i = 0; i < track_entries_size_; ++i) { + if (track_entries[i]->number() == track_num) { + track_num++; + exit = false; + break; + } + } + } while (!exit); + } + track->set_number(track_num); + + track_entries_ = track_entries; + track_entries_[track_entries_size_] = track; + track_entries_size_ = count; + return true; +} + +const Track* Tracks::GetTrackByIndex(uint32_t index) const { + if (track_entries_ == NULL) + return NULL; + + if (index >= track_entries_size_) + return NULL; + + return track_entries_[index]; +} + +Track* Tracks::GetTrackByNumber(uint64_t track_number) const { + const int32_t count = track_entries_size(); + for (int32_t i = 0; i < count; ++i) { + if (track_entries_[i]->number() == track_number) + return track_entries_[i]; + } + + return NULL; +} + +bool Tracks::TrackIsAudio(uint64_t track_number) const { + const Track* const track = GetTrackByNumber(track_number); + + if (track->type() == kAudio) + return true; + + return false; +} + +bool Tracks::TrackIsVideo(uint64_t track_number) const { + const Track* const track = GetTrackByNumber(track_number); + + if (track->type() == kVideo) + return true; + + return false; +} + +bool Tracks::Write(IMkvWriter* writer) const { + uint64_t size = 0; + const int32_t count = track_entries_size(); + for (int32_t i = 0; i < count; ++i) { + const Track* const track = GetTrackByIndex(i); + + if (!track) + return false; + + size += track->Size(); + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvTracks, size)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + for (int32_t i = 0; i < count; ++i) { + const Track* const track = GetTrackByIndex(i); + if (!track->Write(writer)) + return false; + } + + const int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(size)) + return false; + + wrote_tracks_ = true; + return true; +} + +/////////////////////////////////////////////////////////////// +// +// Chapter Class + +bool Chapter::set_id(const char* id) { return StrCpy(id, &id_); } + +void Chapter::set_time(const Segment& segment, uint64_t start_ns, + uint64_t end_ns) { + const SegmentInfo* const info = segment.GetSegmentInfo(); + const uint64_t timecode_scale = info->timecode_scale(); + start_timecode_ = start_ns / timecode_scale; + end_timecode_ = end_ns / timecode_scale; +} + +bool Chapter::add_string(const char* title, const char* language, + const char* country) { + if (!ExpandDisplaysArray()) + return false; + + Display& d = displays_[displays_count_++]; + d.Init(); + + if (!d.set_title(title)) + return false; + + if (!d.set_language(language)) + return false; + + if (!d.set_country(country)) + return false; + + return true; +} + +Chapter::Chapter() { + // This ctor only constructs the object. Proper initialization is + // done in Init() (called in Chapters::AddChapter()). The only + // reason we bother implementing this ctor is because we had to + // declare it as private (along with the dtor), in order to prevent + // clients from creating Chapter instances (a privelege we grant + // only to the Chapters class). Doing no initialization here also + // means that creating arrays of chapter objects is more efficient, + // because we only initialize each new chapter object as it becomes + // active on the array. +} + +Chapter::~Chapter() {} + +void Chapter::Init(unsigned int* seed) { + id_ = NULL; + start_timecode_ = 0; + end_timecode_ = 0; + displays_ = NULL; + displays_size_ = 0; + displays_count_ = 0; + uid_ = MakeUID(seed); +} + +void Chapter::ShallowCopy(Chapter* dst) const { + dst->id_ = id_; + dst->start_timecode_ = start_timecode_; + dst->end_timecode_ = end_timecode_; + dst->uid_ = uid_; + dst->displays_ = displays_; + dst->displays_size_ = displays_size_; + dst->displays_count_ = displays_count_; +} + +void Chapter::Clear() { + StrCpy(NULL, &id_); + + while (displays_count_ > 0) { + Display& d = displays_[--displays_count_]; + d.Clear(); + } + + delete[] displays_; + displays_ = NULL; + + displays_size_ = 0; +} + +bool Chapter::ExpandDisplaysArray() { + if (displays_size_ > displays_count_) + return true; // nothing to do yet + + const int size = (displays_size_ == 0) ? 1 : 2 * displays_size_; + + Display* const displays = new (std::nothrow) Display[size]; // NOLINT + if (displays == NULL) + return false; + + for (int idx = 0; idx < displays_count_; ++idx) { + displays[idx] = displays_[idx]; // shallow copy + } + + delete[] displays_; + + displays_ = displays; + displays_size_ = size; + + return true; +} + +uint64_t Chapter::WriteAtom(IMkvWriter* writer) const { + uint64_t payload_size = + EbmlElementSize(libwebm::kMkvChapterStringUID, id_) + + EbmlElementSize(libwebm::kMkvChapterUID, static_cast(uid_)) + + EbmlElementSize(libwebm::kMkvChapterTimeStart, + static_cast(start_timecode_)) + + EbmlElementSize(libwebm::kMkvChapterTimeEnd, + static_cast(end_timecode_)); + + for (int idx = 0; idx < displays_count_; ++idx) { + const Display& d = displays_[idx]; + payload_size += d.WriteDisplay(NULL); + } + + const uint64_t atom_size = + EbmlMasterElementSize(libwebm::kMkvChapterAtom, payload_size) + + payload_size; + + if (writer == NULL) + return atom_size; + + const int64_t start = writer->Position(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvChapterAtom, payload_size)) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvChapterStringUID, id_)) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvChapterUID, + static_cast(uid_))) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvChapterTimeStart, + static_cast(start_timecode_))) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvChapterTimeEnd, + static_cast(end_timecode_))) + return 0; + + for (int idx = 0; idx < displays_count_; ++idx) { + const Display& d = displays_[idx]; + + if (!d.WriteDisplay(writer)) + return 0; + } + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != atom_size) + return 0; + + return atom_size; +} + +void Chapter::Display::Init() { + title_ = NULL; + language_ = NULL; + country_ = NULL; +} + +void Chapter::Display::Clear() { + StrCpy(NULL, &title_); + StrCpy(NULL, &language_); + StrCpy(NULL, &country_); +} + +bool Chapter::Display::set_title(const char* title) { + return StrCpy(title, &title_); +} + +bool Chapter::Display::set_language(const char* language) { + return StrCpy(language, &language_); +} + +bool Chapter::Display::set_country(const char* country) { + return StrCpy(country, &country_); +} + +uint64_t Chapter::Display::WriteDisplay(IMkvWriter* writer) const { + uint64_t payload_size = EbmlElementSize(libwebm::kMkvChapString, title_); + + if (language_) + payload_size += EbmlElementSize(libwebm::kMkvChapLanguage, language_); + + if (country_) + payload_size += EbmlElementSize(libwebm::kMkvChapCountry, country_); + + const uint64_t display_size = + EbmlMasterElementSize(libwebm::kMkvChapterDisplay, payload_size) + + payload_size; + + if (writer == NULL) + return display_size; + + const int64_t start = writer->Position(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvChapterDisplay, + payload_size)) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvChapString, title_)) + return 0; + + if (language_) { + if (!WriteEbmlElement(writer, libwebm::kMkvChapLanguage, language_)) + return 0; + } + + if (country_) { + if (!WriteEbmlElement(writer, libwebm::kMkvChapCountry, country_)) + return 0; + } + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != display_size) + return 0; + + return display_size; +} + +/////////////////////////////////////////////////////////////// +// +// Chapters Class + +Chapters::Chapters() : chapters_size_(0), chapters_count_(0), chapters_(NULL) {} + +Chapters::~Chapters() { + while (chapters_count_ > 0) { + Chapter& chapter = chapters_[--chapters_count_]; + chapter.Clear(); + } + + delete[] chapters_; + chapters_ = NULL; +} + +int Chapters::Count() const { return chapters_count_; } + +Chapter* Chapters::AddChapter(unsigned int* seed) { + if (!ExpandChaptersArray()) + return NULL; + + Chapter& chapter = chapters_[chapters_count_++]; + chapter.Init(seed); + + return &chapter; +} + +bool Chapters::Write(IMkvWriter* writer) const { + if (writer == NULL) + return false; + + const uint64_t payload_size = WriteEdition(NULL); // return size only + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvChapters, payload_size)) + return false; + + const int64_t start = writer->Position(); + + if (WriteEdition(writer) == 0) // error + return false; + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != payload_size) + return false; + + return true; +} + +bool Chapters::ExpandChaptersArray() { + if (chapters_size_ > chapters_count_) + return true; // nothing to do yet + + const int size = (chapters_size_ == 0) ? 1 : 2 * chapters_size_; + + Chapter* const chapters = new (std::nothrow) Chapter[size]; // NOLINT + if (chapters == NULL) + return false; + + for (int idx = 0; idx < chapters_count_; ++idx) { + const Chapter& src = chapters_[idx]; + Chapter* const dst = chapters + idx; + src.ShallowCopy(dst); + } + + delete[] chapters_; + + chapters_ = chapters; + chapters_size_ = size; + + return true; +} + +uint64_t Chapters::WriteEdition(IMkvWriter* writer) const { + uint64_t payload_size = 0; + + for (int idx = 0; idx < chapters_count_; ++idx) { + const Chapter& chapter = chapters_[idx]; + payload_size += chapter.WriteAtom(NULL); + } + + const uint64_t edition_size = + EbmlMasterElementSize(libwebm::kMkvEditionEntry, payload_size) + + payload_size; + + if (writer == NULL) // return size only + return edition_size; + + const int64_t start = writer->Position(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvEditionEntry, payload_size)) + return 0; // error + + for (int idx = 0; idx < chapters_count_; ++idx) { + const Chapter& chapter = chapters_[idx]; + + const uint64_t chapter_size = chapter.WriteAtom(writer); + if (chapter_size == 0) // error + return 0; + } + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != edition_size) + return 0; + + return edition_size; +} + +// Tag Class + +bool Tag::add_simple_tag(const char* tag_name, const char* tag_string) { + if (!ExpandSimpleTagsArray()) + return false; + + SimpleTag& st = simple_tags_[simple_tags_count_++]; + st.Init(); + + if (!st.set_tag_name(tag_name)) + return false; + + if (!st.set_tag_string(tag_string)) + return false; + + return true; +} + +Tag::Tag() { + simple_tags_ = NULL; + simple_tags_size_ = 0; + simple_tags_count_ = 0; +} + +Tag::~Tag() {} + +void Tag::ShallowCopy(Tag* dst) const { + dst->simple_tags_ = simple_tags_; + dst->simple_tags_size_ = simple_tags_size_; + dst->simple_tags_count_ = simple_tags_count_; +} + +void Tag::Clear() { + while (simple_tags_count_ > 0) { + SimpleTag& st = simple_tags_[--simple_tags_count_]; + st.Clear(); + } + + delete[] simple_tags_; + simple_tags_ = NULL; + + simple_tags_size_ = 0; +} + +bool Tag::ExpandSimpleTagsArray() { + if (simple_tags_size_ > simple_tags_count_) + return true; // nothing to do yet + + const int size = (simple_tags_size_ == 0) ? 1 : 2 * simple_tags_size_; + + SimpleTag* const simple_tags = new (std::nothrow) SimpleTag[size]; // NOLINT + if (simple_tags == NULL) + return false; + + for (int idx = 0; idx < simple_tags_count_; ++idx) { + simple_tags[idx] = simple_tags_[idx]; // shallow copy + } + + delete[] simple_tags_; + + simple_tags_ = simple_tags; + simple_tags_size_ = size; + + return true; +} + +uint64_t Tag::Write(IMkvWriter* writer) const { + uint64_t payload_size = 0; + + for (int idx = 0; idx < simple_tags_count_; ++idx) { + const SimpleTag& st = simple_tags_[idx]; + payload_size += st.Write(NULL); + } + + const uint64_t tag_size = + EbmlMasterElementSize(libwebm::kMkvTag, payload_size) + payload_size; + + if (writer == NULL) + return tag_size; + + const int64_t start = writer->Position(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvTag, payload_size)) + return 0; + + for (int idx = 0; idx < simple_tags_count_; ++idx) { + const SimpleTag& st = simple_tags_[idx]; + + if (!st.Write(writer)) + return 0; + } + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != tag_size) + return 0; + + return tag_size; +} + +// Tag::SimpleTag + +void Tag::SimpleTag::Init() { + tag_name_ = NULL; + tag_string_ = NULL; +} + +void Tag::SimpleTag::Clear() { + StrCpy(NULL, &tag_name_); + StrCpy(NULL, &tag_string_); +} + +bool Tag::SimpleTag::set_tag_name(const char* tag_name) { + return StrCpy(tag_name, &tag_name_); +} + +bool Tag::SimpleTag::set_tag_string(const char* tag_string) { + return StrCpy(tag_string, &tag_string_); +} + +uint64_t Tag::SimpleTag::Write(IMkvWriter* writer) const { + uint64_t payload_size = EbmlElementSize(libwebm::kMkvTagName, tag_name_); + + payload_size += EbmlElementSize(libwebm::kMkvTagString, tag_string_); + + const uint64_t simple_tag_size = + EbmlMasterElementSize(libwebm::kMkvSimpleTag, payload_size) + + payload_size; + + if (writer == NULL) + return simple_tag_size; + + const int64_t start = writer->Position(); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvSimpleTag, payload_size)) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvTagName, tag_name_)) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvTagString, tag_string_)) + return 0; + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != simple_tag_size) + return 0; + + return simple_tag_size; +} + +// Tags Class + +Tags::Tags() : tags_size_(0), tags_count_(0), tags_(NULL) {} + +Tags::~Tags() { + while (tags_count_ > 0) { + Tag& tag = tags_[--tags_count_]; + tag.Clear(); + } + + delete[] tags_; + tags_ = NULL; +} + +int Tags::Count() const { return tags_count_; } + +Tag* Tags::AddTag() { + if (!ExpandTagsArray()) + return NULL; + + Tag& tag = tags_[tags_count_++]; + + return &tag; +} + +bool Tags::Write(IMkvWriter* writer) const { + if (writer == NULL) + return false; + + uint64_t payload_size = 0; + + for (int idx = 0; idx < tags_count_; ++idx) { + const Tag& tag = tags_[idx]; + payload_size += tag.Write(NULL); + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvTags, payload_size)) + return false; + + const int64_t start = writer->Position(); + + for (int idx = 0; idx < tags_count_; ++idx) { + const Tag& tag = tags_[idx]; + + const uint64_t tag_size = tag.Write(writer); + if (tag_size == 0) // error + return 0; + } + + const int64_t stop = writer->Position(); + + if (stop >= start && uint64_t(stop - start) != payload_size) + return false; + + return true; +} + +bool Tags::ExpandTagsArray() { + if (tags_size_ > tags_count_) + return true; // nothing to do yet + + const int size = (tags_size_ == 0) ? 1 : 2 * tags_size_; + + Tag* const tags = new (std::nothrow) Tag[size]; // NOLINT + if (tags == NULL) + return false; + + for (int idx = 0; idx < tags_count_; ++idx) { + const Tag& src = tags_[idx]; + Tag* const dst = tags + idx; + src.ShallowCopy(dst); + } + + delete[] tags_; + + tags_ = tags; + tags_size_ = size; + + return true; +} + +/////////////////////////////////////////////////////////////// +// +// Cluster class + +Cluster::Cluster(uint64_t timecode, int64_t cues_pos, uint64_t timecode_scale, + bool write_last_frame_with_duration, bool fixed_size_timecode) + : blocks_added_(0), + finalized_(false), + fixed_size_timecode_(fixed_size_timecode), + header_written_(false), + payload_size_(0), + position_for_cues_(cues_pos), + size_position_(-1), + timecode_(timecode), + timecode_scale_(timecode_scale), + write_last_frame_with_duration_(write_last_frame_with_duration), + writer_(NULL) {} + +Cluster::~Cluster() { + // Delete any stored frames that are left behind. This will happen if the + // Cluster was not Finalized for whatever reason. + while (!stored_frames_.empty()) { + while (!stored_frames_.begin()->second.empty()) { + delete stored_frames_.begin()->second.front(); + stored_frames_.begin()->second.pop_front(); + } + stored_frames_.erase(stored_frames_.begin()->first); + } +} + +bool Cluster::Init(IMkvWriter* ptr_writer) { + if (!ptr_writer) { + return false; + } + writer_ = ptr_writer; + return true; +} + +bool Cluster::AddFrame(const Frame* const frame) { + return QueueOrWriteFrame(frame); +} + +bool Cluster::AddFrame(const uint8_t* data, uint64_t length, + uint64_t track_number, uint64_t abs_timecode, + bool is_key) { + Frame frame; + if (!frame.Init(data, length)) + return false; + frame.set_track_number(track_number); + frame.set_timestamp(abs_timecode); + frame.set_is_key(is_key); + return QueueOrWriteFrame(&frame); +} + +bool Cluster::AddFrameWithAdditional(const uint8_t* data, uint64_t length, + const uint8_t* additional, + uint64_t additional_length, + uint64_t add_id, uint64_t track_number, + uint64_t abs_timecode, bool is_key) { + if (!additional || additional_length == 0) { + return false; + } + Frame frame; + if (!frame.Init(data, length) || + !frame.AddAdditionalData(additional, additional_length, add_id)) { + return false; + } + frame.set_track_number(track_number); + frame.set_timestamp(abs_timecode); + frame.set_is_key(is_key); + return QueueOrWriteFrame(&frame); +} + +bool Cluster::AddFrameWithDiscardPadding(const uint8_t* data, uint64_t length, + int64_t discard_padding, + uint64_t track_number, + uint64_t abs_timecode, bool is_key) { + Frame frame; + if (!frame.Init(data, length)) + return false; + frame.set_discard_padding(discard_padding); + frame.set_track_number(track_number); + frame.set_timestamp(abs_timecode); + frame.set_is_key(is_key); + return QueueOrWriteFrame(&frame); +} + +bool Cluster::AddMetadata(const uint8_t* data, uint64_t length, + uint64_t track_number, uint64_t abs_timecode, + uint64_t duration_timecode) { + Frame frame; + if (!frame.Init(data, length)) + return false; + frame.set_track_number(track_number); + frame.set_timestamp(abs_timecode); + frame.set_duration(duration_timecode); + frame.set_is_key(true); // All metadata blocks are keyframes. + return QueueOrWriteFrame(&frame); +} + +void Cluster::AddPayloadSize(uint64_t size) { payload_size_ += size; } + +bool Cluster::Finalize() { + return !write_last_frame_with_duration_ && Finalize(false, 0); +} + +bool Cluster::Finalize(bool set_last_frame_duration, uint64_t duration) { + if (!writer_ || finalized_) + return false; + + if (write_last_frame_with_duration_) { + // Write out held back Frames. This essentially performs a k-way merge + // across all tracks in the increasing order of timestamps. + while (!stored_frames_.empty()) { + Frame* frame = stored_frames_.begin()->second.front(); + + // Get the next frame to write (frame with least timestamp across all + // tracks). + for (FrameMapIterator frames_iterator = ++stored_frames_.begin(); + frames_iterator != stored_frames_.end(); ++frames_iterator) { + if (frames_iterator->second.front()->timestamp() < frame->timestamp()) { + frame = frames_iterator->second.front(); + } + } + + // Set the duration if it's the last frame for the track. + if (set_last_frame_duration && + stored_frames_[frame->track_number()].size() == 1 && + !frame->duration_set()) { + frame->set_duration(duration - frame->timestamp()); + if (!frame->is_key() && !frame->reference_block_timestamp_set()) { + frame->set_reference_block_timestamp( + last_block_timestamp_[frame->track_number()]); + } + } + + // Write the frame and remove it from |stored_frames_|. + const bool wrote_frame = DoWriteFrame(frame); + stored_frames_[frame->track_number()].pop_front(); + if (stored_frames_[frame->track_number()].empty()) { + stored_frames_.erase(frame->track_number()); + } + delete frame; + if (!wrote_frame) + return false; + } + } + + if (size_position_ == -1) + return false; + + if (writer_->Seekable()) { + const int64_t pos = writer_->Position(); + + if (writer_->Position(size_position_)) + return false; + + if (WriteUIntSize(writer_, payload_size(), 8)) + return false; + + if (writer_->Position(pos)) + return false; + } + + finalized_ = true; + + return true; +} + +uint64_t Cluster::Size() const { + const uint64_t element_size = + EbmlMasterElementSize(libwebm::kMkvCluster, 0xFFFFFFFFFFFFFFFFULL) + + payload_size_; + return element_size; +} + +bool Cluster::PreWriteBlock() { + if (finalized_) + return false; + + if (!header_written_) { + if (!WriteClusterHeader()) + return false; + } + + return true; +} + +void Cluster::PostWriteBlock(uint64_t element_size) { + AddPayloadSize(element_size); + ++blocks_added_; +} + +int64_t Cluster::GetRelativeTimecode(int64_t abs_timecode) const { + const int64_t cluster_timecode = this->Cluster::timecode(); + const int64_t rel_timecode = + static_cast(abs_timecode) - cluster_timecode; + + if (rel_timecode < 0 || rel_timecode > kMaxBlockTimecode) + return -1; + + return rel_timecode; +} + +bool Cluster::DoWriteFrame(const Frame* const frame) { + if (!frame || !frame->IsValid()) + return false; + + if (!PreWriteBlock()) + return false; + + const uint64_t element_size = WriteFrame(writer_, frame, this); + if (element_size == 0) + return false; + + PostWriteBlock(element_size); + last_block_timestamp_[frame->track_number()] = frame->timestamp(); + return true; +} + +bool Cluster::QueueOrWriteFrame(const Frame* const frame) { + if (!frame || !frame->IsValid()) + return false; + + // If |write_last_frame_with_duration_| is not set, then write the frame right + // away. + if (!write_last_frame_with_duration_) { + return DoWriteFrame(frame); + } + + // Queue the current frame. + uint64_t track_number = frame->track_number(); + Frame* const frame_to_store = new Frame(); + frame_to_store->CopyFrom(*frame); + stored_frames_[track_number].push_back(frame_to_store); + + // Iterate through all queued frames in the current track except the last one + // and write it if it is okay to do so (i.e.) no other track has an held back + // frame with timestamp <= the timestamp of the frame in question. + std::vector::iterator> frames_to_erase; + for (std::list::iterator + current_track_iterator = stored_frames_[track_number].begin(), + end = --stored_frames_[track_number].end(); + current_track_iterator != end; ++current_track_iterator) { + const Frame* const frame_to_write = *current_track_iterator; + bool okay_to_write = true; + for (FrameMapIterator track_iterator = stored_frames_.begin(); + track_iterator != stored_frames_.end(); ++track_iterator) { + if (track_iterator->first == track_number) { + continue; + } + if (track_iterator->second.front()->timestamp() < + frame_to_write->timestamp()) { + okay_to_write = false; + break; + } + } + if (okay_to_write) { + const bool wrote_frame = DoWriteFrame(frame_to_write); + delete frame_to_write; + if (!wrote_frame) + return false; + frames_to_erase.push_back(current_track_iterator); + } else { + break; + } + } + for (std::vector::iterator>::iterator iterator = + frames_to_erase.begin(); + iterator != frames_to_erase.end(); ++iterator) { + stored_frames_[track_number].erase(*iterator); + } + return true; +} + +bool Cluster::WriteClusterHeader() { + if (finalized_) + return false; + + if (WriteID(writer_, libwebm::kMkvCluster)) + return false; + + // Save for later. + size_position_ = writer_->Position(); + + // Write "unknown" (EBML coded -1) as cluster size value. We need to write 8 + // bytes because we do not know how big our cluster will be. + if (SerializeInt(writer_, kEbmlUnknownValue, 8)) + return false; + + if (!WriteEbmlElement(writer_, libwebm::kMkvTimecode, timecode(), + fixed_size_timecode_ ? 8 : 0)) { + return false; + } + AddPayloadSize(EbmlElementSize(libwebm::kMkvTimecode, timecode(), + fixed_size_timecode_ ? 8 : 0)); + header_written_ = true; + + return true; +} + +/////////////////////////////////////////////////////////////// +// +// SeekHead Class + +SeekHead::SeekHead() : start_pos_(0ULL) { + for (int32_t i = 0; i < kSeekEntryCount; ++i) { + seek_entry_id_[i] = 0; + seek_entry_pos_[i] = 0; + } +} + +SeekHead::~SeekHead() {} + +bool SeekHead::Finalize(IMkvWriter* writer) const { + if (writer->Seekable()) { + if (start_pos_ == -1) + return false; + + uint64_t payload_size = 0; + uint64_t entry_size[kSeekEntryCount]; + + for (int32_t i = 0; i < kSeekEntryCount; ++i) { + if (seek_entry_id_[i] != 0) { + entry_size[i] = EbmlElementSize(libwebm::kMkvSeekID, + static_cast(seek_entry_id_[i])); + entry_size[i] += EbmlElementSize( + libwebm::kMkvSeekPosition, static_cast(seek_entry_pos_[i])); + + payload_size += + EbmlMasterElementSize(libwebm::kMkvSeek, entry_size[i]) + + entry_size[i]; + } + } + + // No SeekHead elements + if (payload_size == 0) + return true; + + const int64_t pos = writer->Position(); + if (writer->Position(start_pos_)) + return false; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvSeekHead, payload_size)) + return false; + + for (int32_t i = 0; i < kSeekEntryCount; ++i) { + if (seek_entry_id_[i] != 0) { + if (!WriteEbmlMasterElement(writer, libwebm::kMkvSeek, entry_size[i])) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvSeekID, + static_cast(seek_entry_id_[i]))) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvSeekPosition, + static_cast(seek_entry_pos_[i]))) + return false; + } + } + + const uint64_t total_entry_size = kSeekEntryCount * MaxEntrySize(); + const uint64_t total_size = + EbmlMasterElementSize(libwebm::kMkvSeekHead, total_entry_size) + + total_entry_size; + const int64_t size_left = total_size - (writer->Position() - start_pos_); + + const uint64_t bytes_written = WriteVoidElement(writer, size_left); + if (!bytes_written) + return false; + + if (writer->Position(pos)) + return false; + } + + return true; +} + +bool SeekHead::Write(IMkvWriter* writer) { + const uint64_t entry_size = kSeekEntryCount * MaxEntrySize(); + const uint64_t size = + EbmlMasterElementSize(libwebm::kMkvSeekHead, entry_size); + + start_pos_ = writer->Position(); + + const uint64_t bytes_written = WriteVoidElement(writer, size + entry_size); + if (!bytes_written) + return false; + + return true; +} + +bool SeekHead::AddSeekEntry(uint32_t id, uint64_t pos) { + for (int32_t i = 0; i < kSeekEntryCount; ++i) { + if (seek_entry_id_[i] == 0) { + seek_entry_id_[i] = id; + seek_entry_pos_[i] = pos; + return true; + } + } + return false; +} + +uint32_t SeekHead::GetId(int index) const { + if (index < 0 || index >= kSeekEntryCount) + return UINT_MAX; + return seek_entry_id_[index]; +} + +uint64_t SeekHead::GetPosition(int index) const { + if (index < 0 || index >= kSeekEntryCount) + return ULLONG_MAX; + return seek_entry_pos_[index]; +} + +bool SeekHead::SetSeekEntry(int index, uint32_t id, uint64_t position) { + if (index < 0 || index >= kSeekEntryCount) + return false; + seek_entry_id_[index] = id; + seek_entry_pos_[index] = position; + return true; +} + +uint64_t SeekHead::MaxEntrySize() const { + const uint64_t max_entry_payload_size = + EbmlElementSize(libwebm::kMkvSeekID, + static_cast(UINT64_C(0xffffffff))) + + EbmlElementSize(libwebm::kMkvSeekPosition, + static_cast(UINT64_C(0xffffffffffffffff))); + const uint64_t max_entry_size = + EbmlMasterElementSize(libwebm::kMkvSeek, max_entry_payload_size) + + max_entry_payload_size; + + return max_entry_size; +} + +/////////////////////////////////////////////////////////////// +// +// SegmentInfo Class + +SegmentInfo::SegmentInfo() + : duration_(-1.0), + muxing_app_(NULL), + timecode_scale_(1000000ULL), + writing_app_(NULL), + date_utc_(LLONG_MIN), + duration_pos_(-1) {} + +SegmentInfo::~SegmentInfo() { + delete[] muxing_app_; + delete[] writing_app_; +} + +bool SegmentInfo::Init() { + int32_t major; + int32_t minor; + int32_t build; + int32_t revision; + GetVersion(&major, &minor, &build, &revision); + char temp[256]; +#ifdef _MSC_VER + sprintf_s(temp, sizeof(temp) / sizeof(temp[0]), "libwebm-%d.%d.%d.%d", major, + minor, build, revision); +#else + snprintf(temp, sizeof(temp) / sizeof(temp[0]), "libwebm-%d.%d.%d.%d", major, + minor, build, revision); +#endif + + const size_t app_len = strlen(temp) + 1; + + delete[] muxing_app_; + + muxing_app_ = new (std::nothrow) char[app_len]; // NOLINT + if (!muxing_app_) + return false; + +#ifdef _MSC_VER + strcpy_s(muxing_app_, app_len, temp); +#else + strcpy(muxing_app_, temp); +#endif + + set_writing_app(temp); + if (!writing_app_) + return false; + return true; +} + +bool SegmentInfo::Finalize(IMkvWriter* writer) const { + if (!writer) + return false; + + if (duration_ > 0.0) { + if (writer->Seekable()) { + if (duration_pos_ == -1) + return false; + + const int64_t pos = writer->Position(); + + if (writer->Position(duration_pos_)) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvDuration, + static_cast(duration_))) + return false; + + if (writer->Position(pos)) + return false; + } + } + + return true; +} + +bool SegmentInfo::Write(IMkvWriter* writer) { + if (!writer || !muxing_app_ || !writing_app_) + return false; + + uint64_t size = EbmlElementSize(libwebm::kMkvTimecodeScale, + static_cast(timecode_scale_)); + if (duration_ > 0.0) + size += + EbmlElementSize(libwebm::kMkvDuration, static_cast(duration_)); + if (date_utc_ != LLONG_MIN) + size += EbmlDateElementSize(libwebm::kMkvDateUTC); + size += EbmlElementSize(libwebm::kMkvMuxingApp, muxing_app_); + size += EbmlElementSize(libwebm::kMkvWritingApp, writing_app_); + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvInfo, size)) + return false; + + const int64_t payload_position = writer->Position(); + if (payload_position < 0) + return false; + + if (!WriteEbmlElement(writer, libwebm::kMkvTimecodeScale, + static_cast(timecode_scale_))) + return false; + + if (duration_ > 0.0) { + // Save for later + duration_pos_ = writer->Position(); + + if (!WriteEbmlElement(writer, libwebm::kMkvDuration, + static_cast(duration_))) + return false; + } + + if (date_utc_ != LLONG_MIN) + WriteEbmlDateElement(writer, libwebm::kMkvDateUTC, date_utc_); + + if (!WriteEbmlElement(writer, libwebm::kMkvMuxingApp, muxing_app_)) + return false; + if (!WriteEbmlElement(writer, libwebm::kMkvWritingApp, writing_app_)) + return false; + + const int64_t stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(size)) + return false; + + return true; +} + +void SegmentInfo::set_muxing_app(const char* app) { + if (app) { + const size_t length = strlen(app) + 1; + char* temp_str = new (std::nothrow) char[length]; // NOLINT + if (!temp_str) + return; + +#ifdef _MSC_VER + strcpy_s(temp_str, length, app); +#else + strcpy(temp_str, app); +#endif + + delete[] muxing_app_; + muxing_app_ = temp_str; + } +} + +void SegmentInfo::set_writing_app(const char* app) { + if (app) { + const size_t length = strlen(app) + 1; + char* temp_str = new (std::nothrow) char[length]; // NOLINT + if (!temp_str) + return; + +#ifdef _MSC_VER + strcpy_s(temp_str, length, app); +#else + strcpy(temp_str, app); +#endif + + delete[] writing_app_; + writing_app_ = temp_str; + } +} + +/////////////////////////////////////////////////////////////// +// +// Segment Class + +Segment::Segment() + : + cluster_list_(NULL), + cluster_list_capacity_(0), + cluster_list_size_(0), + cues_position_(kAfterClusters), + cues_track_(0), + force_new_cluster_(false), + frames_(NULL), + frames_capacity_(0), + frames_size_(0), + has_video_(false), + header_written_(false), + last_block_duration_(0), + last_timestamp_(0), + max_cluster_duration_(kDefaultMaxClusterDuration), + max_cluster_size_(0), + mode_(kFile), + new_cuepoint_(false), + output_cues_(true), + accurate_cluster_duration_(false), + fixed_size_cluster_timecode_(false), + payload_pos_(0), + size_position_(0), + doc_type_version_(kDefaultDocTypeVersion), + doc_type_version_written_(0), + writer_cluster_(NULL), + writer_cues_(NULL), + writer_header_(NULL) { + const time_t curr_time = time(NULL); + seed_ = static_cast(curr_time); +#ifdef _WIN32 + srand(seed_); +#endif +} + +Segment::~Segment() { + if (cluster_list_) { + for (int32_t i = 0; i < cluster_list_size_; ++i) { + Cluster* const cluster = cluster_list_[i]; + delete cluster; + } + delete[] cluster_list_; + } + + if (frames_) { + for (int32_t i = 0; i < frames_size_; ++i) { + Frame* const frame = frames_[i]; + delete frame; + } + delete[] frames_; + } +} + +void Segment::MoveCuesBeforeClustersHelper(uint64_t diff, int32_t index, + uint64_t* cues_size) { + CuePoint* const cue_point = cues_.GetCueByIndex(index); + if (cue_point == NULL) + return; + const uint64_t old_cue_point_size = cue_point->Size(); + const uint64_t cluster_pos = cue_point->cluster_pos() + diff; + cue_point->set_cluster_pos(cluster_pos); // update the new cluster position + // New size of the cue is computed as follows + // Let a = current sum of size of all CuePoints + // Let b = Increase in Cue Point's size due to this iteration + // Let c = Increase in size of Cues Element's length due to this iteration + // (This is computed as CodedSize(a + b) - CodedSize(a)) + // Let d = b + c. Now d is the |diff| passed to the next recursive call. + // Let e = a + b. Now e is the |cues_size| passed to the next recursive + // call. + const uint64_t cue_point_size_diff = cue_point->Size() - old_cue_point_size; + const uint64_t cue_size_diff = + GetCodedUIntSize(*cues_size + cue_point_size_diff) - + GetCodedUIntSize(*cues_size); + *cues_size += cue_point_size_diff; + diff = cue_size_diff + cue_point_size_diff; + if (diff > 0) { + for (int32_t i = 0; i < cues_.cue_entries_size(); ++i) { + MoveCuesBeforeClustersHelper(diff, i, cues_size); + } + } +} + +void Segment::MoveCuesBeforeClusters() { + const uint64_t current_cue_size = cues_.Size(); + uint64_t cue_size = 0; + for (int32_t i = 0; i < cues_.cue_entries_size(); ++i) + cue_size += cues_.GetCueByIndex(i)->Size(); + for (int32_t i = 0; i < cues_.cue_entries_size(); ++i) + MoveCuesBeforeClustersHelper(current_cue_size, i, &cue_size); + + // Adjust the Seek Entry to reflect the change in position + // of Cluster and Cues + int32_t cluster_index = 0; + int32_t cues_index = 0; + for (int32_t i = 0; i < SeekHead::kSeekEntryCount; ++i) { + if (seek_head_.GetId(i) == libwebm::kMkvCluster) + cluster_index = i; + if (seek_head_.GetId(i) == libwebm::kMkvCues) + cues_index = i; + } + seek_head_.SetSeekEntry(cues_index, libwebm::kMkvCues, + seek_head_.GetPosition(cluster_index)); + seek_head_.SetSeekEntry(cluster_index, libwebm::kMkvCluster, + cues_.Size() + seek_head_.GetPosition(cues_index)); +} + +bool Segment::Init(IMkvWriter* ptr_writer) { + if (!ptr_writer) { + return false; + } + writer_cluster_ = ptr_writer; + writer_cues_ = ptr_writer; + writer_header_ = ptr_writer; + return segment_info_.Init(); +} + +bool Segment::Finalize() { + if (WriteFramesAll() < 0) + return false; + + // In kLive mode, call Cluster::Finalize only if |accurate_cluster_duration_| + // is set. In all other modes, always call Cluster::Finalize. + if ((mode_ == kLive ? accurate_cluster_duration_ : true) && + cluster_list_size_ > 0) { + // Update last cluster's size + Cluster* const old_cluster = cluster_list_[cluster_list_size_ - 1]; + + // For the last frame of the last Cluster, we don't write it as a BlockGroup + // with Duration unless the frame itself has duration set explicitly. + if (!old_cluster || !old_cluster->Finalize(false, 0)) + return false; + } + + if (mode_ == kFile) { + + const double duration = + (static_cast(last_timestamp_) + last_block_duration_) / + segment_info_.timecode_scale(); + segment_info_.set_duration(duration); + if (!segment_info_.Finalize(writer_header_)) + return false; + + if (output_cues_) + if (!seek_head_.AddSeekEntry(libwebm::kMkvCues, MaxOffset())) + return false; + + cluster_end_offset_ = writer_cluster_->Position(); + + // Write the seek headers and cues + if (output_cues_) + if (!cues_.Write(writer_cues_)) + return false; + + if (!seek_head_.Finalize(writer_header_)) + return false; + + if (writer_header_->Seekable()) { + if (size_position_ == -1) + return false; + + const int64_t segment_size = MaxOffset(); + if (segment_size < 1) + return false; + + const int64_t pos = writer_header_->Position(); + UpdateDocTypeVersion(); + if (doc_type_version_ != doc_type_version_written_) { + if (writer_header_->Position(0)) + return false; + + const char* const doc_type = + DocTypeIsWebm() ? kDocTypeWebm : kDocTypeMatroska; + if (!WriteEbmlHeader(writer_header_, doc_type_version_, doc_type)) + return false; + if (writer_header_->Position() != ebml_header_size_) + return false; + + doc_type_version_written_ = doc_type_version_; + } + + if (writer_header_->Position(size_position_)) + return false; + + if (WriteUIntSize(writer_header_, segment_size, 8)) + return false; + + if (writer_header_->Position(pos)) + return false; + } + } + + return true; +} + +Track* Segment::AddTrack(int32_t number) { + Track* const track = new (std::nothrow) Track(&seed_); // NOLINT + + if (!track) + return NULL; + + if (!tracks_.AddTrack(track, number)) { + delete track; + return NULL; + } + + return track; +} + +Chapter* Segment::AddChapter() { return chapters_.AddChapter(&seed_); } + +Tag* Segment::AddTag() { return tags_.AddTag(); } + +uint64_t Segment::AddVideoTrack(int32_t width, int32_t height, int32_t number) { + VideoTrack* const track = new (std::nothrow) VideoTrack(&seed_); // NOLINT + if (!track) + return 0; + + track->set_type(Tracks::kVideo); + track->set_codec_id(Tracks::kVp8CodecId); + track->set_width(width); + track->set_height(height); + + tracks_.AddTrack(track, number); + has_video_ = true; + + return track->number(); +} + +bool Segment::AddCuePoint(uint64_t timestamp, uint64_t track) { + if (cluster_list_size_ < 1) + return false; + + const Cluster* const cluster = cluster_list_[cluster_list_size_ - 1]; + if (!cluster) + return false; + + CuePoint* const cue = new (std::nothrow) CuePoint(); // NOLINT + if (!cue) + return false; + + cue->set_time(timestamp / segment_info_.timecode_scale()); + cue->set_block_number(cluster->blocks_added()); + cue->set_cluster_pos(cluster->position_for_cues()); + cue->set_track(track); + if (!cues_.AddCue(cue)) + return false; + + new_cuepoint_ = false; + return true; +} + +uint64_t Segment::AddAudioTrack(int32_t sample_rate, int32_t channels, + int32_t number) { + AudioTrack* const track = new (std::nothrow) AudioTrack(&seed_); // NOLINT + if (!track) + return 0; + + track->set_type(Tracks::kAudio); + track->set_codec_id(Tracks::kVorbisCodecId); + track->set_sample_rate(sample_rate); + track->set_channels(channels); + + tracks_.AddTrack(track, number); + + return track->number(); +} + +bool Segment::AddFrame(const uint8_t* data, uint64_t length, + uint64_t track_number, uint64_t timestamp, bool is_key) { + if (!data) + return false; + + Frame frame; + if (!frame.Init(data, length)) + return false; + frame.set_track_number(track_number); + frame.set_timestamp(timestamp); + frame.set_is_key(is_key); + return AddGenericFrame(&frame); +} + +bool Segment::AddFrameWithAdditional(const uint8_t* data, uint64_t length, + const uint8_t* additional, + uint64_t additional_length, + uint64_t add_id, uint64_t track_number, + uint64_t timestamp, bool is_key) { + if (!data || !additional) + return false; + + Frame frame; + if (!frame.Init(data, length) || + !frame.AddAdditionalData(additional, additional_length, add_id)) { + return false; + } + frame.set_track_number(track_number); + frame.set_timestamp(timestamp); + frame.set_is_key(is_key); + return AddGenericFrame(&frame); +} + +bool Segment::AddFrameWithDiscardPadding(const uint8_t* data, uint64_t length, + int64_t discard_padding, + uint64_t track_number, + uint64_t timestamp, bool is_key) { + if (!data) + return false; + + Frame frame; + if (!frame.Init(data, length)) + return false; + frame.set_discard_padding(discard_padding); + frame.set_track_number(track_number); + frame.set_timestamp(timestamp); + frame.set_is_key(is_key); + return AddGenericFrame(&frame); +} + +bool Segment::AddMetadata(const uint8_t* data, uint64_t length, + uint64_t track_number, uint64_t timestamp_ns, + uint64_t duration_ns) { + if (!data) + return false; + + Frame frame; + if (!frame.Init(data, length)) + return false; + frame.set_track_number(track_number); + frame.set_timestamp(timestamp_ns); + frame.set_duration(duration_ns); + frame.set_is_key(true); // All metadata blocks are keyframes. + return AddGenericFrame(&frame); +} + +bool Segment::AddGenericFrame(const Frame* frame) { + if (!frame) + return false; + + if (!CheckHeaderInfo()) + return false; + + // Check for non-monotonically increasing timestamps. + if (frame->timestamp() < last_timestamp_) + return false; + + // Check if the track number is valid. + if (!tracks_.GetTrackByNumber(frame->track_number())) + return false; + + if (frame->discard_padding() != 0) + doc_type_version_ = 4; + + // If the segment has a video track hold onto audio frames to make sure the + // audio that is associated with the start time of a video key-frame is + // muxed into the same cluster. + if (has_video_ && tracks_.TrackIsAudio(frame->track_number()) && + !force_new_cluster_) { + Frame* const new_frame = new (std::nothrow) Frame(); + if (!new_frame || !new_frame->CopyFrom(*frame)) + return false; + return QueueFrame(new_frame); + } + + if (!DoNewClusterProcessing(frame->track_number(), frame->timestamp(), + frame->is_key())) { + return false; + } + + if (cluster_list_size_ < 1) + return false; + + Cluster* const cluster = cluster_list_[cluster_list_size_ - 1]; + if (!cluster) + return false; + + // If the Frame is not a SimpleBlock, then set the reference_block_timestamp + // if it is not set already. + bool frame_created = false; + if (!frame->CanBeSimpleBlock() && !frame->is_key() && + !frame->reference_block_timestamp_set()) { + Frame* const new_frame = new (std::nothrow) Frame(); + if (!new_frame->CopyFrom(*frame)) + return false; + new_frame->set_reference_block_timestamp( + last_track_timestamp_[frame->track_number() - 1]); + frame = new_frame; + frame_created = true; + } + + if (!cluster->AddFrame(frame)) + return false; + + if (new_cuepoint_ && cues_track_ == frame->track_number()) { + if (!AddCuePoint(frame->timestamp(), cues_track_)) + return false; + } + + last_timestamp_ = frame->timestamp(); + last_track_timestamp_[frame->track_number() - 1] = frame->timestamp(); + last_block_duration_ = frame->duration(); + + if (frame_created) + delete frame; + + return true; +} + +void Segment::OutputCues(bool output_cues) { output_cues_ = output_cues; } + +void Segment::AccurateClusterDuration(bool accurate_cluster_duration) { + accurate_cluster_duration_ = accurate_cluster_duration; +} + +void Segment::UseFixedSizeClusterTimecode(bool fixed_size_cluster_timecode) { + fixed_size_cluster_timecode_ = fixed_size_cluster_timecode; +} + +bool Segment::CuesTrack(uint64_t track_number) { + const Track* const track = GetTrackByNumber(track_number); + if (!track) + return false; + + cues_track_ = track_number; + return true; +} + +void Segment::ForceNewClusterOnNextFrame() { force_new_cluster_ = true; } + +Track* Segment::GetTrackByNumber(uint64_t track_number) const { + return tracks_.GetTrackByNumber(track_number); +} + +bool Segment::WriteSegmentHeader() { + UpdateDocTypeVersion(); + + const char* const doc_type = + DocTypeIsWebm() ? kDocTypeWebm : kDocTypeMatroska; + if (!WriteEbmlHeader(writer_header_, doc_type_version_, doc_type)) + return false; + doc_type_version_written_ = doc_type_version_; + ebml_header_size_ = static_cast(writer_header_->Position()); + + // Write "unknown" (-1) as segment size value. If mode is kFile, Segment + // will write over duration when the file is finalized. + if (WriteID(writer_header_, libwebm::kMkvSegment)) + return false; + + // Save for later. + size_position_ = writer_header_->Position(); + + // Write "unknown" (EBML coded -1) as segment size value. We need to write 8 + // bytes because if we are going to overwrite the segment size later we do + // not know how big our segment will be. + if (SerializeInt(writer_header_, kEbmlUnknownValue, 8)) + return false; + + payload_pos_ = writer_header_->Position(); + + if (mode_ == kFile && writer_header_->Seekable()) { + // Set the duration > 0.0 so SegmentInfo will write out the duration. When + // the muxer is done writing we will set the correct duration and have + // SegmentInfo upadte it. + segment_info_.set_duration(1.0); + + if (!seek_head_.Write(writer_header_)) + return false; + } + + if (!seek_head_.AddSeekEntry(libwebm::kMkvInfo, MaxOffset())) + return false; + if (!segment_info_.Write(writer_header_)) + return false; + + if (!seek_head_.AddSeekEntry(libwebm::kMkvTracks, MaxOffset())) + return false; + if (!tracks_.Write(writer_header_)) + return false; + + if (chapters_.Count() > 0) { + if (!seek_head_.AddSeekEntry(libwebm::kMkvChapters, MaxOffset())) + return false; + if (!chapters_.Write(writer_header_)) + return false; + } + + if (tags_.Count() > 0) { + if (!seek_head_.AddSeekEntry(libwebm::kMkvTags, MaxOffset())) + return false; + if (!tags_.Write(writer_header_)) + return false; + } + + header_written_ = true; + + return true; +} + +// Here we are testing whether to create a new cluster, given a frame +// having time frame_timestamp_ns. +// +int Segment::TestFrame(uint64_t track_number, uint64_t frame_timestamp_ns, + bool is_key) const { + if (force_new_cluster_) + return 1; + + // If no clusters have been created yet, then create a new cluster + // and write this frame immediately, in the new cluster. This path + // should only be followed once, the first time we attempt to write + // a frame. + + if (cluster_list_size_ <= 0) + return 1; + + // There exists at least one cluster. We must compare the frame to + // the last cluster, in order to determine whether the frame is + // written to the existing cluster, or that a new cluster should be + // created. + + const uint64_t timecode_scale = segment_info_.timecode_scale(); + const uint64_t frame_timecode = frame_timestamp_ns / timecode_scale; + + const Cluster* const last_cluster = cluster_list_[cluster_list_size_ - 1]; + const uint64_t last_cluster_timecode = last_cluster->timecode(); + + // For completeness we test for the case when the frame's timecode + // is less than the cluster's timecode. Although in principle that + // is allowed, this muxer doesn't actually write clusters like that, + // so this indicates a bug somewhere in our algorithm. + + if (frame_timecode < last_cluster_timecode) // should never happen + return -1; + + // If the frame has a timestamp significantly larger than the last + // cluster (in Matroska, cluster-relative timestamps are serialized + // using a 16-bit signed integer), then we cannot write this frame + // to that cluster, and so we must create a new cluster. + + const int64_t delta_timecode = frame_timecode - last_cluster_timecode; + + if (delta_timecode > kMaxBlockTimecode) + return 2; + + // We decide to create a new cluster when we have a video keyframe. + // This will flush queued (audio) frames, and write the keyframe + // immediately, in the newly-created cluster. + + if (is_key && tracks_.TrackIsVideo(track_number)) + return 1; + + // Create a new cluster if we have accumulated too many frames + // already, where "too many" is defined as "the total time of frames + // in the cluster exceeds a threshold". + + const uint64_t delta_ns = delta_timecode * timecode_scale; + + if (max_cluster_duration_ > 0 && delta_ns >= max_cluster_duration_) + return 1; + + // This is similar to the case above, with the difference that a new + // cluster is created when the size of the current cluster exceeds a + // threshold. + + const uint64_t cluster_size = last_cluster->payload_size(); + + if (max_cluster_size_ > 0 && cluster_size >= max_cluster_size_) + return 1; + + // There's no need to create a new cluster, so emit this frame now. + + return 0; +} + +bool Segment::MakeNewCluster(uint64_t frame_timestamp_ns) { + const int32_t new_size = cluster_list_size_ + 1; + + if (new_size > cluster_list_capacity_) { + // Add more clusters. + const int32_t new_capacity = + (cluster_list_capacity_ <= 0) ? 1 : cluster_list_capacity_ * 2; + Cluster** const clusters = + new (std::nothrow) Cluster*[new_capacity]; // NOLINT + if (!clusters) + return false; + + for (int32_t i = 0; i < cluster_list_size_; ++i) { + clusters[i] = cluster_list_[i]; + } + + delete[] cluster_list_; + + cluster_list_ = clusters; + cluster_list_capacity_ = new_capacity; + } + + if (!WriteFramesLessThan(frame_timestamp_ns)) + return false; + + if (cluster_list_size_ > 0) { + // Update old cluster's size + Cluster* const old_cluster = cluster_list_[cluster_list_size_ - 1]; + + if (!old_cluster || !old_cluster->Finalize(true, frame_timestamp_ns)) + return false; + } + + if (output_cues_) + new_cuepoint_ = true; + + const uint64_t timecode_scale = segment_info_.timecode_scale(); + const uint64_t frame_timecode = frame_timestamp_ns / timecode_scale; + + uint64_t cluster_timecode = frame_timecode; + + if (frames_size_ > 0) { + const Frame* const f = frames_[0]; // earliest queued frame + const uint64_t ns = f->timestamp(); + const uint64_t tc = ns / timecode_scale; + + if (tc < cluster_timecode) + cluster_timecode = tc; + } + + Cluster*& cluster = cluster_list_[cluster_list_size_]; + const int64_t offset = MaxOffset(); + cluster = new (std::nothrow) + Cluster(cluster_timecode, offset, segment_info_.timecode_scale(), + accurate_cluster_duration_, fixed_size_cluster_timecode_); + if (!cluster) + return false; + + if (!cluster->Init(writer_cluster_)) + return false; + + cluster_list_size_ = new_size; + return true; +} + +bool Segment::DoNewClusterProcessing(uint64_t track_number, + uint64_t frame_timestamp_ns, bool is_key) { + for (;;) { + // Based on the characteristics of the current frame and current + // cluster, decide whether to create a new cluster. + const int result = TestFrame(track_number, frame_timestamp_ns, is_key); + if (result < 0) // error + return false; + + // Always set force_new_cluster_ to false after TestFrame. + force_new_cluster_ = false; + + // A non-zero result means create a new cluster. + if (result > 0 && !MakeNewCluster(frame_timestamp_ns)) + return false; + + // Write queued (audio) frames. + const int frame_count = WriteFramesAll(); + if (frame_count < 0) // error + return false; + + // Write the current frame to the current cluster (if TestFrame + // returns 0) or to a newly created cluster (TestFrame returns 1). + if (result <= 1) + return true; + + // TestFrame returned 2, which means there was a large time + // difference between the cluster and the frame itself. Do the + // test again, comparing the frame to the new cluster. + } +} + +bool Segment::CheckHeaderInfo() { + if (!header_written_) { + if (!WriteSegmentHeader()) + return false; + + if (!seek_head_.AddSeekEntry(libwebm::kMkvCluster, MaxOffset())) + return false; + + if (output_cues_ && cues_track_ == 0) { + // Check for a video track + for (uint32_t i = 0; i < tracks_.track_entries_size(); ++i) { + const Track* const track = tracks_.GetTrackByIndex(i); + if (!track) + return false; + + if (tracks_.TrackIsVideo(track->number())) { + cues_track_ = track->number(); + break; + } + } + + // Set first track found + if (cues_track_ == 0) { + const Track* const track = tracks_.GetTrackByIndex(0); + if (!track) + return false; + + cues_track_ = track->number(); + } + } + } + return true; +} + +void Segment::UpdateDocTypeVersion() { + for (uint32_t index = 0; index < tracks_.track_entries_size(); ++index) { + const Track* track = tracks_.GetTrackByIndex(index); + if (track == NULL) + break; + if ((track->codec_delay() || track->seek_pre_roll()) && + doc_type_version_ < 4) { + doc_type_version_ = 4; + break; + } + } +} + +int64_t Segment::MaxOffset() { + if (!writer_header_) + return -1; + + int64_t offset = writer_header_->Position() - payload_pos_; + + return offset; +} + +bool Segment::QueueFrame(Frame* frame) { + const int32_t new_size = frames_size_ + 1; + + if (new_size > frames_capacity_) { + // Add more frames. + const int32_t new_capacity = (!frames_capacity_) ? 2 : frames_capacity_ * 2; + + if (new_capacity < 1) + return false; + + Frame** const frames = new (std::nothrow) Frame*[new_capacity]; // NOLINT + if (!frames) + return false; + + for (int32_t i = 0; i < frames_size_; ++i) { + frames[i] = frames_[i]; + } + + delete[] frames_; + frames_ = frames; + frames_capacity_ = new_capacity; + } + + frames_[frames_size_++] = frame; + + return true; +} + +int Segment::WriteFramesAll() { + if (frames_ == NULL) + return 0; + + if (cluster_list_size_ < 1) + return -1; + + Cluster* const cluster = cluster_list_[cluster_list_size_ - 1]; + + if (!cluster) + return -1; + + for (int32_t i = 0; i < frames_size_; ++i) { + Frame*& frame = frames_[i]; + // TODO(jzern/vigneshv): using Segment::AddGenericFrame here would limit the + // places where |doc_type_version_| needs to be updated. + if (frame->discard_padding() != 0) + doc_type_version_ = 4; + if (!cluster->AddFrame(frame)) + return -1; + + if (new_cuepoint_ && cues_track_ == frame->track_number()) { + if (!AddCuePoint(frame->timestamp(), cues_track_)) + return -1; + } + + if (frame->timestamp() > last_timestamp_) { + last_timestamp_ = frame->timestamp(); + last_track_timestamp_[frame->track_number() - 1] = frame->timestamp(); + } + + delete frame; + frame = NULL; + } + + const int result = frames_size_; + frames_size_ = 0; + + return result; +} + +bool Segment::WriteFramesLessThan(uint64_t timestamp) { + // Check |cluster_list_size_| to see if this is the first cluster. If it is + // the first cluster the audio frames that are less than the first video + // timesatmp will be written in a later step. + if (frames_size_ > 0 && cluster_list_size_ > 0) { + if (!frames_) + return false; + + Cluster* const cluster = cluster_list_[cluster_list_size_ - 1]; + if (!cluster) + return false; + + int32_t shift_left = 0; + + // TODO(fgalligan): Change this to use the durations of frames instead of + // the next frame's start time if the duration is accurate. + for (int32_t i = 1; i < frames_size_; ++i) { + const Frame* const frame_curr = frames_[i]; + + if (frame_curr->timestamp() > timestamp) + break; + + const Frame* const frame_prev = frames_[i - 1]; + if (frame_prev->discard_padding() != 0) + doc_type_version_ = 4; + if (!cluster->AddFrame(frame_prev)) + return false; + + if (new_cuepoint_ && cues_track_ == frame_prev->track_number()) { + if (!AddCuePoint(frame_prev->timestamp(), cues_track_)) + return false; + } + + ++shift_left; + if (frame_prev->timestamp() > last_timestamp_) { + last_timestamp_ = frame_prev->timestamp(); + last_track_timestamp_[frame_prev->track_number() - 1] = + frame_prev->timestamp(); + } + + delete frame_prev; + } + + if (shift_left > 0) { + if (shift_left >= frames_size_) + return false; + + const int32_t new_frames_size = frames_size_ - shift_left; + for (int32_t i = 0; i < new_frames_size; ++i) { + frames_[i] = frames_[i + shift_left]; + } + + frames_size_ = new_frames_size; + } + } + + return true; +} + +bool Segment::DocTypeIsWebm() const { + const int kNumCodecIds = 9; + + // TODO(vigneshv): Tweak .clang-format. + const char* kWebmCodecIds[kNumCodecIds] = { + Tracks::kOpusCodecId, Tracks::kVorbisCodecId, + Tracks::kVp8CodecId, Tracks::kVp9CodecId, + Tracks::kVp10CodecId, Tracks::kWebVttCaptionsId, + Tracks::kWebVttDescriptionsId, Tracks::kWebVttMetadataId, + Tracks::kWebVttSubtitlesId}; + + const int num_tracks = static_cast(tracks_.track_entries_size()); + for (int track_index = 0; track_index < num_tracks; ++track_index) { + const Track* const track = tracks_.GetTrackByIndex(track_index); + const std::string codec_id = track->codec_id(); + + bool id_is_webm = false; + for (int id_index = 0; id_index < kNumCodecIds; ++id_index) { + if (codec_id == kWebmCodecIds[id_index]) { + id_is_webm = true; + break; + } + } + + if (!id_is_webm) + return false; + } + + return true; +} + +} // namespace mkvmuxer diff --git a/src/client/bundled/mkvmuxer/mkvmuxer.h b/src/client/bundled/mkvmuxer/mkvmuxer.h new file mode 100644 index 000000000..ac1aad053 --- /dev/null +++ b/src/client/bundled/mkvmuxer/mkvmuxer.h @@ -0,0 +1,1656 @@ +// Copyright (c) 2012 The WebM project authors. All Rights Reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. An additional intellectual property rights grant can be found +// in the file PATENTS. All contributing project authors may +// be found in the AUTHORS file in the root of the source tree. + +#ifndef MKVMUXER_MKVMUXER_H_ +#define MKVMUXER_MKVMUXER_H_ + +#include + +#include +#include +#include + +#include "common/webmids.h" +#include "mkvmuxer/mkvmuxertypes.h" + +// For a description of the WebM elements see +// http://www.webmproject.org/code/specs/container/. + +namespace mkvparser { +class IMkvReader; +} // namespace mkvparser + +namespace mkvmuxer { + +class Segment; + +const uint64_t kMaxTrackNumber = 126; + +/////////////////////////////////////////////////////////////// +// Interface used by the mkvmuxer to write out the Mkv data. +class IMkvWriter { + public: + // Writes out |len| bytes of |buf|. Returns 0 on success. + virtual int32 Write(const void* buf, uint32 len) = 0; + + // Returns the offset of the output position from the beginning of the + // output. + virtual int64 Position() const = 0; + + // Set the current File position. Returns 0 on success. + virtual int32 Position(int64 position) = 0; + + // Returns true if the writer is seekable. + virtual bool Seekable() const = 0; + + // Element start notification. Called whenever an element identifier is about + // to be written to the stream. |element_id| is the element identifier, and + // |position| is the location in the WebM stream where the first octet of the + // element identifier will be written. + // Note: the |MkvId| enumeration in webmids.hpp defines element values. + virtual void ElementStartNotify(uint64 element_id, int64 position) = 0; + + protected: + IMkvWriter(); + virtual ~IMkvWriter(); + + private: + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(IMkvWriter); +}; + +// Writes out the EBML header for a WebM file, but allows caller to specify +// DocType. This function must be called before any other libwebm writing +// functions are called. +bool WriteEbmlHeader(IMkvWriter* writer, uint64_t doc_type_version, + const char* const doc_type); + +// Writes out the EBML header for a WebM file. This function must be called +// before any other libwebm writing functions are called. +bool WriteEbmlHeader(IMkvWriter* writer, uint64_t doc_type_version); + +// Deprecated. Writes out EBML header with doc_type_version as +// kDefaultDocTypeVersion. Exists for backward compatibility. +bool WriteEbmlHeader(IMkvWriter* writer); + +// Copies in Chunk from source to destination between the given byte positions +bool ChunkedCopy(mkvparser::IMkvReader* source, IMkvWriter* dst, int64_t start, + int64_t size); + +/////////////////////////////////////////////////////////////// +// Class to hold data the will be written to a block. +class Frame { + public: + Frame(); + ~Frame(); + + // Sets this frame's contents based on |frame|. Returns true on success. On + // failure, this frame's existing contents may be lost. + bool CopyFrom(const Frame& frame); + + // Copies |frame| data into |frame_|. Returns true on success. + bool Init(const uint8_t* frame, uint64_t length); + + // Copies |additional| data into |additional_|. Returns true on success. + bool AddAdditionalData(const uint8_t* additional, uint64_t length, + uint64_t add_id); + + // Returns true if the frame has valid parameters. + bool IsValid() const; + + // Returns true if the frame can be written as a SimpleBlock based on current + // parameters. + bool CanBeSimpleBlock() const; + + uint64_t add_id() const { return add_id_; } + const uint8_t* additional() const { return additional_; } + uint64_t additional_length() const { return additional_length_; } + void set_duration(uint64_t duration); + uint64_t duration() const { return duration_; } + bool duration_set() const { return duration_set_; } + const uint8_t* frame() const { return frame_; } + void set_is_key(bool key) { is_key_ = key; } + bool is_key() const { return is_key_; } + uint64_t length() const { return length_; } + void set_track_number(uint64_t track_number) { track_number_ = track_number; } + uint64_t track_number() const { return track_number_; } + void set_timestamp(uint64_t timestamp) { timestamp_ = timestamp; } + uint64_t timestamp() const { return timestamp_; } + void set_discard_padding(int64_t discard_padding) { + discard_padding_ = discard_padding; + } + int64_t discard_padding() const { return discard_padding_; } + void set_reference_block_timestamp(int64_t reference_block_timestamp); + int64_t reference_block_timestamp() const { + return reference_block_timestamp_; + } + bool reference_block_timestamp_set() const { + return reference_block_timestamp_set_; + } + + private: + // Id of the Additional data. + uint64_t add_id_; + + // Pointer to additional data. Owned by this class. + uint8_t* additional_; + + // Length of the additional data. + uint64_t additional_length_; + + // Duration of the frame in nanoseconds. + uint64_t duration_; + + // Flag indicating that |duration_| has been set. Setting duration causes the + // frame to be written out as a Block with BlockDuration instead of as a + // SimpleBlock. + bool duration_set_; + + // Pointer to the data. Owned by this class. + uint8_t* frame_; + + // Flag telling if the data should set the key flag of a block. + bool is_key_; + + // Length of the data. + uint64_t length_; + + // Mkv track number the data is associated with. + uint64_t track_number_; + + // Timestamp of the data in nanoseconds. + uint64_t timestamp_; + + // Discard padding for the frame. + int64_t discard_padding_; + + // Reference block timestamp. + int64_t reference_block_timestamp_; + + // Flag indicating if |reference_block_timestamp_| has been set. + bool reference_block_timestamp_set_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Frame); +}; + +/////////////////////////////////////////////////////////////// +// Class to hold one cue point in a Cues element. +class CuePoint { + public: + CuePoint(); + ~CuePoint(); + + // Returns the size in bytes for the entire CuePoint element. + uint64_t Size() const; + + // Output the CuePoint element to the writer. Returns true on success. + bool Write(IMkvWriter* writer) const; + + void set_time(uint64_t time) { time_ = time; } + uint64_t time() const { return time_; } + void set_track(uint64_t track) { track_ = track; } + uint64_t track() const { return track_; } + void set_cluster_pos(uint64_t cluster_pos) { cluster_pos_ = cluster_pos; } + uint64_t cluster_pos() const { return cluster_pos_; } + void set_block_number(uint64_t block_number) { block_number_ = block_number; } + uint64_t block_number() const { return block_number_; } + void set_output_block_number(bool output_block_number) { + output_block_number_ = output_block_number; + } + bool output_block_number() const { return output_block_number_; } + + private: + // Returns the size in bytes for the payload of the CuePoint element. + uint64_t PayloadSize() const; + + // Absolute timecode according to the segment time base. + uint64_t time_; + + // The Track element associated with the CuePoint. + uint64_t track_; + + // The position of the Cluster containing the Block. + uint64_t cluster_pos_; + + // Number of the Block within the Cluster, starting from 1. + uint64_t block_number_; + + // If true the muxer will write out the block number for the cue if the + // block number is different than the default of 1. Default is set to true. + bool output_block_number_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(CuePoint); +}; + +/////////////////////////////////////////////////////////////// +// Cues element. +class Cues { + public: + Cues(); + ~Cues(); + + // Adds a cue point to the Cues element. Returns true on success. + bool AddCue(CuePoint* cue); + + // Returns the cue point by index. Returns NULL if there is no cue point + // match. + CuePoint* GetCueByIndex(int32_t index) const; + + // Returns the total size of the Cues element + uint64_t Size(); + + // Output the Cues element to the writer. Returns true on success. + bool Write(IMkvWriter* writer) const; + + int32_t cue_entries_size() const { return cue_entries_size_; } + void set_output_block_number(bool output_block_number) { + output_block_number_ = output_block_number; + } + bool output_block_number() const { return output_block_number_; } + + private: + // Number of allocated elements in |cue_entries_|. + int32_t cue_entries_capacity_; + + // Number of CuePoints in |cue_entries_|. + int32_t cue_entries_size_; + + // CuePoint list. + CuePoint** cue_entries_; + + // If true the muxer will write out the block number for the cue if the + // block number is different than the default of 1. Default is set to true. + bool output_block_number_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Cues); +}; + +/////////////////////////////////////////////////////////////// +// ContentEncAESSettings element +class ContentEncAESSettings { + public: + enum { kCTR = 1 }; + + ContentEncAESSettings(); + ~ContentEncAESSettings() {} + + // Returns the size in bytes for the ContentEncAESSettings element. + uint64_t Size() const; + + // Writes out the ContentEncAESSettings element to |writer|. Returns true on + // success. + bool Write(IMkvWriter* writer) const; + + uint64_t cipher_mode() const { return cipher_mode_; } + + private: + // Returns the size in bytes for the payload of the ContentEncAESSettings + // element. + uint64_t PayloadSize() const; + + // Sub elements + uint64_t cipher_mode_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(ContentEncAESSettings); +}; + +/////////////////////////////////////////////////////////////// +// ContentEncoding element +// Elements used to describe if the track data has been encrypted or +// compressed with zlib or header stripping. +// Currently only whole frames can be encrypted with AES. This dictates that +// ContentEncodingOrder will be 0, ContentEncodingScope will be 1, +// ContentEncodingType will be 1, and ContentEncAlgo will be 5. +class ContentEncoding { + public: + ContentEncoding(); + ~ContentEncoding(); + + // Sets the content encryption id. Copies |length| bytes from |id| to + // |enc_key_id_|. Returns true on success. + bool SetEncryptionID(const uint8_t* id, uint64_t length); + + // Returns the size in bytes for the ContentEncoding element. + uint64_t Size() const; + + // Writes out the ContentEncoding element to |writer|. Returns true on + // success. + bool Write(IMkvWriter* writer) const; + + uint64_t enc_algo() const { return enc_algo_; } + uint64_t encoding_order() const { return encoding_order_; } + uint64_t encoding_scope() const { return encoding_scope_; } + uint64_t encoding_type() const { return encoding_type_; } + ContentEncAESSettings* enc_aes_settings() { return &enc_aes_settings_; } + + private: + // Returns the size in bytes for the encoding elements. + uint64_t EncodingSize(uint64_t compresion_size, + uint64_t encryption_size) const; + + // Returns the size in bytes for the encryption elements. + uint64_t EncryptionSize() const; + + // Track element names + uint64_t enc_algo_; + uint8_t* enc_key_id_; + uint64_t encoding_order_; + uint64_t encoding_scope_; + uint64_t encoding_type_; + + // ContentEncAESSettings element. + ContentEncAESSettings enc_aes_settings_; + + // Size of the ContentEncKeyID data in bytes. + uint64_t enc_key_id_length_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(ContentEncoding); +}; + +/////////////////////////////////////////////////////////////// +// Colour element. +struct PrimaryChromaticity { + PrimaryChromaticity(float x_val, float y_val) : x(x_val), y(y_val) {} + PrimaryChromaticity() : x(0), y(0) {} + ~PrimaryChromaticity() {} + uint64_t PrimaryChromaticityPayloadSize(libwebm::MkvId x_id, + libwebm::MkvId y_id) const; + bool Write(IMkvWriter* writer, libwebm::MkvId x_id, + libwebm::MkvId y_id) const; + + float x; + float y; +}; + +class MasteringMetadata { + public: + static const float kValueNotPresent; + + MasteringMetadata() + : luminance_max(kValueNotPresent), + luminance_min(kValueNotPresent), + r_(NULL), + g_(NULL), + b_(NULL), + white_point_(NULL) {} + ~MasteringMetadata() { + delete r_; + delete g_; + delete b_; + delete white_point_; + } + + // Returns total size of the MasteringMetadata element. + uint64_t MasteringMetadataSize() const; + bool Write(IMkvWriter* writer) const; + + // Copies non-null chromaticity. + bool SetChromaticity(const PrimaryChromaticity* r, + const PrimaryChromaticity* g, + const PrimaryChromaticity* b, + const PrimaryChromaticity* white_point); + const PrimaryChromaticity* r() const { return r_; } + const PrimaryChromaticity* g() const { return g_; } + const PrimaryChromaticity* b() const { return b_; } + const PrimaryChromaticity* white_point() const { return white_point_; } + + float luminance_max; + float luminance_min; + + private: + // Returns size of MasteringMetadata child elements. + uint64_t PayloadSize() const; + + PrimaryChromaticity* r_; + PrimaryChromaticity* g_; + PrimaryChromaticity* b_; + PrimaryChromaticity* white_point_; +}; + +class Colour { + public: + static const uint64_t kValueNotPresent; + Colour() + : matrix_coefficients(kValueNotPresent), + bits_per_channel(kValueNotPresent), + chroma_subsampling_horz(kValueNotPresent), + chroma_subsampling_vert(kValueNotPresent), + cb_subsampling_horz(kValueNotPresent), + cb_subsampling_vert(kValueNotPresent), + chroma_siting_horz(kValueNotPresent), + chroma_siting_vert(kValueNotPresent), + range(kValueNotPresent), + transfer_characteristics(kValueNotPresent), + primaries(kValueNotPresent), + max_cll(kValueNotPresent), + max_fall(kValueNotPresent), + mastering_metadata_(NULL) {} + ~Colour() { delete mastering_metadata_; } + + // Returns total size of the Colour element. + uint64_t ColourSize() const; + bool Write(IMkvWriter* writer) const; + + // Deep copies |mastering_metadata|. + bool SetMasteringMetadata(const MasteringMetadata& mastering_metadata); + + const MasteringMetadata* mastering_metadata() const { + return mastering_metadata_; + } + + uint64_t matrix_coefficients; + uint64_t bits_per_channel; + uint64_t chroma_subsampling_horz; + uint64_t chroma_subsampling_vert; + uint64_t cb_subsampling_horz; + uint64_t cb_subsampling_vert; + uint64_t chroma_siting_horz; + uint64_t chroma_siting_vert; + uint64_t range; + uint64_t transfer_characteristics; + uint64_t primaries; + uint64_t max_cll; + uint64_t max_fall; + + private: + // Returns size of Colour child elements. + uint64_t PayloadSize() const; + + MasteringMetadata* mastering_metadata_; +}; + +/////////////////////////////////////////////////////////////// +// Track element. +class Track { + public: + // The |seed| parameter is used to synthesize a UID for the track. + explicit Track(unsigned int* seed); + virtual ~Track(); + + // Adds a ContentEncoding element to the Track. Returns true on success. + virtual bool AddContentEncoding(); + + // Returns the ContentEncoding by index. Returns NULL if there is no + // ContentEncoding match. + ContentEncoding* GetContentEncodingByIndex(uint32_t index) const; + + // Returns the size in bytes for the payload of the Track element. + virtual uint64_t PayloadSize() const; + + // Returns the size in bytes of the Track element. + virtual uint64_t Size() const; + + // Output the Track element to the writer. Returns true on success. + virtual bool Write(IMkvWriter* writer) const; + + // Sets the CodecPrivate element of the Track element. Copies |length| + // bytes from |codec_private| to |codec_private_|. Returns true on success. + bool SetCodecPrivate(const uint8_t* codec_private, uint64_t length); + + void set_codec_id(const char* codec_id); + const char* codec_id() const { return codec_id_; } + const uint8_t* codec_private() const { return codec_private_; } + void set_language(const char* language); + const char* language() const { return language_; } + void set_max_block_additional_id(uint64_t max_block_additional_id) { + max_block_additional_id_ = max_block_additional_id; + } + uint64_t max_block_additional_id() const { return max_block_additional_id_; } + void set_name(const char* name); + const char* name() const { return name_; } + void set_number(uint64_t number) { number_ = number; } + uint64_t number() const { return number_; } + void set_type(uint64_t type) { type_ = type; } + uint64_t type() const { return type_; } + void set_uid(uint64_t uid) { uid_ = uid; } + uint64_t uid() const { return uid_; } + void set_codec_delay(uint64_t codec_delay) { codec_delay_ = codec_delay; } + uint64_t codec_delay() const { return codec_delay_; } + void set_seek_pre_roll(uint64_t seek_pre_roll) { + seek_pre_roll_ = seek_pre_roll; + } + uint64_t seek_pre_roll() const { return seek_pre_roll_; } + void set_default_duration(uint64_t default_duration) { + default_duration_ = default_duration; + } + uint64_t default_duration() const { return default_duration_; } + + uint64_t codec_private_length() const { return codec_private_length_; } + uint32_t content_encoding_entries_size() const { + return content_encoding_entries_size_; + } + + private: + // Track element names. + char* codec_id_; + uint8_t* codec_private_; + char* language_; + uint64_t max_block_additional_id_; + char* name_; + uint64_t number_; + uint64_t type_; + uint64_t uid_; + uint64_t codec_delay_; + uint64_t seek_pre_roll_; + uint64_t default_duration_; + + // Size of the CodecPrivate data in bytes. + uint64_t codec_private_length_; + + // ContentEncoding element list. + ContentEncoding** content_encoding_entries_; + + // Number of ContentEncoding elements added. + uint32_t content_encoding_entries_size_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Track); +}; + +/////////////////////////////////////////////////////////////// +// Track that has video specific elements. +class VideoTrack : public Track { + public: + // Supported modes for stereo 3D. + enum StereoMode { + kMono = 0, + kSideBySideLeftIsFirst = 1, + kTopBottomRightIsFirst = 2, + kTopBottomLeftIsFirst = 3, + kSideBySideRightIsFirst = 11 + }; + + enum AlphaMode { kNoAlpha = 0, kAlpha = 1 }; + + // The |seed| parameter is used to synthesize a UID for the track. + explicit VideoTrack(unsigned int* seed); + virtual ~VideoTrack(); + + // Returns the size in bytes for the payload of the Track element plus the + // video specific elements. + virtual uint64_t PayloadSize() const; + + // Output the VideoTrack element to the writer. Returns true on success. + virtual bool Write(IMkvWriter* writer) const; + + // Sets the video's stereo mode. Returns true on success. + bool SetStereoMode(uint64_t stereo_mode); + + // Sets the video's alpha mode. Returns true on success. + bool SetAlphaMode(uint64_t alpha_mode); + + void set_display_height(uint64_t height) { display_height_ = height; } + uint64_t display_height() const { return display_height_; } + void set_display_width(uint64_t width) { display_width_ = width; } + uint64_t display_width() const { return display_width_; } + + void set_crop_left(uint64_t crop_left) { crop_left_ = crop_left; } + uint64_t crop_left() const { return crop_left_; } + void set_crop_right(uint64_t crop_right) { crop_right_ = crop_right; } + uint64_t crop_right() const { return crop_right_; } + void set_crop_top(uint64_t crop_top) { crop_top_ = crop_top; } + uint64_t crop_top() const { return crop_top_; } + void set_crop_bottom(uint64_t crop_bottom) { crop_bottom_ = crop_bottom; } + uint64_t crop_bottom() const { return crop_bottom_; } + + void set_frame_rate(double frame_rate) { frame_rate_ = frame_rate; } + double frame_rate() const { return frame_rate_; } + void set_height(uint64_t height) { height_ = height; } + uint64_t height() const { return height_; } + uint64_t stereo_mode() { return stereo_mode_; } + uint64_t alpha_mode() { return alpha_mode_; } + void set_width(uint64_t width) { width_ = width; } + uint64_t width() const { return width_; } + + Colour* colour() { return colour_; } + + // Deep copies |colour|. + bool SetColour(const Colour& colour); + + private: + // Returns the size in bytes of the Video element. + uint64_t VideoPayloadSize() const; + + // Video track element names. + uint64_t display_height_; + uint64_t display_width_; + uint64_t crop_left_; + uint64_t crop_right_; + uint64_t crop_top_; + uint64_t crop_bottom_; + double frame_rate_; + uint64_t height_; + uint64_t stereo_mode_; + uint64_t alpha_mode_; + uint64_t width_; + + Colour* colour_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(VideoTrack); +}; + +/////////////////////////////////////////////////////////////// +// Track that has audio specific elements. +class AudioTrack : public Track { + public: + // The |seed| parameter is used to synthesize a UID for the track. + explicit AudioTrack(unsigned int* seed); + virtual ~AudioTrack(); + + // Returns the size in bytes for the payload of the Track element plus the + // audio specific elements. + virtual uint64_t PayloadSize() const; + + // Output the AudioTrack element to the writer. Returns true on success. + virtual bool Write(IMkvWriter* writer) const; + + void set_bit_depth(uint64_t bit_depth) { bit_depth_ = bit_depth; } + uint64_t bit_depth() const { return bit_depth_; } + void set_channels(uint64_t channels) { channels_ = channels; } + uint64_t channels() const { return channels_; } + void set_sample_rate(double sample_rate) { sample_rate_ = sample_rate; } + double sample_rate() const { return sample_rate_; } + + private: + // Audio track element names. + uint64_t bit_depth_; + uint64_t channels_; + double sample_rate_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(AudioTrack); +}; + +/////////////////////////////////////////////////////////////// +// Tracks element +class Tracks { + public: + // Audio and video type defined by the Matroska specs. + enum { kVideo = 0x1, kAudio = 0x2 }; + + static const char kOpusCodecId[]; + static const char kVorbisCodecId[]; + static const char kVp8CodecId[]; + static const char kVp9CodecId[]; + static const char kVp10CodecId[]; + static const char kWebVttCaptionsId[]; + static const char kWebVttDescriptionsId[]; + static const char kWebVttMetadataId[]; + static const char kWebVttSubtitlesId[]; + + Tracks(); + ~Tracks(); + + // Adds a Track element to the Tracks object. |track| will be owned and + // deleted by the Tracks object. Returns true on success. |number| is the + // number to use for the track. |number| must be >= 0. If |number| == 0 + // then the muxer will decide on the track number. + bool AddTrack(Track* track, int32_t number); + + // Returns the track by index. Returns NULL if there is no track match. + const Track* GetTrackByIndex(uint32_t idx) const; + + // Search the Tracks and return the track that matches |tn|. Returns NULL + // if there is no track match. + Track* GetTrackByNumber(uint64_t track_number) const; + + // Returns true if the track number is an audio track. + bool TrackIsAudio(uint64_t track_number) const; + + // Returns true if the track number is a video track. + bool TrackIsVideo(uint64_t track_number) const; + + // Output the Tracks element to the writer. Returns true on success. + bool Write(IMkvWriter* writer) const; + + uint32_t track_entries_size() const { return track_entries_size_; } + + private: + // Track element list. + Track** track_entries_; + + // Number of Track elements added. + uint32_t track_entries_size_; + + // Whether or not Tracks element has already been written via IMkvWriter. + mutable bool wrote_tracks_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Tracks); +}; + +/////////////////////////////////////////////////////////////// +// Chapter element +// +class Chapter { + public: + // Set the identifier for this chapter. (This corresponds to the + // Cue Identifier line in WebVTT.) + // TODO(matthewjheaney): the actual serialization of this item in + // MKV is pending. + bool set_id(const char* id); + + // Converts the nanosecond start and stop times of this chapter to + // their corresponding timecode values, and stores them that way. + void set_time(const Segment& segment, uint64_t start_time_ns, + uint64_t end_time_ns); + + // Sets the uid for this chapter. Primarily used to enable + // deterministic output from the muxer. + void set_uid(const uint64_t uid) { uid_ = uid; } + + // Add a title string to this chapter, per the semantics described + // here: + // http://www.matroska.org/technical/specs/index.html + // + // The title ("chapter string") is a UTF-8 string. + // + // The language has ISO 639-2 representation, described here: + // http://www.loc.gov/standards/iso639-2/englangn.html + // http://www.loc.gov/standards/iso639-2/php/English_list.php + // If you specify NULL as the language value, this implies + // English ("eng"). + // + // The country value corresponds to the codes listed here: + // http://www.iana.org/domains/root/db/ + // + // The function returns false if the string could not be allocated. + bool add_string(const char* title, const char* language, const char* country); + + private: + friend class Chapters; + + // For storage of chapter titles that differ by language. + class Display { + public: + // Establish representation invariant for new Display object. + void Init(); + + // Reclaim resources, in anticipation of destruction. + void Clear(); + + // Copies the title to the |title_| member. Returns false on + // error. + bool set_title(const char* title); + + // Copies the language to the |language_| member. Returns false + // on error. + bool set_language(const char* language); + + // Copies the country to the |country_| member. Returns false on + // error. + bool set_country(const char* country); + + // If |writer| is non-NULL, serialize the Display sub-element of + // the Atom into the stream. Returns the Display element size on + // success, 0 if error. + uint64_t WriteDisplay(IMkvWriter* writer) const; + + private: + char* title_; + char* language_; + char* country_; + }; + + Chapter(); + ~Chapter(); + + // Establish the representation invariant for a newly-created + // Chapter object. The |seed| parameter is used to create the UID + // for this chapter atom. + void Init(unsigned int* seed); + + // Copies this Chapter object to a different one. This is used when + // expanding a plain array of Chapter objects (see Chapters). + void ShallowCopy(Chapter* dst) const; + + // Reclaim resources used by this Chapter object, pending its + // destruction. + void Clear(); + + // If there is no storage remaining on the |displays_| array for a + // new display object, creates a new, longer array and copies the + // existing Display objects to the new array. Returns false if the + // array cannot be expanded. + bool ExpandDisplaysArray(); + + // If |writer| is non-NULL, serialize the Atom sub-element into the + // stream. Returns the total size of the element on success, 0 if + // error. + uint64_t WriteAtom(IMkvWriter* writer) const; + + // The string identifier for this chapter (corresponds to WebVTT cue + // identifier). + char* id_; + + // Start timecode of the chapter. + uint64_t start_timecode_; + + // Stop timecode of the chapter. + uint64_t end_timecode_; + + // The binary identifier for this chapter. + uint64_t uid_; + + // The Atom element can contain multiple Display sub-elements, as + // the same logical title can be rendered in different languages. + Display* displays_; + + // The physical length (total size) of the |displays_| array. + int displays_size_; + + // The logical length (number of active elements) on the |displays_| + // array. + int displays_count_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Chapter); +}; + +/////////////////////////////////////////////////////////////// +// Chapters element +// +class Chapters { + public: + Chapters(); + ~Chapters(); + + Chapter* AddChapter(unsigned int* seed); + + // Returns the number of chapters that have been added. + int Count() const; + + // Output the Chapters element to the writer. Returns true on success. + bool Write(IMkvWriter* writer) const; + + private: + // Expands the chapters_ array if there is not enough space to contain + // another chapter object. Returns true on success. + bool ExpandChaptersArray(); + + // If |writer| is non-NULL, serialize the Edition sub-element of the + // Chapters element into the stream. Returns the Edition element + // size on success, 0 if error. + uint64_t WriteEdition(IMkvWriter* writer) const; + + // Total length of the chapters_ array. + int chapters_size_; + + // Number of active chapters on the chapters_ array. + int chapters_count_; + + // Array for storage of chapter objects. + Chapter* chapters_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Chapters); +}; + +/////////////////////////////////////////////////////////////// +// Tag element +// +class Tag { + public: + bool add_simple_tag(const char* tag_name, const char* tag_string); + + private: + // Tags calls Clear and the destructor of Tag + friend class Tags; + + // For storage of simple tags + class SimpleTag { + public: + // Establish representation invariant for new SimpleTag object. + void Init(); + + // Reclaim resources, in anticipation of destruction. + void Clear(); + + // Copies the title to the |tag_name_| member. Returns false on + // error. + bool set_tag_name(const char* tag_name); + + // Copies the language to the |tag_string_| member. Returns false + // on error. + bool set_tag_string(const char* tag_string); + + // If |writer| is non-NULL, serialize the SimpleTag sub-element of + // the Atom into the stream. Returns the SimpleTag element size on + // success, 0 if error. + uint64_t Write(IMkvWriter* writer) const; + + private: + char* tag_name_; + char* tag_string_; + }; + + Tag(); + ~Tag(); + + // Copies this Tag object to a different one. This is used when + // expanding a plain array of Tag objects (see Tags). + void ShallowCopy(Tag* dst) const; + + // Reclaim resources used by this Tag object, pending its + // destruction. + void Clear(); + + // If there is no storage remaining on the |simple_tags_| array for a + // new display object, creates a new, longer array and copies the + // existing SimpleTag objects to the new array. Returns false if the + // array cannot be expanded. + bool ExpandSimpleTagsArray(); + + // If |writer| is non-NULL, serialize the Tag sub-element into the + // stream. Returns the total size of the element on success, 0 if + // error. + uint64_t Write(IMkvWriter* writer) const; + + // The Atom element can contain multiple SimpleTag sub-elements + SimpleTag* simple_tags_; + + // The physical length (total size) of the |simple_tags_| array. + int simple_tags_size_; + + // The logical length (number of active elements) on the |simple_tags_| + // array. + int simple_tags_count_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Tag); +}; + +/////////////////////////////////////////////////////////////// +// Tags element +// +class Tags { + public: + Tags(); + ~Tags(); + + Tag* AddTag(); + + // Returns the number of tags that have been added. + int Count() const; + + // Output the Tags element to the writer. Returns true on success. + bool Write(IMkvWriter* writer) const; + + private: + // Expands the tags_ array if there is not enough space to contain + // another tag object. Returns true on success. + bool ExpandTagsArray(); + + // Total length of the tags_ array. + int tags_size_; + + // Number of active tags on the tags_ array. + int tags_count_; + + // Array for storage of tag objects. + Tag* tags_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Tags); +}; + +/////////////////////////////////////////////////////////////// +// Cluster element +// +// Notes: +// |Init| must be called before any other method in this class. +class Cluster { + public: + // |timecode| is the absolute timecode of the cluster. |cues_pos| is the + // position for the cluster within the segment that should be written in + // the cues element. |timecode_scale| is the timecode scale of the segment. + Cluster(uint64_t timecode, int64_t cues_pos, uint64_t timecode_scale, + bool write_last_frame_with_duration = false, + bool fixed_size_timecode = false); + ~Cluster(); + + bool Init(IMkvWriter* ptr_writer); + + // Adds a frame to be output in the file. The frame is written out through + // |writer_| if successful. Returns true on success. + bool AddFrame(const Frame* frame); + + // Adds a frame to be output in the file. The frame is written out through + // |writer_| if successful. Returns true on success. + // Inputs: + // data: Pointer to the data + // length: Length of the data + // track_number: Track to add the data to. Value returned by Add track + // functions. The range of allowed values is [1, 126]. + // timecode: Absolute (not relative to cluster) timestamp of the + // frame, expressed in timecode units. + // is_key: Flag telling whether or not this frame is a key frame. + bool AddFrame(const uint8_t* data, uint64_t length, uint64_t track_number, + uint64_t timecode, // timecode units (absolute) + bool is_key); + + // Adds a frame to be output in the file. The frame is written out through + // |writer_| if successful. Returns true on success. + // Inputs: + // data: Pointer to the data + // length: Length of the data + // additional: Pointer to the additional data + // additional_length: Length of the additional data + // add_id: Value of BlockAddID element + // track_number: Track to add the data to. Value returned by Add track + // functions. The range of allowed values is [1, 126]. + // abs_timecode: Absolute (not relative to cluster) timestamp of the + // frame, expressed in timecode units. + // is_key: Flag telling whether or not this frame is a key frame. + bool AddFrameWithAdditional(const uint8_t* data, uint64_t length, + const uint8_t* additional, + uint64_t additional_length, uint64_t add_id, + uint64_t track_number, uint64_t abs_timecode, + bool is_key); + + // Adds a frame to be output in the file. The frame is written out through + // |writer_| if successful. Returns true on success. + // Inputs: + // data: Pointer to the data. + // length: Length of the data. + // discard_padding: DiscardPadding element value. + // track_number: Track to add the data to. Value returned by Add track + // functions. The range of allowed values is [1, 126]. + // abs_timecode: Absolute (not relative to cluster) timestamp of the + // frame, expressed in timecode units. + // is_key: Flag telling whether or not this frame is a key frame. + bool AddFrameWithDiscardPadding(const uint8_t* data, uint64_t length, + int64_t discard_padding, + uint64_t track_number, uint64_t abs_timecode, + bool is_key); + + // Writes a frame of metadata to the output medium; returns true on + // success. + // Inputs: + // data: Pointer to the data + // length: Length of the data + // track_number: Track to add the data to. Value returned by Add track + // functions. The range of allowed values is [1, 126]. + // timecode: Absolute (not relative to cluster) timestamp of the + // metadata frame, expressed in timecode units. + // duration: Duration of metadata frame, in timecode units. + // + // The metadata frame is written as a block group, with a duration + // sub-element but no reference time sub-elements (indicating that + // it is considered a keyframe, per Matroska semantics). + bool AddMetadata(const uint8_t* data, uint64_t length, uint64_t track_number, + uint64_t timecode, uint64_t duration); + + // Increments the size of the cluster's data in bytes. + void AddPayloadSize(uint64_t size); + + // Closes the cluster so no more data can be written to it. Will update the + // cluster's size if |writer_| is seekable. Returns true on success. This + // variant of Finalize() fails when |write_last_frame_with_duration_| is set + // to true. + bool Finalize(); + + // Closes the cluster so no more data can be written to it. Will update the + // cluster's size if |writer_| is seekable. Returns true on success. + // Inputs: + // set_last_frame_duration: Boolean indicating whether or not the duration + // of the last frame should be set. If set to + // false, the |duration| value is ignored and + // |write_last_frame_with_duration_| will not be + // honored. + // duration: Duration of the Cluster in timecode scale. + bool Finalize(bool set_last_frame_duration, uint64_t duration); + + // Returns the size in bytes for the entire Cluster element. + uint64_t Size() const; + + // Given |abs_timecode|, calculates timecode relative to most recent timecode. + // Returns -1 on failure, or a relative timecode. + int64_t GetRelativeTimecode(int64_t abs_timecode) const; + + int64_t size_position() const { return size_position_; } + int32_t blocks_added() const { return blocks_added_; } + uint64_t payload_size() const { return payload_size_; } + int64_t position_for_cues() const { return position_for_cues_; } + uint64_t timecode() const { return timecode_; } + uint64_t timecode_scale() const { return timecode_scale_; } + void set_write_last_frame_with_duration(bool write_last_frame_with_duration) { + write_last_frame_with_duration_ = write_last_frame_with_duration; + } + bool write_last_frame_with_duration() const { + return write_last_frame_with_duration_; + } + + private: + // Iterator type for the |stored_frames_| map. + typedef std::map >::iterator FrameMapIterator; + + // Utility method that confirms that blocks can still be added, and that the + // cluster header has been written. Used by |DoWriteFrame*|. Returns true + // when successful. + bool PreWriteBlock(); + + // Utility method used by the |DoWriteFrame*| methods that handles the book + // keeping required after each block is written. + void PostWriteBlock(uint64_t element_size); + + // Does some verification and calls WriteFrame. + bool DoWriteFrame(const Frame* const frame); + + // Either holds back the given frame, or writes it out depending on whether or + // not |write_last_frame_with_duration_| is set. + bool QueueOrWriteFrame(const Frame* const frame); + + // Outputs the Cluster header to |writer_|. Returns true on success. + bool WriteClusterHeader(); + + // Number of blocks added to the cluster. + int32_t blocks_added_; + + // Flag telling if the cluster has been closed. + bool finalized_; + + // Flag indicating whether the cluster's timecode will always be written out + // using 8 bytes. + bool fixed_size_timecode_; + + // Flag telling if the cluster's header has been written. + bool header_written_; + + // The size of the cluster elements in bytes. + uint64_t payload_size_; + + // The file position used for cue points. + const int64_t position_for_cues_; + + // The file position of the cluster's size element. + int64_t size_position_; + + // The absolute timecode of the cluster. + const uint64_t timecode_; + + // The timecode scale of the Segment containing the cluster. + const uint64_t timecode_scale_; + + // Flag indicating whether the last frame of the cluster should be written as + // a Block with Duration. If set to true, then it will result in holding back + // of frames and the parameterized version of Finalize() must be called to + // finish writing the Cluster. + bool write_last_frame_with_duration_; + + // Map used to hold back frames, if required. Track number is the key. + std::map > stored_frames_; + + // Map from track number to the timestamp of the last block written for that + // track. + std::map last_block_timestamp_; + + // Pointer to the writer object. Not owned by this class. + IMkvWriter* writer_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Cluster); +}; + +/////////////////////////////////////////////////////////////// +// SeekHead element +class SeekHead { + public: + SeekHead(); + ~SeekHead(); + + // TODO(fgalligan): Change this to reserve a certain size. Then check how + // big the seek entry to be added is as not every seek entry will be the + // maximum size it could be. + // Adds a seek entry to be written out when the element is finalized. |id| + // must be the coded mkv element id. |pos| is the file position of the + // element. Returns true on success. + bool AddSeekEntry(uint32_t id, uint64_t pos); + + // Writes out SeekHead and SeekEntry elements. Returns true on success. + bool Finalize(IMkvWriter* writer) const; + + // Returns the id of the Seek Entry at the given index. Returns -1 if index is + // out of range. + uint32_t GetId(int index) const; + + // Returns the position of the Seek Entry at the given index. Returns -1 if + // index is out of range. + uint64_t GetPosition(int index) const; + + // Sets the Seek Entry id and position at given index. + // Returns true on success. + bool SetSeekEntry(int index, uint32_t id, uint64_t position); + + // Reserves space by writing out a Void element which will be updated with + // a SeekHead element later. Returns true on success. + bool Write(IMkvWriter* writer); + + // We are going to put a cap on the number of Seek Entries. + const static int32_t kSeekEntryCount = 5; + + private: + // Returns the maximum size in bytes of one seek entry. + uint64_t MaxEntrySize() const; + + // Seek entry id element list. + uint32_t seek_entry_id_[kSeekEntryCount]; + + // Seek entry pos element list. + uint64_t seek_entry_pos_[kSeekEntryCount]; + + // The file position of SeekHead element. + int64_t start_pos_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(SeekHead); +}; + +/////////////////////////////////////////////////////////////// +// Segment Information element +class SegmentInfo { + public: + SegmentInfo(); + ~SegmentInfo(); + + // Will update the duration if |duration_| is > 0.0. Returns true on success. + bool Finalize(IMkvWriter* writer) const; + + // Sets |muxing_app_| and |writing_app_|. + bool Init(); + + // Output the Segment Information element to the writer. Returns true on + // success. + bool Write(IMkvWriter* writer); + + void set_duration(double duration) { duration_ = duration; } + double duration() const { return duration_; } + void set_muxing_app(const char* app); + const char* muxing_app() const { return muxing_app_; } + void set_timecode_scale(uint64_t scale) { timecode_scale_ = scale; } + uint64_t timecode_scale() const { return timecode_scale_; } + void set_writing_app(const char* app); + const char* writing_app() const { return writing_app_; } + void set_date_utc(int64_t date_utc) { date_utc_ = date_utc; } + int64_t date_utc() const { return date_utc_; } + + private: + // Segment Information element names. + // Initially set to -1 to signify that a duration has not been set and should + // not be written out. + double duration_; + // Set to libwebm-%d.%d.%d.%d, major, minor, build, revision. + char* muxing_app_; + uint64_t timecode_scale_; + // Initially set to libwebm-%d.%d.%d.%d, major, minor, build, revision. + char* writing_app_; + // LLONG_MIN when DateUTC is not set. + int64_t date_utc_; + + // The file position of the duration element. + int64_t duration_pos_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(SegmentInfo); +}; + +/////////////////////////////////////////////////////////////// +// This class represents the main segment in a WebM file. Currently only +// supports one Segment element. +// +// Notes: +// |Init| must be called before any other method in this class. +class Segment { + public: + enum Mode { kLive = 0x1, kFile = 0x2 }; + + enum CuesPosition { + kAfterClusters = 0x0, // Position Cues after Clusters - Default + kBeforeClusters = 0x1 // Position Cues before Clusters + }; + + const static uint32_t kDefaultDocTypeVersion = 2; + const static uint64_t kDefaultMaxClusterDuration = 30000000000ULL; + + Segment(); + ~Segment(); + + // Initializes |SegmentInfo| and returns result. Always returns false when + // |ptr_writer| is NULL. + bool Init(IMkvWriter* ptr_writer); + + // Adds a generic track to the segment. Returns the newly-allocated + // track object (which is owned by the segment) on success, NULL on + // error. |number| is the number to use for the track. |number| + // must be >= 0. If |number| == 0 then the muxer will decide on the + // track number. + Track* AddTrack(int32_t number); + + // Adds a Vorbis audio track to the segment. Returns the number of the track + // on success, 0 on error. |number| is the number to use for the audio track. + // |number| must be >= 0. If |number| == 0 then the muxer will decide on + // the track number. + uint64_t AddAudioTrack(int32_t sample_rate, int32_t channels, int32_t number); + + // Adds an empty chapter to the chapters of this segment. Returns + // non-NULL on success. After adding the chapter, the caller should + // populate its fields via the Chapter member functions. + Chapter* AddChapter(); + + // Adds an empty tag to the tags of this segment. Returns + // non-NULL on success. After adding the tag, the caller should + // populate its fields via the Tag member functions. + Tag* AddTag(); + + // Adds a cue point to the Cues element. |timestamp| is the time in + // nanoseconds of the cue's time. |track| is the Track of the Cue. This + // function must be called after AddFrame to calculate the correct + // BlockNumber for the CuePoint. Returns true on success. + bool AddCuePoint(uint64_t timestamp, uint64_t track); + + // Adds a frame to be output in the file. Returns true on success. + // Inputs: + // data: Pointer to the data + // length: Length of the data + // track_number: Track to add the data to. Value returned by Add track + // functions. + // timestamp: Timestamp of the frame in nanoseconds from 0. + // is_key: Flag telling whether or not this frame is a key frame. + bool AddFrame(const uint8_t* data, uint64_t length, uint64_t track_number, + uint64_t timestamp_ns, bool is_key); + + // Writes a frame of metadata to the output medium; returns true on + // success. + // Inputs: + // data: Pointer to the data + // length: Length of the data + // track_number: Track to add the data to. Value returned by Add track + // functions. + // timecode: Absolute timestamp of the metadata frame, expressed + // in nanosecond units. + // duration: Duration of metadata frame, in nanosecond units. + // + // The metadata frame is written as a block group, with a duration + // sub-element but no reference time sub-elements (indicating that + // it is considered a keyframe, per Matroska semantics). + bool AddMetadata(const uint8_t* data, uint64_t length, uint64_t track_number, + uint64_t timestamp_ns, uint64_t duration_ns); + + // Writes a frame with additional data to the output medium; returns true on + // success. + // Inputs: + // data: Pointer to the data. + // length: Length of the data. + // additional: Pointer to additional data. + // additional_length: Length of additional data. + // add_id: Additional ID which identifies the type of additional data. + // track_number: Track to add the data to. Value returned by Add track + // functions. + // timestamp: Absolute timestamp of the frame, expressed in nanosecond + // units. + // is_key: Flag telling whether or not this frame is a key frame. + bool AddFrameWithAdditional(const uint8_t* data, uint64_t length, + const uint8_t* additional, + uint64_t additional_length, uint64_t add_id, + uint64_t track_number, uint64_t timestamp, + bool is_key); + + // Writes a frame with DiscardPadding to the output medium; returns true on + // success. + // Inputs: + // data: Pointer to the data. + // length: Length of the data. + // discard_padding: DiscardPadding element value. + // track_number: Track to add the data to. Value returned by Add track + // functions. + // timestamp: Absolute timestamp of the frame, expressed in nanosecond + // units. + // is_key: Flag telling whether or not this frame is a key frame. + bool AddFrameWithDiscardPadding(const uint8_t* data, uint64_t length, + int64_t discard_padding, + uint64_t track_number, uint64_t timestamp, + bool is_key); + + // Writes a Frame to the output medium. Chooses the correct way of writing + // the frame (Block vs SimpleBlock) based on the parameters passed. + // Inputs: + // frame: frame object + bool AddGenericFrame(const Frame* frame); + + // Adds a VP8 video track to the segment. Returns the number of the track on + // success, 0 on error. |number| is the number to use for the video track. + // |number| must be >= 0. If |number| == 0 then the muxer will decide on + // the track number. + uint64_t AddVideoTrack(int32_t width, int32_t height, int32_t number); + + // Sets which track to use for the Cues element. Must have added the track + // before calling this function. Returns true on success. |track_number| is + // returned by the Add track functions. + bool CuesTrack(uint64_t track_number); + + // This will force the muxer to create a new Cluster when the next frame is + // added. + void ForceNewClusterOnNextFrame(); + + // Writes out any frames that have not been written out. Finalizes the last + // cluster. May update the size and duration of the segment. May output the + // Cues element. May finalize the SeekHead element. Returns true on success. + bool Finalize(); + + // Returns the Cues object. + Cues* GetCues() { return &cues_; } + + // Returns the Segment Information object. + const SegmentInfo* GetSegmentInfo() const { return &segment_info_; } + SegmentInfo* GetSegmentInfo() { return &segment_info_; } + + // Search the Tracks and return the track that matches |track_number|. + // Returns NULL if there is no track match. + Track* GetTrackByNumber(uint64_t track_number) const; + + // Toggles whether to output a cues element. + void OutputCues(bool output_cues); + + // Toggles whether to write the last frame in each Cluster with Duration. + void AccurateClusterDuration(bool accurate_cluster_duration); + + // Toggles whether to write the Cluster Timecode using exactly 8 bytes. + void UseFixedSizeClusterTimecode(bool fixed_size_cluster_timecode); + + uint64_t cues_track() const { return cues_track_; } + void set_max_cluster_duration(uint64_t max_cluster_duration) { + max_cluster_duration_ = max_cluster_duration; + } + uint64_t max_cluster_duration() const { return max_cluster_duration_; } + void set_max_cluster_size(uint64_t max_cluster_size) { + max_cluster_size_ = max_cluster_size; + } + uint64_t max_cluster_size() const { return max_cluster_size_; } + void set_mode(Mode mode) { mode_ = mode; } + Mode mode() const { return mode_; } + CuesPosition cues_position() const { return cues_position_; } + bool output_cues() const { return output_cues_; } + const SegmentInfo* segment_info() const { return &segment_info_; } + + // Returns true when codec IDs are valid for WebM. + bool DocTypeIsWebm() const; + + private: + // Checks if header information has been output and initialized. If not it + // will output the Segment element and initialize the SeekHead elment and + // Cues elements. + bool CheckHeaderInfo(); + + // Sets |doc_type_version_| based on the current element requirements. + void UpdateDocTypeVersion(); + + // Sets |name| according to how many chunks have been written. |ext| is the + // file extension. |name| must be deleted by the calling app. Returns true + // on success. + bool UpdateChunkName(const char* ext, char** name) const; + + // Returns the maximum offset within the segment's payload. When chunking + // this function is needed to determine offsets of elements within the + // chunked files. Returns -1 on error. + int64_t MaxOffset(); + + // Adds the frame to our frame array. + bool QueueFrame(Frame* frame); + + // Output all frames that are queued. Returns -1 on error, otherwise + // it returns the number of frames written. + int WriteFramesAll(); + + // Output all frames that are queued that have an end time that is less + // then |timestamp|. Returns true on success and if there are no frames + // queued. + bool WriteFramesLessThan(uint64_t timestamp); + + // Outputs the segment header, Segment Information element, SeekHead element, + // and Tracks element to |writer_|. + bool WriteSegmentHeader(); + + // Given a frame with the specified timestamp (nanosecond units) and + // keyframe status, determine whether a new cluster should be + // created, before writing enqueued frames and the frame itself. The + // function returns one of the following values: + // -1 = error: an out-of-order frame was detected + // 0 = do not create a new cluster, and write frame to the existing cluster + // 1 = create a new cluster, and write frame to that new cluster + // 2 = create a new cluster, and re-run test + int TestFrame(uint64_t track_num, uint64_t timestamp_ns, bool key) const; + + // Create a new cluster, using the earlier of the first enqueued + // frame, or the indicated time. Returns true on success. + bool MakeNewCluster(uint64_t timestamp_ns); + + // Checks whether a new cluster needs to be created, and if so + // creates a new cluster. Returns false if creation of a new cluster + // was necessary but creation was not successful. + bool DoNewClusterProcessing(uint64_t track_num, uint64_t timestamp_ns, + bool key); + + // Adjusts Cue Point values (to place Cues before Clusters) so that they + // reflect the correct offsets. + void MoveCuesBeforeClusters(); + + // This function recursively computes the correct cluster offsets (this is + // done to move the Cues before Clusters). It recursively updates the change + // in size (which indicates a change in cluster offset) until no sizes change. + // Parameters: + // diff - indicates the difference in size of the Cues element that needs to + // accounted for. + // index - index in the list of Cues which is currently being adjusted. + // cue_size - sum of size of all the CuePoint elements. + void MoveCuesBeforeClustersHelper(uint64_t diff, int index, + uint64_t* cue_size); + + // Seeds the random number generator used to make UIDs. + unsigned int seed_; + + // WebM elements + Cues cues_; + SeekHead seek_head_; + SegmentInfo segment_info_; + Tracks tracks_; + Chapters chapters_; + Tags tags_; + + // File position offset where the Clusters end. + int64_t cluster_end_offset_; + + // List of clusters. + Cluster** cluster_list_; + + // Number of cluster pointers allocated in the cluster list. + int32_t cluster_list_capacity_; + + // Number of clusters in the cluster list. + int32_t cluster_list_size_; + + // Indicates whether Cues should be written before or after Clusters + CuesPosition cues_position_; + + // Track number that is associated with the cues element for this segment. + uint64_t cues_track_; + + // Tells the muxer to force a new cluster on the next Block. + bool force_new_cluster_; + + // List of stored audio frames. These variables are used to store frames so + // the muxer can follow the guideline "Audio blocks that contain the video + // key frame's timecode should be in the same cluster as the video key frame + // block." + Frame** frames_; + + // Number of frame pointers allocated in the frame list. + int32_t frames_capacity_; + + // Number of frames in the frame list. + int32_t frames_size_; + + // Flag telling if a video track has been added to the segment. + bool has_video_; + + // Flag telling if the segment's header has been written. + bool header_written_; + + // Duration of the last block in nanoseconds. + uint64_t last_block_duration_; + + // Last timestamp in nanoseconds added to a cluster. + uint64_t last_timestamp_; + + // Last timestamp in nanoseconds by track number added to a cluster. + uint64_t last_track_timestamp_[kMaxTrackNumber]; + + // Maximum time in nanoseconds for a cluster duration. This variable is a + // guideline and some clusters may have a longer duration. Default is 30 + // seconds. + uint64_t max_cluster_duration_; + + // Maximum size in bytes for a cluster. This variable is a guideline and + // some clusters may have a larger size. Default is 0 which signifies that + // the muxer will decide the size. + uint64_t max_cluster_size_; + + // The mode that segment is in. If set to |kLive| the writer must not + // seek backwards. + Mode mode_; + + // Flag telling the muxer that a new cue point should be added. + bool new_cuepoint_; + + // TODO(fgalligan): Should we add support for more than one Cues element? + // Flag whether or not the muxer should output a Cues element. + bool output_cues_; + + // Flag whether or not the last frame in each Cluster will have a Duration + // element in it. + bool accurate_cluster_duration_; + + // Flag whether or not to write the Cluster Timecode using exactly 8 bytes. + bool fixed_size_cluster_timecode_; + + // The size of the EBML header, used to validate the header if + // WriteEbmlHeader() is called more than once. + int32_t ebml_header_size_; + + // The file position of the segment's payload. + int64_t payload_pos_; + + // The file position of the element's size. + int64_t size_position_; + + // Current DocTypeVersion (|doc_type_version_|) and that written in + // WriteSegmentHeader(). + // WriteEbmlHeader() will be called from Finalize() if |doc_type_version_| + // differs from |doc_type_version_written_|. + uint32_t doc_type_version_; + uint32_t doc_type_version_written_; + + // Pointer to the writer objects. Not owned by this class. + IMkvWriter* writer_cluster_; + IMkvWriter* writer_cues_; + IMkvWriter* writer_header_; + + LIBWEBM_DISALLOW_COPY_AND_ASSIGN(Segment); +}; + +} // namespace mkvmuxer + +#endif // MKVMUXER_MKVMUXER_H_ diff --git a/src/client/bundled/mkvmuxer/mkvmuxertypes.h b/src/client/bundled/mkvmuxer/mkvmuxertypes.h new file mode 100644 index 000000000..e5db12160 --- /dev/null +++ b/src/client/bundled/mkvmuxer/mkvmuxertypes.h @@ -0,0 +1,28 @@ +// Copyright (c) 2012 The WebM project authors. All Rights Reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. An additional intellectual property rights grant can be found +// in the file PATENTS. All contributing project authors may +// be found in the AUTHORS file in the root of the source tree. + +#ifndef MKVMUXER_MKVMUXERTYPES_H_ +#define MKVMUXER_MKVMUXERTYPES_H_ + +namespace mkvmuxer { +typedef unsigned char uint8; +typedef short int16; +typedef int int32; +typedef unsigned int uint32; +typedef long long int64; +typedef unsigned long long uint64; +} // namespace mkvmuxer + +// Copied from Chromium basictypes.h +// A macro to disallow the copy constructor and operator= functions +// This should be used in the private: declarations for a class +#define LIBWEBM_DISALLOW_COPY_AND_ASSIGN(TypeName) \ + TypeName(const TypeName&); \ + void operator=(const TypeName&) + +#endif // MKVMUXER_MKVMUXERTYPES_HPP_ diff --git a/src/client/bundled/mkvmuxer/mkvmuxerutil.cc b/src/client/bundled/mkvmuxer/mkvmuxerutil.cc new file mode 100644 index 000000000..6e79cba49 --- /dev/null +++ b/src/client/bundled/mkvmuxer/mkvmuxerutil.cc @@ -0,0 +1,648 @@ +// Copyright (c) 2012 The WebM project authors. All Rights Reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. An additional intellectual property rights grant can be found +// in the file PATENTS. All contributing project authors may +// be found in the AUTHORS file in the root of the source tree. + +#include "mkvmuxer/mkvmuxerutil.h" + +#ifdef __ANDROID__ +#include +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include "common/webmids.h" +#include "mkvmuxer/mkvmuxer.h" + +namespace mkvmuxer { + +namespace { + +// Date elements are always 8 octets in size. +const int kDateElementSize = 8; + +uint64 WriteBlock(IMkvWriter* writer, const Frame* const frame, int64 timecode, + uint64 timecode_scale) { + uint64 block_additional_elem_size = 0; + uint64 block_addid_elem_size = 0; + uint64 block_more_payload_size = 0; + uint64 block_more_elem_size = 0; + uint64 block_additions_payload_size = 0; + uint64 block_additions_elem_size = 0; + if (frame->additional()) { + block_additional_elem_size = + EbmlElementSize(libwebm::kMkvBlockAdditional, frame->additional(), + frame->additional_length()); + block_addid_elem_size = EbmlElementSize( + libwebm::kMkvBlockAddID, static_cast(frame->add_id())); + + block_more_payload_size = + block_addid_elem_size + block_additional_elem_size; + block_more_elem_size = + EbmlMasterElementSize(libwebm::kMkvBlockMore, block_more_payload_size) + + block_more_payload_size; + block_additions_payload_size = block_more_elem_size; + block_additions_elem_size = + EbmlMasterElementSize(libwebm::kMkvBlockAdditions, + block_additions_payload_size) + + block_additions_payload_size; + } + + uint64 discard_padding_elem_size = 0; + if (frame->discard_padding() != 0) { + discard_padding_elem_size = + EbmlElementSize(libwebm::kMkvDiscardPadding, + static_cast(frame->discard_padding())); + } + + const uint64 reference_block_timestamp = + frame->reference_block_timestamp() / timecode_scale; + uint64 reference_block_elem_size = 0; + if (!frame->is_key()) { + reference_block_elem_size = + EbmlElementSize(libwebm::kMkvReferenceBlock, reference_block_timestamp); + } + + const uint64 duration = frame->duration() / timecode_scale; + uint64 block_duration_elem_size = 0; + if (duration > 0) + block_duration_elem_size = + EbmlElementSize(libwebm::kMkvBlockDuration, duration); + + const uint64 block_payload_size = 4 + frame->length(); + const uint64 block_elem_size = + EbmlMasterElementSize(libwebm::kMkvBlock, block_payload_size) + + block_payload_size; + + const uint64 block_group_payload_size = + block_elem_size + block_additions_elem_size + block_duration_elem_size + + discard_padding_elem_size + reference_block_elem_size; + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvBlockGroup, + block_group_payload_size)) { + return 0; + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvBlock, block_payload_size)) + return 0; + + if (WriteUInt(writer, frame->track_number())) + return 0; + + if (SerializeInt(writer, timecode, 2)) + return 0; + + // For a Block, flags is always 0. + if (SerializeInt(writer, 0, 1)) + return 0; + + if (writer->Write(frame->frame(), static_cast(frame->length()))) + return 0; + + if (frame->additional()) { + if (!WriteEbmlMasterElement(writer, libwebm::kMkvBlockAdditions, + block_additions_payload_size)) { + return 0; + } + + if (!WriteEbmlMasterElement(writer, libwebm::kMkvBlockMore, + block_more_payload_size)) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvBlockAddID, + static_cast(frame->add_id()))) + return 0; + + if (!WriteEbmlElement(writer, libwebm::kMkvBlockAdditional, + frame->additional(), frame->additional_length())) { + return 0; + } + } + + if (frame->discard_padding() != 0 && + !WriteEbmlElement(writer, libwebm::kMkvDiscardPadding, + static_cast(frame->discard_padding()))) { + return false; + } + + if (!frame->is_key() && + !WriteEbmlElement(writer, libwebm::kMkvReferenceBlock, + reference_block_timestamp)) { + return false; + } + + if (duration > 0 && + !WriteEbmlElement(writer, libwebm::kMkvBlockDuration, duration)) { + return false; + } + return EbmlMasterElementSize(libwebm::kMkvBlockGroup, + block_group_payload_size) + + block_group_payload_size; +} + +uint64 WriteSimpleBlock(IMkvWriter* writer, const Frame* const frame, + int64 timecode) { + if (WriteID(writer, libwebm::kMkvSimpleBlock)) + return 0; + + const int32 size = static_cast(frame->length()) + 4; + if (WriteUInt(writer, size)) + return 0; + + if (WriteUInt(writer, static_cast(frame->track_number()))) + return 0; + + if (SerializeInt(writer, timecode, 2)) + return 0; + + uint64 flags = 0; + if (frame->is_key()) + flags |= 0x80; + + if (SerializeInt(writer, flags, 1)) + return 0; + + if (writer->Write(frame->frame(), static_cast(frame->length()))) + return 0; + + return GetUIntSize(libwebm::kMkvSimpleBlock) + GetCodedUIntSize(size) + 4 + + frame->length(); +} + +} // namespace + +int32 GetCodedUIntSize(uint64 value) { + if (value < 0x000000000000007FULL) + return 1; + else if (value < 0x0000000000003FFFULL) + return 2; + else if (value < 0x00000000001FFFFFULL) + return 3; + else if (value < 0x000000000FFFFFFFULL) + return 4; + else if (value < 0x00000007FFFFFFFFULL) + return 5; + else if (value < 0x000003FFFFFFFFFFULL) + return 6; + else if (value < 0x0001FFFFFFFFFFFFULL) + return 7; + return 8; +} + +int32 GetUIntSize(uint64 value) { + if (value < 0x0000000000000100ULL) + return 1; + else if (value < 0x0000000000010000ULL) + return 2; + else if (value < 0x0000000001000000ULL) + return 3; + else if (value < 0x0000000100000000ULL) + return 4; + else if (value < 0x0000010000000000ULL) + return 5; + else if (value < 0x0001000000000000ULL) + return 6; + else if (value < 0x0100000000000000ULL) + return 7; + return 8; +} + +int32 GetIntSize(int64 value) { + // Doubling the requested value ensures positive values with their high bit + // set are written with 0-padding to avoid flipping the signedness. + const uint64 v = (value < 0) ? value ^ -1LL : value; + return GetUIntSize(2 * v); +} + +uint64 EbmlMasterElementSize(uint64 type, uint64 value) { + // Size of EBML ID + int32 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += GetCodedUIntSize(value); + + return ebml_size; +} + +uint64 EbmlElementSize(uint64 type, int64 value) { + // Size of EBML ID + int32 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += GetIntSize(value); + + // Size of Datasize + ebml_size++; + + return ebml_size; +} + +uint64 EbmlElementSize(uint64 type, uint64 value) { + return EbmlElementSize(type, value, 0); +} + +uint64 EbmlElementSize(uint64 type, uint64 value, uint64 fixed_size) { + // Size of EBML ID + int32 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += (fixed_size > 0) ? fixed_size : GetUIntSize(value); + + // Size of Datasize + ebml_size++; + + return ebml_size; +} + +uint64 EbmlElementSize(uint64 type, float /* value */) { + // Size of EBML ID + uint64 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += sizeof(float); + + // Size of Datasize + ebml_size++; + + return ebml_size; +} + +uint64 EbmlElementSize(uint64 type, const char* value) { + if (!value) + return 0; + + // Size of EBML ID + uint64 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += strlen(value); + + // Size of Datasize + ebml_size++; + + return ebml_size; +} + +uint64 EbmlElementSize(uint64 type, const uint8* value, uint64 size) { + if (!value) + return 0; + + // Size of EBML ID + uint64 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += size; + + // Size of Datasize + ebml_size += GetCodedUIntSize(size); + + return ebml_size; +} + +uint64 EbmlDateElementSize(uint64 type) { + // Size of EBML ID + uint64 ebml_size = GetUIntSize(type); + + // Datasize + ebml_size += kDateElementSize; + + // Size of Datasize + ebml_size++; + + return ebml_size; +} + +int32 SerializeInt(IMkvWriter* writer, int64 value, int32 size) { + if (!writer || size < 1 || size > 8) + return -1; + + for (int32 i = 1; i <= size; ++i) { + const int32 byte_count = size - i; + const int32 bit_count = byte_count * 8; + + const int64 bb = value >> bit_count; + const uint8 b = static_cast(bb); + + const int32 status = writer->Write(&b, 1); + + if (status < 0) + return status; + } + + return 0; +} + +int32 SerializeFloat(IMkvWriter* writer, float f) { + if (!writer) + return -1; + + assert(sizeof(uint32) == sizeof(float)); + // This union is merely used to avoid a reinterpret_cast from float& to + // uint32& which will result in violation of strict aliasing. + union U32 { + uint32 u32; + float f; + } value; + value.f = f; + + for (int32 i = 1; i <= 4; ++i) { + const int32 byte_count = 4 - i; + const int32 bit_count = byte_count * 8; + + const uint8 byte = static_cast(value.u32 >> bit_count); + + const int32 status = writer->Write(&byte, 1); + + if (status < 0) + return status; + } + + return 0; +} + +int32 WriteUInt(IMkvWriter* writer, uint64 value) { + if (!writer) + return -1; + + int32 size = GetCodedUIntSize(value); + + return WriteUIntSize(writer, value, size); +} + +int32 WriteUIntSize(IMkvWriter* writer, uint64 value, int32 size) { + if (!writer || size < 0 || size > 8) + return -1; + + if (size > 0) { + const uint64 bit = 1LL << (size * 7); + + if (value > (bit - 2)) + return -1; + + value |= bit; + } else { + size = 1; + int64 bit; + + for (;;) { + bit = 1LL << (size * 7); + const uint64 max = bit - 2; + + if (value <= max) + break; + + ++size; + } + + if (size > 8) + return false; + + value |= bit; + } + + return SerializeInt(writer, value, size); +} + +int32 WriteID(IMkvWriter* writer, uint64 type) { + if (!writer) + return -1; + + writer->ElementStartNotify(type, writer->Position()); + + const int32 size = GetUIntSize(type); + + return SerializeInt(writer, type, size); +} + +bool WriteEbmlMasterElement(IMkvWriter* writer, uint64 type, uint64 size) { + if (!writer) + return false; + + if (WriteID(writer, type)) + return false; + + if (WriteUInt(writer, size)) + return false; + + return true; +} + +bool WriteEbmlElement(IMkvWriter* writer, uint64 type, uint64 value) { + return WriteEbmlElement(writer, type, value, 0); +} + +bool WriteEbmlElement(IMkvWriter* writer, uint64 type, uint64 value, + uint64 fixed_size) { + if (!writer) + return false; + + if (WriteID(writer, type)) + return false; + + uint64 size = GetUIntSize(value); + if (fixed_size > 0) { + if (size > fixed_size) + return false; + size = fixed_size; + } + if (WriteUInt(writer, size)) + return false; + + if (SerializeInt(writer, value, static_cast(size))) + return false; + + return true; +} + +bool WriteEbmlElement(IMkvWriter* writer, uint64 type, int64 value) { + if (!writer) + return false; + + if (WriteID(writer, type)) + return 0; + + const uint64 size = GetIntSize(value); + if (WriteUInt(writer, size)) + return false; + + if (SerializeInt(writer, value, static_cast(size))) + return false; + + return true; +} + +bool WriteEbmlElement(IMkvWriter* writer, uint64 type, float value) { + if (!writer) + return false; + + if (WriteID(writer, type)) + return false; + + if (WriteUInt(writer, 4)) + return false; + + if (SerializeFloat(writer, value)) + return false; + + return true; +} + +bool WriteEbmlElement(IMkvWriter* writer, uint64 type, const char* value) { + if (!writer || !value) + return false; + + if (WriteID(writer, type)) + return false; + + const uint64 length = strlen(value); + if (WriteUInt(writer, length)) + return false; + + if (writer->Write(value, static_cast(length))) + return false; + + return true; +} + +bool WriteEbmlElement(IMkvWriter* writer, uint64 type, const uint8* value, + uint64 size) { + if (!writer || !value || size < 1) + return false; + + if (WriteID(writer, type)) + return false; + + if (WriteUInt(writer, size)) + return false; + + if (writer->Write(value, static_cast(size))) + return false; + + return true; +} + +bool WriteEbmlDateElement(IMkvWriter* writer, uint64 type, int64 value) { + if (!writer) + return false; + + if (WriteID(writer, type)) + return false; + + if (WriteUInt(writer, kDateElementSize)) + return false; + + if (SerializeInt(writer, value, kDateElementSize)) + return false; + + return true; +} + +uint64 WriteFrame(IMkvWriter* writer, const Frame* const frame, + Cluster* cluster) { + if (!writer || !frame || !frame->IsValid() || !cluster || + !cluster->timecode_scale()) + return 0; + + // Technically the timecode for a block can be less than the + // timecode for the cluster itself (remember that block timecode + // is a signed, 16-bit integer). However, as a simplification we + // only permit non-negative cluster-relative timecodes for blocks. + const int64 relative_timecode = cluster->GetRelativeTimecode( + frame->timestamp() / cluster->timecode_scale()); + if (relative_timecode < 0 || relative_timecode > kMaxBlockTimecode) + return 0; + + return frame->CanBeSimpleBlock() ? + WriteSimpleBlock(writer, frame, relative_timecode) : + WriteBlock(writer, frame, relative_timecode, + cluster->timecode_scale()); +} + +uint64 WriteVoidElement(IMkvWriter* writer, uint64 size) { + if (!writer) + return false; + + // Subtract one for the void ID and the coded size. + uint64 void_entry_size = size - 1 - GetCodedUIntSize(size - 1); + uint64 void_size = EbmlMasterElementSize(libwebm::kMkvVoid, void_entry_size) + + void_entry_size; + + if (void_size != size) + return 0; + + const int64 payload_position = writer->Position(); + if (payload_position < 0) + return 0; + + if (WriteID(writer, libwebm::kMkvVoid)) + return 0; + + if (WriteUInt(writer, void_entry_size)) + return 0; + + const uint8 value = 0; + for (int32 i = 0; i < static_cast(void_entry_size); ++i) { + if (writer->Write(&value, 1)) + return 0; + } + + const int64 stop_position = writer->Position(); + if (stop_position < 0 || + stop_position - payload_position != static_cast(void_size)) + return 0; + + return void_size; +} + +void GetVersion(int32* major, int32* minor, int32* build, int32* revision) { + *major = 0; + *minor = 2; + *build = 1; + *revision = 0; +} + +uint64 MakeUID(unsigned int* seed) { + uint64 uid = 0; + +#ifdef __MINGW32__ + srand(*seed); +#endif + + for (int i = 0; i < 7; ++i) { // avoid problems with 8-byte values + uid <<= 8; + +// TODO(fgalligan): Move random number generation to platform specific code. +#ifdef _MSC_VER + (void)seed; + const int32 nn = rand(); +#elif __ANDROID__ + int32 temp_num = 1; + int fd = open("/dev/urandom", O_RDONLY); + if (fd != -1) { + read(fd, &temp_num, sizeof(temp_num)); + close(fd); + } + const int32 nn = temp_num; +#elif defined __MINGW32__ + const int32 nn = rand(); +#else + const int32 nn = rand_r(seed); +#endif + const int32 n = 0xFF & (nn >> 4); // throw away low-order bits + + uid |= n; + } + + return uid; +} + +} // namespace mkvmuxer diff --git a/src/client/bundled/mkvmuxer/mkvmuxerutil.h b/src/client/bundled/mkvmuxer/mkvmuxerutil.h new file mode 100644 index 000000000..2c9bdab57 --- /dev/null +++ b/src/client/bundled/mkvmuxer/mkvmuxerutil.h @@ -0,0 +1,102 @@ +// Copyright (c) 2012 The WebM project authors. All Rights Reserved. +// +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file in the root of the source +// tree. An additional intellectual property rights grant can be found +// in the file PATENTS. All contributing project authors may +// be found in the AUTHORS file in the root of the source tree. +#ifndef MKVMUXER_MKVMUXERUTIL_H_ +#define MKVMUXER_MKVMUXERUTIL_H_ + +#include "mkvmuxertypes.h" + +namespace mkvmuxer { +class Cluster; +class Frame; +class IMkvWriter; + +// TODO(tomfinegan): mkvmuxer:: integer types continue to be used here because +// changing them causes pain for downstream projects. It would be nice if a +// solution that allows removal of the mkvmuxer:: integer types while avoiding +// pain for downstream users of libwebm. Considering that mkvmuxerutil.{cc,h} +// are really, for the great majority of cases, EBML size calculation and writer +// functions, perhaps a more EBML focused utility would be the way to go as a +// first step. + +const uint64 kEbmlUnknownValue = 0x01FFFFFFFFFFFFFFULL; +const int64 kMaxBlockTimecode = 0x07FFFLL; + +// Writes out |value| in Big Endian order. Returns 0 on success. +int32 SerializeInt(IMkvWriter* writer, int64 value, int32 size); + +// Returns the size in bytes of the element. +int32 GetUIntSize(uint64 value); +int32 GetIntSize(int64 value); +int32 GetCodedUIntSize(uint64 value); +uint64 EbmlMasterElementSize(uint64 type, uint64 value); +uint64 EbmlElementSize(uint64 type, int64 value); +uint64 EbmlElementSize(uint64 type, uint64 value); +uint64 EbmlElementSize(uint64 type, float value); +uint64 EbmlElementSize(uint64 type, const char* value); +uint64 EbmlElementSize(uint64 type, const uint8* value, uint64 size); +uint64 EbmlDateElementSize(uint64 type); + +// Returns the size in bytes of the element assuming that the element was +// written using |fixed_size| bytes. If |fixed_size| is set to zero, then it +// computes the necessary number of bytes based on |value|. +uint64 EbmlElementSize(uint64 type, uint64 value, uint64 fixed_size); + +// Creates an EBML coded number from |value| and writes it out. The size of +// the coded number is determined by the value of |value|. |value| must not +// be in a coded form. Returns 0 on success. +int32 WriteUInt(IMkvWriter* writer, uint64 value); + +// Creates an EBML coded number from |value| and writes it out. The size of +// the coded number is determined by the value of |size|. |value| must not +// be in a coded form. Returns 0 on success. +int32 WriteUIntSize(IMkvWriter* writer, uint64 value, int32 size); + +// Output an Mkv master element. Returns true if the element was written. +bool WriteEbmlMasterElement(IMkvWriter* writer, uint64 value, uint64 size); + +// Outputs an Mkv ID, calls |IMkvWriter::ElementStartNotify|, and passes the +// ID to |SerializeInt|. Returns 0 on success. +int32 WriteID(IMkvWriter* writer, uint64 type); + +// Output an Mkv non-master element. Returns true if the element was written. +bool WriteEbmlElement(IMkvWriter* writer, uint64 type, uint64 value); +bool WriteEbmlElement(IMkvWriter* writer, uint64 type, int64 value); +bool WriteEbmlElement(IMkvWriter* writer, uint64 type, float value); +bool WriteEbmlElement(IMkvWriter* writer, uint64 type, const char* value); +bool WriteEbmlElement(IMkvWriter* writer, uint64 type, const uint8* value, + uint64 size); +bool WriteEbmlDateElement(IMkvWriter* writer, uint64 type, int64 value); + +// Output an Mkv non-master element using fixed size. The element will be +// written out using exactly |fixed_size| bytes. If |fixed_size| is set to zero +// then it computes the necessary number of bytes based on |value|. Returns true +// if the element was written. +bool WriteEbmlElement(IMkvWriter* writer, uint64 type, uint64 value, + uint64 fixed_size); + +// Output a Mkv Frame. It decides the correct element to write (Block vs +// SimpleBlock) based on the parameters of the Frame. +uint64 WriteFrame(IMkvWriter* writer, const Frame* const frame, + Cluster* cluster); + +// Output a void element. |size| must be the entire size in bytes that will be +// void. The function will calculate the size of the void header and subtract +// it from |size|. +uint64 WriteVoidElement(IMkvWriter* writer, uint64 size); + +// Returns the version number of the muxer in |major|, |minor|, |build|, +// and |revision|. +void GetVersion(int32* major, int32* minor, int32* build, int32* revision); + +// Returns a random number to be used for UID, using |seed| to seed +// the random-number generator (see POSIX rand_r() for semantics). +uint64 MakeUID(unsigned int* seed); + +} // namespace mkvmuxer + +#endif // MKVMUXER_MKVMUXERUTIL_H_ diff --git a/src/client/canvas/aclfilter.cpp b/src/client/canvas/aclfilter.cpp index f6c0759d6..46395d0e3 100644 --- a/src/client/canvas/aclfilter.cpp +++ b/src/client/canvas/aclfilter.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2015-2017 Calle Laakkonen + Copyright (C) 2015-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,10 +21,15 @@ #include "../shared/net/meta.h" #include "../shared/net/meta2.h" -#include "../shared/net/pen.h" +#include "../shared/net/brushes.h" #include "../shared/net/image.h" #include "../shared/net/layer.h" #include "../shared/net/annotation.h" +#include "../shared/net/undo.h" + +#ifndef Q_FALLTHROUGH // not present in Qt 5.6 +#define Q_FALLTHROUGH() (void)0 +#endif namespace canvas { @@ -37,31 +42,33 @@ void AclFilter::reset(int myId, bool localMode) { m_layers.clear(); m_myId = myId; - m_isOperator = localMode; + m_isTrusted = false; m_sessionLocked = false; m_localUserLocked = false; - m_layerCtrlLocked = false; - m_imagesLocked = false; - m_ownLayers = false; - m_lockAnnotationCreation = false; - m_userLayers.clear(); - - m_lockDefault = false; - m_ops.clear(); - m_userlocks.clear(); + m_ops.reset(); + m_trusted.reset(); + m_auth.reset(); + m_userlocks.reset(); m_protectedAnnotations.clear(); if(localMode) - m_ops << myId; + m_ops.set(myId); + setOperator(localMode); - emit localOpChanged(m_isOperator); emit localLockChanged(false); - emit ownLayersChanged(m_ownLayers); - emit layerControlLockChanged(m_layerCtrlLocked); - emit imageCmdLockChanged(m_imagesLocked); - emit lockByDefaultChanged(m_lockDefault); - emit annotationCreationLockChanged(m_lockAnnotationCreation); + + // Default feature access levels + setFeature( Feature::PutImage, Tier::Guest); + setFeature( Feature::RegionMove, Tier::Guest); + setFeature( Feature::Resize, Tier::Op); + setFeature( Feature::Background, Tier::Op); + setFeature( Feature::EditLayers, Tier::Op); + setFeature( Feature::OwnLayers, Tier::Guest); + setFeature(Feature::CreateAnnotation, Tier::Guest); + setFeature( Feature::Laser, Tier::Guest); + setFeature( Feature::Undo, Tier::Guest); + static_assert(FeatureCount == 9, "missing default feature tiers"); } // Get the ID of the layer's creator. This assumes the ID prefixing convention is used. @@ -73,176 +80,200 @@ bool AclFilter::filterMessage(const protocol::Message &msg) { using namespace protocol; - // User list is empty in local mode - const bool isOpUser = m_ops.contains(msg.contextId()); - - // First: check if this is an operator-only command - if(msg.contextId()!=0 && msg.isOpCommand() && !isOpUser) + // Session and user specific locks apply to all Command type messages + if(msg.isCommand() && (m_sessionLocked || m_userlocks.contains(msg.contextId()))) return false; - // Special commands that affect access controls + // This user's access level tier determines which features are available + const Tier tier = userTier(msg.contextId()); + switch(msg.type()) { case MSG_USER_JOIN: - // Set our own default lock. We can't set the user list item's properties - // here, since it hasn't been created yet. - if(isLockedByDefault()) { - if(msg.contextId() == m_myId) - setUserLock(true); - m_userlocks << msg.contextId(); - } + if((static_cast(msg).flags() & UserJoin::FLAG_AUTH)) + m_auth.set(msg.contextId()); // Make sure the user's OP status bits are up to date - emit operatorListChanged(m_ops); + emit operatorListChanged(m_ops.toList()); break; case MSG_USER_LEAVE: { // User left: remove locks - if(m_ops.removeAll(msg.contextId())) { - emit operatorListChanged(m_ops); - } - - if(m_userlocks.removeAll(msg.contextId())>0) - emit userLocksChanged(m_userlocks); - - m_userLayers.remove(msg.contextId()); + m_ops.unset(msg.contextId()); + m_trusted.unset(msg.contextId()); + m_auth.unset(msg.contextId()); + m_userlocks.unset(msg.contextId()); QMutableHashIterator i(m_layers); while(i.hasNext()) { i.next(); - if(i.value().exclusive.removeAll(msg.contextId())>0) { - emit layerAclChange(i.key(), i.value().locked, i.value().exclusive); - } + if(i.value().exclusive.removeAll(msg.contextId())>0) + emit layerAclChanged(i.key()); } // Refresh UI if(msg.contextId() == m_myId) { - m_isOperator = false; - emit localOpChanged(false); + setOperator(false); + setTrusted(false); m_localUserLocked = false; emit localLockChanged(isLocked()); } break; } case MSG_SESSION_OWNER: + // This command is validated by the server updateSessionOwnership(static_cast(msg)); return true; - case MSG_LAYER_ACL: { - const auto &lmsg = static_cast(msg); - if(isOpUser || (isOwnLayers() && layerCreator(lmsg.id()) == msg.contextId())) { - m_layers[lmsg.id()] = LayerAcl(lmsg.locked(), lmsg.exclusive()); - emit layerAclChange(lmsg.id(), lmsg.locked(), lmsg.exclusive()); + + case MSG_TRUSTED_USERS: + // this command is validated by the server + updateTrustedUserList(static_cast(msg)); + return true; + + case MSG_LAYER_ACL: + if( tier <= featureTier(Feature::EditLayers) || + (tier <= featureTier(Feature::OwnLayers) && layerCreator(msg.layer()) == msg.contextId()) + ) { + const auto &lmsg = static_cast(msg); + + if(lmsg.layer() == 0) { + // Layer 0 sets the general session lock. + // Exclusive user list is not used in this case. + if(tier > Tier::Op) + return false; + setSessionLock(lmsg.locked()); + return true; + } + + const Tier tier = Tier(qBound(0, int(lmsg.tier()), TierCount)); + m_layers[lmsg.layer()] = LayerAcl { lmsg.locked(), tier, lmsg.exclusive() }; // Emit this to refresh the UI in case our selected layer was (un)locked. // (We don't actually know which layer is selected in the UI here.) emit localLockChanged(isLocked()); + emit layerAclChanged(lmsg.layer()); + return true; } return false; - } - case MSG_SESSION_ACL: { - const auto &lmsg = static_cast(msg); - - setSessionLock(lmsg.isSessionLocked()); - setLayerControlLock(lmsg.isLayerControlLocked()); - setOwnLayers(lmsg.isOwnLayers()); - setLockImages(lmsg.isImagesLocked()); - setLockByDefault(lmsg.isLockedByDefault()); - setAnnotationCreationLock(lmsg.isAnnotationCreationLocked()); + + case MSG_FEATURE_LEVELS: { + if(tier > Tier::Op) + return false; + + const auto &flmsg = static_cast(msg); + for(int i=0;i Tier::Op) + return false; + const auto &lmsg = static_cast(msg); - m_userlocks = lmsg.ids(); + m_userlocks.setFromList(lmsg.ids()); emit userLocksChanged(lmsg.ids()); - setUserLock(lmsg.ids().contains(m_myId)); - return true; - } - case MSG_TOOLCHANGE: - m_userLayers[msg.contextId()] = static_cast(msg).layer(); + setUserLock(m_userlocks.contains(m_myId)); return true; - default: break; } - // General action filtering when user is locked - if(msg.isCommand() && (m_sessionLocked || m_userlocks.contains(msg.contextId()))) - return false; + case MSG_LAYER_DEFAULT: + return tier == Tier::Op; - // Message specific filtering - switch(msg.type()) { case MSG_CHAT: - // Only OPs can pin messages - if(static_cast(msg).isPin() && !isOpUser) + // Only operators can pin messages + if(static_cast(msg).isPin() && tier > Tier::Op) return false; break; - case MSG_LAYER_CREATE: + case MSG_LASERTRAIL: + return tier <= featureTier(Feature::Laser); + + case MSG_CANVAS_RESIZE: return tier <= featureTier(Feature::Resize); + case MSG_PUTTILE: return tier == Tier::Op; + case MSG_CANVAS_BACKGROUND: return tier <= featureTier(Feature::Background); + + case MSG_LAYER_CREATE: { + if(tier > Tier::Op && layerCreator(msg.layer()) != msg.contextId()) { + qWarning("non-op user %d tried to create layer with context id %d", msg.contextId(), layerCreator(msg.layer())); + return false; + } + + // Must have either general or ownlayer permission to create layers + return tier <= featureTier(Feature::EditLayers) || tier <= featureTier(Feature::OwnLayers); + } case MSG_LAYER_ATTR: + if(static_cast(msg).sublayer()>0 && tier > Tier::Op) { + // Direct sublayer editing is used only by operators during session init + return false; + } + Q_FALLTHROUGH(); + case MSG_LAYER_RETITLE: case MSG_LAYER_DELETE: { - uint16_t layerId=0; - if(msg.type() == MSG_LAYER_CREATE) { - layerId = static_cast(msg).id(); - if(!isOpUser && (layerId>>8) != msg.contextId()) { - qWarning("non-op user %d tried to create layer with context id %d", msg.contextId(), (layerId>>8)); - return false; - } - } else if(msg.type() == MSG_LAYER_ATTR) layerId = static_cast(msg).id(); - else if(msg.type() == MSG_LAYER_RETITLE) layerId = static_cast(msg).id(); - else if(msg.type() == MSG_LAYER_DELETE) layerId = static_cast(msg).id(); - - // In OwnLayer mode, users may create, delete and adjust their own layers. - // Otherwise, session operator privileges are required. - if(isLayerControlLocked() && !isOpUser && !(isOwnLayers() && layerCreator(layerId) == msg.contextId())) + const uint8_t createdBy = layerCreator(msg.layer()); + // EDITLAYERS feature gives permission to edit all layers + // OWNLAYERS feature gives permission to edit layers created by this user + if( + (createdBy != msg.contextId() && tier > featureTier(Feature::EditLayers)) || + (createdBy == msg.contextId() && tier > featureTier(Feature::OwnLayers)) + ) return false; - if(msg.type() == MSG_LAYER_DELETE) { - m_layers.remove(layerId); - } + if(msg.type() == MSG_LAYER_DELETE) + m_layers.remove(msg.layer()); break; } - case MSG_LAYER_ORDER: - return isOpUser || !isLayerControlLocked(); + return tier <= featureTier(Feature::EditLayers); + case MSG_PUTIMAGE: - return !((isImagesLocked() && !isOpUser) || isLayerLockedFor(static_cast(msg).layer(), msg.contextId())); case MSG_FILLRECT: - return !((isImagesLocked() && !isOpUser) || isLayerLockedFor(static_cast(msg).layer(), msg.contextId())); + return tier <= featureTier(Feature::PutImage) && !isLayerLockedFor(msg.layer(), msg.contextId(), tier); - case MSG_PEN_MOVE: - return !isLayerLockedFor(m_userLayers[msg.contextId()], msg.contextId()); + case MSG_DRAWDABS_CLASSIC: + case MSG_DRAWDABS_PIXEL: + return !isLayerLockedFor(msg.layer(), msg.contextId(), tier); - case MSG_ANNOTATION_CREATE: { - const uint16_t annotationId = static_cast(msg).id(); - if(!isOpUser && (annotationId>>8) != msg.contextId()) { - qWarning("non-op user %d tried to create annotation with context id %d", msg.contextId(), (annotationId>>8)); + case MSG_REGION_MOVE: + return tier <= featureTier(Feature::RegionMove) && !isLayerLockedFor(msg.layer(), msg.contextId(), tier); + + case MSG_ANNOTATION_CREATE: + if(tier > featureTier(Feature::CreateAnnotation)) return false; - } - if(m_lockAnnotationCreation && !isOpUser) + + if(tier > Tier::Op && layerCreator(msg.layer()) != msg.contextId()) { + qWarning("non-op user %d tried to create annotation with context id %d", msg.contextId(), layerCreator(msg.layer())); return false; + } break; - } - case MSG_ANNOTATION_EDIT: { - const protocol::AnnotationEdit &ae = static_cast(msg); - if(m_protectedAnnotations.contains(ae.id()) && !isOpUser && (ae.id()>>8)!=msg.contextId()) + case MSG_ANNOTATION_EDIT: + // Non-operators can't edit protected annotations created by other users + if(m_protectedAnnotations.contains(layerCreator(msg.layer())) && tier > Tier::Op && layerCreator(msg.layer()) != msg.contextId()) return false; - if((ae.flags() & protocol::AnnotationEdit::FLAG_PROTECT)) - m_protectedAnnotations.insert(ae.id()); + + if((static_cast(msg).flags() & protocol::AnnotationEdit::FLAG_PROTECT)) + m_protectedAnnotations.insert(msg.layer()); else - m_protectedAnnotations.remove(ae.id()); + m_protectedAnnotations.remove(msg.layer()); break; - } case MSG_ANNOTATION_DELETE: - case MSG_ANNOTATION_RESHAPE: { - uint16_t id = msg.type() == MSG_ANNOTATION_DELETE ? static_cast(msg).id() : static_cast(msg).id(); - if(m_protectedAnnotations.contains(id) && !isOpUser && (id>>8)!=msg.contextId()) + case MSG_ANNOTATION_RESHAPE: + if(m_protectedAnnotations.contains(msg.layer()) && tier > Tier::Op && layerCreator(msg.layer())!=msg.contextId()) + return false; + if(msg.type() == MSG_ANNOTATION_DELETE) + m_protectedAnnotations.remove(msg.layer()); + break; + + case MSG_UNDO: + if(tier > featureTier(Feature::Undo)) + return false; + + // Only operators can override Undos. + if(tier > Tier::Op && static_cast(msg).overrideId()>0) return false; break; - } - case MSG_REGION_MOVE: { - const uint16_t layer = static_cast(msg).layer(); - return !isLayerLockedFor(layer, msg.contextId()); - } default: break; } @@ -253,34 +284,63 @@ bool AclFilter::filterMessage(const protocol::Message &msg) AclFilter::LayerAcl AclFilter::layerAcl(int layerId) const { if(!m_layers.contains(layerId)) - return LayerAcl(); + return LayerAcl { false, Tier::Guest, QList() }; + return m_layers[layerId]; } -bool AclFilter::isLayerLockedFor(int layerId, uint8_t userId) const +bool AclFilter::isLayerLockedFor(int layerId, uint8_t userId, Tier userTier) const { if(!m_layers.contains(layerId)) return false; const LayerAcl &l = m_layers[layerId]; - return l.locked || (!l.exclusive.isEmpty() && !l.exclusive.contains(userId)); + // Locking a layer locks it for everyone + if(l.locked) + return true; + + // If the layer has not been configured for exclusive user access, + // permit access by user tier + if(l.exclusive.isEmpty()) + return l.tier < userTier; + else + return !l.exclusive.contains(userId); } void AclFilter::updateSessionOwnership(const protocol::SessionOwner &msg) { - m_ops = msg.ids(); - if(msg.contextId()!=0 && !m_ops.contains(msg.contextId())) - m_ops << msg.contextId(); - - emit operatorListChanged(m_ops); + m_ops.setFromList(msg.ids()); + emit operatorListChanged(msg.ids()); setOperator(m_ops.contains(m_myId)); } +void AclFilter::updateTrustedUserList(const protocol::TrustedUsers &msg) +{ + m_trusted.setFromList(msg.ids()); + emit trustedUserListChanged(msg.ids()); + setTrusted(m_trusted.contains(m_myId)); +} + void AclFilter::setOperator(bool op) { if(op != m_isOperator) { m_isOperator = op; emit localOpChanged(op); + + // Op and Trusted status change affects available features + for(int i=0;i #include #include +#include namespace protocol { class Message; class SessionOwner; + class TrustedUsers; } namespace canvas { +class UserSet { +public: + UserSet() = default; + + bool contains(uint8_t id) const { return m_users[id]; } + void set(uint8_t id) { m_users[id] = true; } + bool unset(uint8_t id) { bool wasSet = m_users[id]; m_users[id] = false; return wasSet; } + void reset() { m_users.reset(); } + + void setFromList(const QList ids) { + m_users.reset(); + for(int i=0;i toList() const { + QList lst; + for(int i=0;i<256;++i) + if(m_users[i]) + lst << i; + return lst; + } + +private: + std::bitset<256> m_users; +}; + class AclFilter : public QObject { Q_OBJECT public: struct LayerAcl { - bool locked; - QList exclusive; - LayerAcl() : locked(false), exclusive(QList()) {} - LayerAcl(bool locked, const QList exclusive) : locked(locked), exclusive(exclusive) { } + bool locked; // layer is locked for all users + Tier tier; // layer access tier (if not exclusive) + QList exclusive; // if not empty, only these users can draw on this layer }; explicit AclFilter(QObject *parent=nullptr); @@ -57,41 +87,34 @@ class AclFilter : public QObject */ bool filterMessage(const protocol::Message &msg); + //! Does the local user have operator privileges? bool isLocalUserOperator() const { return m_isOperator; } + + //! Has the local user been tagged as trusted? + bool isLocalUserTrusted() const { return m_isTrusted; } + + //! Is there a general session lock in place? bool isSessionLocked() const { return m_sessionLocked; } + + //! Has the local user been locked individually? bool isLocalUserLocked() const { return m_localUserLocked; } + + //! Is the local user locked for any reason? bool isLocked() const { return m_sessionLocked | m_localUserLocked; } - bool isLockedByDefault() const { return m_lockDefault; } - bool isAnnotationCreationLocked() const { return m_lockAnnotationCreation; } - //! Can the local user access this layer's controls? - bool canUseLayerControls(int layerId) const; + //! Check if the local user can draw on the given layer + bool isLayerLocked(int layerId) const { + return isLocked() || isLayerLockedFor(layerId, m_myId, userTier(m_myId)); + } - //! Can the local user create new layers? - bool canCreateLayer() const; + //! Can the local user use this feature? + bool canUseFeature(Feature f) const { return localUserTier() <= featureTier(f); } - //! Are layer controls limited to session operators - bool isLayerControlLocked() const { return m_layerCtrlLocked; } + //! Get the local user's feature access tier + Tier localUserTier() const { return userTier(m_myId); } - /** - * @brief Are users allowed to control layers they've created themselves? - * - * When layer controls are locked, this allows users to adjust their own - * layer properties, but not anyone elses. - * This also allows users to set the access controls for their own layers. - */ - bool isOwnLayers() const { return m_ownLayers; } - - /** - * @brief Are image commands (PutImage, FillRect) locked? - * - * When image commands are locked, commands that can be used to - * upload arbitrary pixel data or affect large areas at once are - * limited to session operators. - */ - bool isImagesLocked() const { return m_imagesLocked; } - - uint16_t sessionAclFlags() const; + //! Get the given feature's access tier + Tier featureTier(Feature f) const { Q_ASSERT(int(f)>=0 && int(f) < FeatureCount); return m_featureTiers[int(f)]; } /** * @brief Get the access controls for an individual layer @@ -100,52 +123,60 @@ class AclFilter : public QObject LayerAcl layerAcl(int id) const; //! Get the list of locked users - QList lockedUsers() const { return m_userlocks; } + QList lockedUsers() const { return m_userlocks.toList(); } signals: void localOpChanged(bool op); - bool localLockChanged(bool lock); - bool layerControlLockChanged(bool lock); - void ownLayersChanged(bool own); - void imageCmdLockChanged(bool lock); - void lockByDefaultChanged(bool lock); - void annotationCreationLockChanged(bool lock); + void localLockChanged(bool lock); + void layerAclChanged(int id); void userLocksChanged(const QList lockedUsers); void operatorListChanged(const QList opUsers); - void layerAclChange(int layerId, bool locked, const QList &exclusive); + void trustedUserListChanged(const QList trustedUsers); + + //! The local user's access to a feature just changed + void featureAccessChanged(Feature feature, bool canUse); + + //! A feature's access tier was changed + void featureTierChanged(Feature feature, Tier tier); private: void setOperator(bool op); + void setTrusted(bool trusted); void setSessionLock(bool lock); void setUserLock(bool lock); - void setLayerControlLock(bool lock); - void setOwnLayers(bool own); - void setLockImages(bool lock); void setLockByDefault(bool lock); - void setAnnotationCreationLock(bool lock); + void setFeature(Feature feature, Tier tier); void updateSessionOwnership(const protocol::SessionOwner &msg); - - bool isLayerLockedFor(int layerId, uint8_t userId) const; - - QHash m_layers; - QHash m_userLayers; // Users' selected layers for brush operations - - int m_myId; - - bool m_isOperator; - bool m_sessionLocked; - bool m_localUserLocked; - bool m_layerCtrlLocked; - bool m_imagesLocked; - bool m_ownLayers; - bool m_lockDefault; - bool m_lockAnnotationCreation; - - QList m_ops; - QList m_userlocks; - QSet m_protectedAnnotations; + void updateTrustedUserList(const protocol::TrustedUsers &msg); + + bool isLayerLockedFor(int layerId, uint8_t userId, Tier userTier) const; + + Tier userTier(int id) const { + if(m_ops.contains(id)) + return Tier::Op; + if(m_trusted.contains(id)) + return Tier::Trusted; + if(m_auth.contains(id)) + return Tier::Auth; + return Tier::Guest; + } + + int m_myId; // the ID of the local user + + bool m_isOperator; // is the local user an operator? + bool m_isTrusted; // does the local user have trusted status? + bool m_sessionLocked; // is the session locked? + bool m_localUserLocked; // is the local user locked individually? + + QHash m_layers; // layer access controls + UserSet m_ops; // list of operators + UserSet m_trusted; // list of trusted users + UserSet m_auth; // list of registered users + UserSet m_userlocks; // list of individually locked users + QSet m_protectedAnnotations; // list of protected annotations + Tier m_featureTiers[FeatureCount]; // feature access tiers }; } diff --git a/src/client/canvas/canvasmodel.cpp b/src/client/canvas/canvasmodel.cpp index 9225ea68f..9208246a1 100644 --- a/src/client/canvas/canvasmodel.cpp +++ b/src/client/canvas/canvasmodel.cpp @@ -30,6 +30,8 @@ #include "core/annotationmodel.h" #include "core/layer.h" #include "ora/orawriter.h" +#include "utils/identicon.h" +#include "net/internalmsg.h" #include "../shared/net/meta.h" #include "../shared/net/meta2.h" @@ -38,10 +40,11 @@ #include #include #include +#include namespace canvas { -CanvasModel::CanvasModel(int localUserId, QObject *parent) +CanvasModel::CanvasModel(uint8_t localUserId, QObject *parent) : QObject(parent), m_selection(nullptr), m_mode(Mode::Offline) { m_layerlist = new LayerListModel(this); @@ -50,8 +53,8 @@ CanvasModel::CanvasModel(int localUserId, QObject *parent) m_aclfilter = new AclFilter(this); connect(m_aclfilter, &AclFilter::operatorListChanged, m_userlist, &UserListModel::updateOperators); + connect(m_aclfilter, &AclFilter::trustedUserListChanged, m_userlist, &UserListModel::updateTrustedUsers); connect(m_aclfilter, &AclFilter::userLocksChanged, m_userlist, &UserListModel::updateLocks); - connect(m_aclfilter, &AclFilter::layerAclChange, m_layerlist, &LayerListModel::updateLayerAcl); m_layerstack = new paintcore::LayerStack(this); m_statetracker = new StateTracker(m_layerstack, m_layerlist, localUserId, this); @@ -61,29 +64,30 @@ CanvasModel::CanvasModel(int localUserId, QObject *parent) m_aclfilter->reset(localUserId, true); m_layerlist->setMyId(localUserId); - m_layerlist->setLayerGetter([this](int id)->paintcore::Layer* { + m_layerlist->setAclFilter(m_aclfilter); + m_layerlist->setLayerGetter([this](int id)->const paintcore::Layer* { return m_layerstack->getLayer(id); }); + m_usercursors->setLayerList(m_layerlist); + connect(m_statetracker, &StateTracker::layerAutoselectRequest, this, &CanvasModel::layerAutoselectRequest); - connect(m_statetracker, &StateTracker::userMarkerAttribs, m_usercursors, &UserCursorModel::setCursorAttributes); connect(m_statetracker, &StateTracker::userMarkerMove, m_usercursors, &UserCursorModel::setCursorPosition); connect(m_statetracker, &StateTracker::userMarkerHide, m_usercursors, &UserCursorModel::hideCursor); connect(m_layerstack, &paintcore::LayerStack::resized, this, &CanvasModel::onCanvasResize); } -int CanvasModel::localUserId() const +uint8_t CanvasModel::localUserId() const { return m_statetracker->localId(); } -void CanvasModel::connectedToServer(int myUserId) +void CanvasModel::connectedToServer(uint8_t myUserId) { Q_ASSERT(m_mode == Mode::Offline); m_layerlist->setMyId(myUserId); - m_layerlist->unlockAll(); m_statetracker->setLocalId(myUserId); m_aclfilter->reset(myUserId, false); m_mode = Mode::Online; @@ -91,10 +95,8 @@ void CanvasModel::connectedToServer(int myUserId) void CanvasModel::disconnectedFromServer() { - Q_ASSERT(m_mode == Mode::Online); m_statetracker->endRemoteContexts(); m_userlist->clearUsers(); - m_layerlist->unlockAll(); m_aclfilter->reset(m_statetracker->localId(), true); m_mode = Mode::Offline; } @@ -139,6 +141,9 @@ void CanvasModel::handleCommand(protocol::MessagePtr cmd) case MSG_CHAT: metaChatMessage(cmd); break; + case MSG_PRIVATE_CHAT: + emit chatMessageReceived(cmd); + break; case MSG_USER_JOIN: metaUserJoin(cmd.cast()); break; @@ -146,8 +151,9 @@ void CanvasModel::handleCommand(protocol::MessagePtr cmd) metaUserLeave(cmd.cast()); break; case MSG_SESSION_OWNER: + case MSG_TRUSTED_USERS: case MSG_USER_ACL: - case MSG_SESSION_ACL: + case MSG_FEATURE_LEVELS: case MSG_LAYER_ACL: // Handled by the ACL filter break; @@ -167,6 +173,9 @@ void CanvasModel::handleCommand(protocol::MessagePtr cmd) case MSG_LAYER_DEFAULT: metaDefaultLayer(cmd.cast()); break; + case MSG_SOFTRESET: + metaSoftReset(cmd->contextId()); + break; default: qWarning("Unhandled meta message %s", qPrintable(cmd->messageName())); } @@ -204,7 +213,7 @@ QList CanvasModel::generateSnapshot(bool forceNew) const if(!m_statetracker->hasFullHistory() || forceNew) { // Generate snapshot - snapshot = SnapshotLoader(m_statetracker->localId(), m_layerstack, m_layerlist->getLayers(), this).loadInitCommands(); + snapshot = SnapshotLoader(m_statetracker->localId(), m_layerstack, this).loadInitCommands(); } else { // Message stream contains (starts with) a snapshot: use it @@ -214,10 +223,14 @@ QList CanvasModel::generateSnapshot(bool forceNew) const if(m_layerlist->defaultLayer() > 0) snapshot.prepend(protocol::MessagePtr(new protocol::DefaultLayer(m_statetracker->localId(), m_layerlist->defaultLayer()))); - // Add layer ACL status - for(const LayerListItem &layer : m_layerlist->getLayers()) { - if(layer.isLockedFor(m_statetracker->localId())) - snapshot << protocol::MessagePtr(new protocol::LayerACL(m_statetracker->localId(), layer.id, true, QList())); + // Add layer ACLs + for(int i=0;ilayerCount();++i) { + const int layerId = m_layerstack->getLayerByIndex(i)->id(); + Q_ASSERT(layerId > 0 && layerId <= 0xffff); // toplevel layers should have IDs in protocol range + + const canvas::AclFilter::LayerAcl acl = aclFilter()->layerAcl(layerId); + if(acl.locked || acl.tier != canvas::Tier::Guest || !acl.exclusive.isEmpty()) + snapshot << protocol::MessagePtr(new protocol::LayerACL(m_statetracker->localId(), uint16_t(layerId), acl.locked, uint8_t(acl.tier), acl.exclusive)); } } @@ -251,14 +264,14 @@ void CanvasModel::pickColor(int x, int y, int layer, int diameter) void CanvasModel::setLayerViewMode(int mode) { - m_layerstack->setViewMode(paintcore::LayerStack::ViewMode(mode)); + m_layerstack->editor().setViewMode(paintcore::LayerStack::ViewMode(mode)); updateLayerViewOptions(); } void CanvasModel::setSelection(Selection *selection) { if(m_selection != selection) { - m_layerstack->removePreviews(); + m_layerstack->editor().removePreviews(); const bool hadSelection = m_selection != nullptr; @@ -280,12 +293,11 @@ void CanvasModel::updateLayerViewOptions() { QSettings cfg; cfg.beginGroup("settings/animation"); - m_layerstack->setOnionskinMode( + m_layerstack->editor().setOnionskinMode( cfg.value("onionskinsbelow", 4).toInt(), cfg.value("onionskinsabove", 4).toInt(), cfg.value("onionskintint", true).toBool() ); - m_layerstack->setViewBackgroundLayer(cfg.value("backgroundlayer", true).toBool()); } /** @@ -294,17 +306,17 @@ void CanvasModel::updateLayerViewOptions() * Find an annotation ID (for this user) that is currently not in use. * @return available ID or 0 if none found */ -int CanvasModel::getAvailableAnnotationId() const +uint16_t CanvasModel::getAvailableAnnotationId() const { - const int prefix = m_statetracker->localId() << 8; - QList takenIds; + const uint16_t prefix = uint16_t(m_statetracker->localId() << 8); + QList takenIds; for(const paintcore::Annotation &a : m_layerstack->annotations()->getAnnotations()) { if((a.id & 0xff00) == prefix) takenIds << a.id; } - for(int i=0;i<256;++i) { - int id = prefix | i; + for(uint16_t i=0;i<256;++i) { + uint16_t id = prefix | i; if(!takenIds.contains(id)) return id; } @@ -316,7 +328,7 @@ QImage CanvasModel::selectionToImage(int layerId) const { QImage img; - paintcore::Layer *layer = m_layerstack->getLayer(layerId); + const paintcore::Layer *layer = m_layerstack->getLayer(layerId); if(layer) img = layer->toImage(); else @@ -373,20 +385,45 @@ void CanvasModel::onCanvasResize(int xoffset, int yoffset, const QSize &oldsize) void CanvasModel::resetCanvas() { setTitle(QString()); - m_layerlist->unlockAll(); - m_layerstack->reset(); + m_layerstack->editor().reset(); m_statetracker->reset(); m_aclfilter->reset(m_statetracker->localId(), false); } void CanvasModel::metaUserJoin(const protocol::UserJoin &msg) { - User u(msg.contextId(), msg.name(), msg.contextId() == m_statetracker->localId(), msg.isAuthenticated(), msg.isModerator()); - if(m_aclfilter->isLockedByDefault()) - u.isLocked = true; + QImage avatar; + if(!msg.avatar().isEmpty()) { + QByteArray avatarData = msg.avatar(); + QBuffer buf(&avatarData); + if(!avatar.load(&buf, "PNG")) + qWarning("Avatar loading failed for user '%s' (#%d)", qPrintable(msg.name()), msg.contextId()); + + // Rescale avatar if its the wrong size + if(avatar.width() != 42 || avatar.height() != 42) { + avatar = avatar.scaled(42, 42); + } + } + if(avatar.isNull()) + avatar = make_identicon(msg.name()); + + const User u { + msg.contextId(), + msg.name(), + QPixmap::fromImage(avatar), + msg.contextId() == m_statetracker->localId(), + false, + false, + msg.isModerator(), + msg.isBot(), + msg.isAuthenticated(), + false, + false + }; m_userlist->addUser(u); m_usercursors->setCursorName(msg.contextId(), msg.name()); + m_usercursors->setCursorAvatar(msg.contextId(), u.avatar); emit userJoined(msg.contextId(), msg.name()); } @@ -421,8 +458,8 @@ void CanvasModel::metaLaserTrail(const protocol::LaserTrail &msg) void CanvasModel::metaMovePointer(const protocol::MovePointer &msg) { - QPointF p(msg.x() / 4.0, msg.y() / 4.0); - m_usercursors->setCursorPosition(msg.contextId(), p); + QPoint p(int(msg.x() / 4.0), int(msg.y() / 4.0)); + m_usercursors->setCursorPosition(msg.contextId(), 0, p); m_lasers->addPoint(msg.contextId(), p); } @@ -433,10 +470,18 @@ void CanvasModel::metaMarkerMessage(const protocol::Marker &msg) void CanvasModel::metaDefaultLayer(const protocol::DefaultLayer &msg) { - m_layerlist->setDefaultLayer(msg.id()); + m_layerlist->setDefaultLayer(msg.layer()); if(!m_statetracker->hasParticipated()) - emit layerAutoselectRequest(msg.id()); + emit layerAutoselectRequest(msg.layer()); } +void CanvasModel::metaSoftReset(uint8_t resetterId) +{ + // Soft reset not fully implemented yet: this client can't initiate soft resets (yet) + if(resetterId == localUserId()) + qWarning("Got soft SoftResetPoint(%d), but that's us!", resetterId); + + m_statetracker->receiveQueuedCommand(protocol::ClientInternal::makeTruncatePoint()); +} } diff --git a/src/client/canvas/canvasmodel.h b/src/client/canvas/canvasmodel.h index 1a26b3fa2..d39f3ae82 100644 --- a/src/client/canvas/canvasmodel.h +++ b/src/client/canvas/canvasmodel.h @@ -63,7 +63,7 @@ class CanvasModel : public QObject Q_OBJECT public: - explicit CanvasModel(int localUserId, QObject *parent = 0); + explicit CanvasModel(uint8_t localUserId, QObject *parent=nullptr); paintcore::LayerStack *layerStack() const { return m_layerstack; } StateTracker *stateTracker() const { return m_statetracker; } @@ -83,14 +83,14 @@ class CanvasModel : public QObject QList generateSnapshot(bool forceNew) const; - int localUserId() const; + uint8_t localUserId() const; - int getAvailableAnnotationId() const; + uint16_t getAvailableAnnotationId() const; QImage selectionToImage(int layerId) const; void pasteFromImage(const QImage &image, const QPoint &defaultPoint, bool forceDefault); - void connectedToServer(int myUserId); + void connectedToServer(uint8_t myUserId); void disconnectedFromServer(); void startPlayback(); void endPlayback(); @@ -157,6 +157,7 @@ private slots: void metaMovePointer(const protocol::MovePointer &msg); void metaMarkerMessage(const protocol::Marker &msg); void metaDefaultLayer(const protocol::DefaultLayer &msg); + void metaSoftReset(uint8_t resetterId); AclFilter *m_aclfilter; UserListModel *m_userlist; diff --git a/src/client/canvas/features.h b/src/client/canvas/features.h new file mode 100644 index 000000000..dbf168ce4 --- /dev/null +++ b/src/client/canvas/features.h @@ -0,0 +1,54 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#ifndef ACL_FEATURES_H +#define ACL_FEATURES_H + +namespace canvas { + +// Features with configurable access levels +// See also shared/net/meta2.{cpp,h} +enum class Feature { + PutImage, + RegionMove, + Resize, + Background, + EditLayers, + OwnLayers, + CreateAnnotation, + Laser, + Undo +}; + +static const int FeatureCount = int(Feature::Undo)+1; + +// Access levels +enum class Tier : unsigned char { + Op, // operators + Trusted, // + users marked as trusted + Auth, // + registered users + Guest // everyone +}; + +static const int TierCount = 4; + +} + +#endif + diff --git a/src/client/canvas/layerlist.cpp b/src/client/canvas/layerlist.cpp index b9a28264f..8fda007f0 100644 --- a/src/client/canvas/layerlist.cpp +++ b/src/client/canvas/layerlist.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2013-2017 Calle Laakkonen + Copyright (C) 2013-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,6 +20,7 @@ #include "layerlist.h" #include "core/layer.h" #include "../shared/net/layer.h" +#include "aclfilter.h" #include #include @@ -30,7 +31,7 @@ namespace canvas { LayerListModel::LayerListModel(QObject *parent) - : QAbstractListModel(parent), m_defaultLayer(0), m_myId(1) + : QAbstractListModel(parent), m_aclfilter(nullptr), m_defaultLayer(0), m_myId(1) { } @@ -52,6 +53,7 @@ QVariant LayerListModel::data(const QModelIndex &index, int role) const case Qt::EditRole: return item.title; case IdRole: return item.id; case IsDefaultRole: return item.id == m_defaultLayer; + case IsLockedRole: return m_aclfilter && m_aclfilter->isLayerLocked(item.id); } } return QVariant(); @@ -128,7 +130,7 @@ void LayerListModel::handleMoveLayer(int oldIdx, int newIdx) emit layerCommand(protocol::MessagePtr(new protocol::LayerOrder(m_myId, layers))); } -int LayerListModel::indexOf(int id) const +int LayerListModel::indexOf(uint16_t id) const { for(int i=0;i=0) @@ -144,26 +146,14 @@ QModelIndex LayerListModel::layerIndex(int id) return QModelIndex(); } -bool LayerListModel::isLayerLockedFor(int layerId, int contextId) const -{ - int i = indexOf(layerId); - if(i>=0) - return m_items.at(i).isLockedFor(contextId); - return false; -} - -void LayerListModel::createLayer(int id, int index, const QString &title) +void LayerListModel::createLayer(uint16_t id, int index, const QString &title) { beginInsertRows(QModelIndex(), index, index); - m_items.insert(index, LayerListItem(id, title)); - if(m_pendingAclChange.contains(id)) { - LayerAcl acl = m_pendingAclChange.take(id); - updateLayerAcl(id, acl.locked, acl.exclusive); - } + m_items.insert(index, LayerListItem { id, title, 1.0, paintcore::BlendMode::MODE_NORMAL, false, false }); endInsertRows(); } -void LayerListModel::deleteLayer(int id) +void LayerListModel::deleteLayer(uint16_t id) { int row = indexOf(id); if(row<0) @@ -180,12 +170,11 @@ void LayerListModel::clear() { beginRemoveRows(QModelIndex(), 0, m_items.size()); m_items.clear(); - m_pendingAclChange.clear(); m_defaultLayer = 0; endRemoveRows(); } -void LayerListModel::changeLayer(int id, float opacity, paintcore::BlendMode::Mode blend) +void LayerListModel::changeLayer(uint16_t id, bool censored, float opacity, paintcore::BlendMode::Mode blend) { int row = indexOf(id); if(row<0) @@ -194,11 +183,12 @@ void LayerListModel::changeLayer(int id, float opacity, paintcore::BlendMode::Mo LayerListItem &item = m_items[row]; item.opacity = opacity; item.blend = blend; + item.censored = censored; const QModelIndex qmi = index(row); emit dataChanged(qmi, qmi); } -void LayerListModel::retitleLayer(int id, const QString &title) +void LayerListModel::retitleLayer(uint16_t id, const QString &title) { int row = indexOf(id); if(row<0) @@ -210,7 +200,7 @@ void LayerListModel::retitleLayer(int id, const QString &title) emit dataChanged(qmi, qmi); } -void LayerListModel::setLayerHidden(int id, bool hidden) +void LayerListModel::setLayerHidden(uint16_t id, bool hidden) { int row = indexOf(id); if(row<0) @@ -222,32 +212,6 @@ void LayerListModel::setLayerHidden(int id, bool hidden) emit dataChanged(qmi, qmi); } -void LayerListModel::updateLayerAcl(int id, bool locked, QList exclusive) -{ - int row = indexOf(id); - if(row<0) { - m_pendingAclChange[id] = LayerAcl { locked, exclusive }; - return; - } - - LayerListItem &item = m_items[row]; - item.locked = locked; - item.exclusive = exclusive; - const QModelIndex qmi = index(row); - emit dataChanged(qmi, qmi); -} - -void LayerListModel::unlockAll() -{ - if(!m_items.isEmpty()) { - for(int i=0;i neworder) { if(neworder.isEmpty()) { @@ -277,7 +241,7 @@ void LayerListModel::setLayers(const QVector &items) endResetModel(); } -void LayerListModel::setDefaultLayer(int id) +void LayerListModel::setDefaultLayer(uint16_t id) { const int oldIdx = indexOf(m_defaultLayer); if(oldIdx >= 0) { @@ -291,14 +255,14 @@ void LayerListModel::setDefaultLayer(int id) } } -const paintcore::Layer *LayerListModel::getLayerData(int id) const +const paintcore::Layer *LayerListModel::getLayerData(uint16_t id) const { if(m_getlayerfn) return m_getlayerfn(id); return nullptr; } -void LayerListModel::previewOpacityChange(int id, float opacity) +void LayerListModel::previewOpacityChange(uint16_t id, float opacity) { emit layerOpacityPreview(id, opacity); } @@ -312,9 +276,9 @@ QVariant LayerMimeData::retrieveData(const QString &mimeType, QVariant::Type typ { Q_UNUSED(mimeType); if(type==QVariant::Image) { - const paintcore::Layer *layer = _source->getLayerData(_id); + const paintcore::Layer *layer = m_source->getLayerData(m_id); if(layer) - return layer->toCroppedImage(0, 0); + return layer->toCroppedImage(nullptr, nullptr); } return QVariant(); diff --git a/src/client/canvas/layerlist.h b/src/client/canvas/layerlist.h index e2bfd7552..cc86c720b 100644 --- a/src/client/canvas/layerlist.h +++ b/src/client/canvas/layerlist.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2013-2017 Calle Laakkonen + Copyright (C) 2013-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,6 +20,7 @@ #define DP_NET_LAYERLIST_H #include "core/blendmodes.h" +#include "features.h" #include #include @@ -37,14 +38,14 @@ namespace paintcore { namespace canvas { -struct LayerListItem { - LayerListItem() : id(0), title(QString()), opacity(1.0), blend(paintcore::BlendMode::MODE_NORMAL), hidden(false), locked(false) {} - LayerListItem(int id_, const QString &title_, float opacity_=1.0, paintcore::BlendMode::Mode blend_=paintcore::BlendMode::MODE_NORMAL, bool hidden_=false, bool locked_=false, const QList &exclusive_=QList()) - : id(id_), title(title_), opacity(opacity_), blend(blend_), hidden(hidden_), locked(locked_), exclusive(exclusive_) - {} +class AclFilter; +struct LayerListItem { //! Layer ID - int id; + // Note: normally, layer ID range is from 0 to 0xffff, but internal + // layers use values outside that range. However, internal layers are not + // shown in the layer list. + uint16_t id; //! Layer title QString title; @@ -58,13 +59,8 @@ struct LayerListItem { //! Layer hidden flag (local only) bool hidden; - //! General layer lock - bool locked; - - //! Exclusive access to these users - QList exclusive; - - bool isLockedFor(int userid) const { return locked || !(exclusive.isEmpty() || exclusive.contains(userid)); } + //! Layer is flagged for censoring + bool censored; }; } @@ -82,9 +78,10 @@ class LayerListModel : public QAbstractListModel { IdRole = Qt::UserRole + 1, TitleRole, IsDefaultRole, + IsLockedRole }; - LayerListModel(QObject *parent=0); + LayerListModel(QObject *parent=nullptr); int rowCount(const QModelIndex &parent=QModelIndex()) const; QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const; @@ -94,36 +91,34 @@ class LayerListModel : public QAbstractListModel { QMimeData *mimeData(const QModelIndexList& indexes) const; bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent); - QModelIndex layerIndex(int id); + QModelIndex layerIndex(uint16_t id); void clear(); - void createLayer(int id, int index, const QString &title); - void deleteLayer(int id); - void changeLayer(int id, float opacity, paintcore::BlendMode::Mode blend); - void retitleLayer(int id, const QString &title); - void setLayerHidden(int id, bool hidden); + void createLayer(uint16_t id, int index, const QString &title); + void deleteLayer(uint16_t id); + void changeLayer(uint16_t id, bool censored, float opacity, paintcore::BlendMode::Mode blend); + void retitleLayer(uint16_t id, const QString &title); + void setLayerHidden(uint16_t id, bool hidden); void reorderLayers(QList neworder); - void unlockAll(); - - bool isLayerLockedFor(int layerId, int contextId) const; QVector getLayers() const { return m_items; } void setLayers(const QVector &items); - void previewOpacityChange(int id, float opacity); + void previewOpacityChange(uint16_t id, float opacity); void setLayerGetter(GetLayerFunction fn) { m_getlayerfn = fn; } - const paintcore::Layer *getLayerData(int id) const; + void setAclFilter(AclFilter *filter) { m_aclfilter = filter; } + const paintcore::Layer *getLayerData(uint16_t id) const; - int myId() const { return m_myId; } - void setMyId(int id) { m_myId = id; } + uint8_t myId() const { return m_myId; } + void setMyId(uint8_t id) { m_myId = id; } /** * @brief Get the default layer to select when logging in * Zero means no default. */ - int defaultLayer() const { return m_defaultLayer; } - void setDefaultLayer(int id); + uint16_t defaultLayer() const { return m_defaultLayer; } + void setDefaultLayer(uint16_t id); /** * @brief Find a free layer ID @@ -138,9 +133,6 @@ class LayerListModel : public QAbstractListModel { */ QString getAvailableLayerName(QString basename) const; -public slots: - void updateLayerAcl(int id, bool locked, QList exclusive); - signals: void layersReordered(); @@ -153,22 +145,13 @@ public slots: private: void handleMoveLayer(int idx, int afterIdx); - int indexOf(int id) const; + int indexOf(uint16_t id) const; - // Terrible hack: the layers are created and edited in the state tracker (which can run concurrently) - // but the ACL changes are updated immediately. So, we must save the ACLs for missing layers in case - // they're created afterwards - // TODO separate the AclFilter better and update the UI as changes are really applied, then get rid of this. - struct LayerAcl { - bool locked; - QList exclusive; - }; - QHash m_pendingAclChange; - QVector m_items; GetLayerFunction m_getlayerfn; - int m_defaultLayer; - int m_myId; + AclFilter *m_aclfilter; + uint16_t m_defaultLayer; + uint8_t m_myId; }; /** @@ -179,11 +162,12 @@ class LayerMimeData : public QMimeData { Q_OBJECT public: - LayerMimeData(const LayerListModel *source, int id) : QMimeData(), _source(source), _id(id) {} + LayerMimeData(const LayerListModel *source, uint16_t id) + : QMimeData(), m_source(source), m_id(id) {} - const LayerListModel *source() const { return _source; } + const LayerListModel *source() const { return m_source; } - int layerId() const { return _id; } + uint16_t layerId() const { return m_id; } QStringList formats() const; @@ -191,8 +175,8 @@ Q_OBJECT QVariant retrieveData(const QString& mimeType, QVariant::Type type) const; private: - const LayerListModel *_source; - int _id; + const LayerListModel *m_source; + uint16_t m_id; }; } diff --git a/src/client/canvas/loader.cpp b/src/client/canvas/loader.cpp index 1a331f12b..2a56b8e4d 100644 --- a/src/client/canvas/loader.cpp +++ b/src/client/canvas/loader.cpp @@ -19,7 +19,6 @@ #include "loader.h" #include "net/client.h" -#include "net/commands.h" #include "ora/orareader.h" #include "canvas/canvasmodel.h" #include "canvas/layerlist.h" @@ -27,6 +26,7 @@ #include "core/layerstack.h" #include "core/layer.h" +#include "core/tilevector.h" #include "../shared/net/layer.h" #include "../shared/net/annotation.h" @@ -45,12 +45,11 @@ using protocol::MessagePtr; QList BlankCanvasLoader::loadInitCommands() { - QList msgs; - - msgs.append(MessagePtr(new protocol::CanvasResize(1, 0, _size.width(), _size.height(), 0))); - msgs.append(MessagePtr(new protocol::LayerCreate(1, 0x0101, 0, _color.rgba(), 0, QGuiApplication::tr("Background")))); - msgs.append(MessagePtr(new protocol::LayerCreate(1, 0x0102, 0, 0, 0, QGuiApplication::tr("Foreground")))); - return msgs; + return QList() + << MessagePtr(new protocol::CanvasResize(1, 0, _size.width(), _size.height(), 0)) + << MessagePtr(new protocol::CanvasBackground(1, _color.rgba())) + << MessagePtr(new protocol::LayerCreate(1, 0x0102, 0, 0, 0, QStringLiteral("Layer 1"))) + ; } QList ImageCanvasLoader::loadInitCommands() @@ -70,7 +69,9 @@ QList ImageCanvasLoader::loadInitCommands() if((ora.warnings & openraster::OraResult::ORA_EXTENDED)) text += "\n- " + QGuiApplication::tr("Application specific extensions are used"); if((ora.warnings & openraster::OraResult::ORA_NESTED)) - text += "\n- " + QGuiApplication::tr("Nested layers are not fully supported."); + text += "\n- " + QGuiApplication::tr("Nested layers are not fully supported"); + if((ora.warnings & openraster::OraResult::UNSUPPORTED_BACKGROUND_TILE)) + text += "\n- " + QGuiApplication::tr("Unsupported background tile size"); m_warning = text; } @@ -97,9 +98,10 @@ QList ImageCanvasLoader::loadInitCommands() msgs << MessagePtr(new protocol::CanvasResize(1, 0, image.size().width(), image.size().height(), 0)); } - image = image.convertToFormat(QImage::Format_ARGB32); - msgs << MessagePtr(new protocol::LayerCreate(1, layerId, 0, 0, 0, QStringLiteral("Layer %1").arg(layerId))); - msgs << net::command::putQImage(1, layerId, 0, 0, image, paintcore::BlendMode::MODE_REPLACE); + msgs << paintcore::LayerTileSet::fromImage( + image.convertToFormat(QImage::Format_ARGB32_Premultiplied) + ).toInitCommands(1, layerId, QStringLiteral("Layer %1").arg(layerId)); + ++layerId; } @@ -111,11 +113,11 @@ QList QImageCanvasLoader::loadInitCommands() { QList msgs; - QImage image = _image.convertToFormat(QImage::Format_ARGB32); + msgs << MessagePtr(new protocol::CanvasResize(1, 0, m_image.size().width(), m_image.size().height(), 0)); - msgs.append(MessagePtr(new protocol::CanvasResize(1, 0, image.size().width(), image.size().height(), 0))); - msgs.append(MessagePtr(new protocol::LayerCreate(1, 1, 0, 0, 0, "Background"))); - msgs.append(net::command::putQImage(1, 1, 0, 0, image, paintcore::BlendMode::MODE_REPLACE)); + msgs << paintcore::LayerTileSet::fromImage( + m_image.convertToFormat(QImage::Format_ARGB32_Premultiplied) + ).toInitCommands(1, 1, QStringLiteral("Layer 1")); return msgs; } @@ -128,6 +130,15 @@ QList SnapshotLoader::loadInitCommands() const QSize imgsize = m_layers->size(); msgs.append(MessagePtr(new protocol::CanvasResize(m_contextId, 0, imgsize.width(), imgsize.height(), 0))); + const QColor solidBgColor = m_layers->background().solidColor(); + if(solidBgColor.isValid()) + msgs.append(MessagePtr(new protocol::CanvasBackground(m_contextId, solidBgColor.rgba()))); + else + msgs.append(MessagePtr(new protocol::CanvasBackground( + m_contextId, + qCompress(reinterpret_cast(m_layers->background().constData()), paintcore::Tile::BYTES) + ))); + // Preset default layer if(m_session && m_session->layerlist()->defaultLayer()>0) msgs.append(MessagePtr(new protocol::DefaultLayer(m_contextId, m_session->layerlist()->defaultLayer()))); @@ -141,21 +152,14 @@ QList SnapshotLoader::loadInitCommands() for(int i=0;ilayerCount();++i) { const paintcore::Layer *layer = m_layers->getLayerByIndex(i); - const QColor fill = layer->isSolidColor(); + msgs << paintcore::LayerTileSet::fromLayer(*layer) + .toInitCommands(m_contextId, layer->id(), layer->title()); - msgs.append(MessagePtr(new protocol::LayerCreate(m_contextId, layer->id(), 0, fill.isValid() ? fill.rgba() : 0, 0, layer->title()))); - msgs.append(MessagePtr(new protocol::LayerAttributes(m_contextId, layer->id(), layer->opacity(), 1))); - - if(!fill.isValid()) - msgs.append(net::command::putQImage(m_contextId, layer->id(), 0, 0, layer->toImage(), paintcore::BlendMode::MODE_REPLACE)); - - // Set extra layer info (if present) - for(int j=0;jid()) { - const LayerListItem &info = m_layerlist[j]; - if(info.locked || !info.exclusive.isEmpty()) - msgs.append(MessagePtr(new protocol::LayerACL(m_contextId, layer->id(), info.locked, info.exclusive))); - } + // Set layer ACLs (if found) + if(m_session) { + const canvas::AclFilter::LayerAcl acl = m_session->aclFilter()->layerAcl(layer->id()); + if(acl.locked || acl.tier != canvas::Tier::Guest || !acl.exclusive.isEmpty()) + msgs << MessagePtr(new protocol::LayerACL(m_contextId, layer->id(), acl.locked, int(acl.tier), acl.exclusive)); } } @@ -168,9 +172,11 @@ QList SnapshotLoader::loadInitCommands() // Session and user ACLs if(m_session) { - // Note: Starting the reset process automatically sets the LOCK_SESSION flag. We don't want that after the reset. - msgs.append(MessagePtr(new protocol::SessionACL(m_contextId, - m_session->aclFilter()->sessionAclFlags() & ~protocol::SessionACL::LOCK_SESSION))); + uint8_t features[canvas::FeatureCount]; + for(int i=0;iaclFilter()->featureTier(Feature(i))); + + msgs.append(MessagePtr(new protocol::FeatureAccessLevels(m_contextId, features))); msgs.append(MessagePtr(new protocol::UserACL(m_contextId, m_session->aclFilter()->lockedUsers()))); } diff --git a/src/client/canvas/loader.h b/src/client/canvas/loader.h index f096ee43e..304fc6413 100644 --- a/src/client/canvas/loader.h +++ b/src/client/canvas/loader.h @@ -25,7 +25,6 @@ #include #include "../shared/net/message.h" -#include "layerlist.h" namespace paintcore { class LayerStack; @@ -103,7 +102,7 @@ class ImageCanvasLoader : public SessionLoader { QList loadInitCommands(); QString filename() const { return m_filename; } QString errorMessage() const { return m_error; } - QString warningMessage() const { return m_error; } + QString warningMessage() const { return m_warning; } private: QString m_filename; @@ -113,14 +112,14 @@ class ImageCanvasLoader : public SessionLoader { class QImageCanvasLoader : public SessionLoader { public: - QImageCanvasLoader(const QImage &image) : _image(image) {} + QImageCanvasLoader(const QImage &image) : m_image(image) {} QList loadInitCommands(); QString filename() const { return QString(); } QString errorMessage() const { return QString(); } private: - QImage _image; + QImage m_image; }; /** @@ -135,11 +134,10 @@ class SnapshotLoader : public SessionLoader { * * @param context ID resetting user ID * @param layers the layer stack (required) - * @param layerlist layer list info model. Used for layer ACLs. (optional) - * @param session the current canvas. Used for general session ACLs and such. (optional) + * @param session the current canvas. Used for session ACLs and such. (optional) */ - SnapshotLoader(uint8_t contextId, const paintcore::LayerStack *layers, const QVector &layerlist, const canvas::CanvasModel *session) - : m_layers(layers), m_layerlist(layerlist), m_session(session), m_contextId(contextId) {} + SnapshotLoader(uint8_t contextId, const paintcore::LayerStack *layers, const canvas::CanvasModel *session) + : m_layers(layers), m_session(session), m_contextId(contextId) {} QList loadInitCommands(); QString filename() const { return QString(); } @@ -147,7 +145,6 @@ class SnapshotLoader : public SessionLoader { private: const paintcore::LayerStack *m_layers; - const QVector m_layerlist; const CanvasModel *m_session; uint8_t m_contextId; }; diff --git a/src/client/canvas/statetracker.cpp b/src/client/canvas/statetracker.cpp index 135e5717f..a2319ec85 100644 --- a/src/client/canvas/statetracker.cpp +++ b/src/client/canvas/statetracker.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2013-2017 Calle Laakkonen + Copyright (C) 2013-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -23,11 +23,12 @@ #include "core/layerstack.h" #include "core/layer.h" +#include "brushes/brushpainter.h" #include "net/commands.h" #include "net/internalmsg.h" #include "tools/selection.h" // for selection transform utils -#include "../shared/net/pen.h" +#include "../shared/net/brushes.h" #include "../shared/net/layer.h" #include "../shared/net/image.h" #include "../shared/net/annotation.h" @@ -43,19 +44,18 @@ namespace canvas { struct StateSavepoint::Data { - Data() : timestamp(0), streampointer(-1), canvas(0), _refcount(1) {} + Data() : timestamp(0), canvas(nullptr), streampointer(-1), m_refcount(1) {} Data(const Data &) = delete; Data &operator=(const Data&) = delete; ~Data() { delete canvas; } qint64 timestamp; - int streampointer; paintcore::Savepoint *canvas; - QHash ctxstate; QVector layermodel; + int streampointer; private: - int _refcount; + int m_refcount; friend class StateSavepoint; }; @@ -63,22 +63,22 @@ StateSavepoint::StateSavepoint(const StateSavepoint &sp) : m_data(sp.m_data) { if(m_data) - ++m_data->_refcount; + ++m_data->m_refcount; } StateSavepoint &StateSavepoint::operator =(const StateSavepoint &sp) { if(m_data) { if(sp.m_data != m_data) { - Q_ASSERT(m_data->_refcount>0); - if(--m_data->_refcount == 0) + Q_ASSERT(m_data->m_refcount>0); + if(--m_data->m_refcount == 0) delete m_data; m_data = sp.m_data; - ++m_data->_refcount; + ++m_data->m_refcount; } } else { m_data = sp.m_data; - ++m_data->_refcount; + ++m_data->m_refcount; } return *this; } @@ -86,8 +86,8 @@ StateSavepoint &StateSavepoint::operator =(const StateSavepoint &sp) StateSavepoint::~StateSavepoint() { if(m_data) { - Q_ASSERT(m_data->_refcount>0); - if(--m_data->_refcount == 0) + Q_ASSERT(m_data->m_refcount>0); + if(--m_data->m_refcount == 0) delete m_data; } } @@ -110,7 +110,7 @@ QImage StateSavepoint::thumbnail(const QSize &maxSize) const return QImage(); paintcore::LayerStack stack; - stack.restoreSavepoint(m_data->canvas); + stack.editor().restoreSavepoint(m_data->canvas); QImage img = stack.toFlatImage(true); if(img.width() > maxSize.width() || img.height() > maxSize.height()) { img = img.scaled(maxSize, Qt::KeepAspectRatio); @@ -124,30 +124,11 @@ QList StateSavepoint::initCommands(uint8_t contextId, Canv return QList(); paintcore::LayerStack stack; - stack.restoreSavepoint(m_data->canvas); - SnapshotLoader loader(contextId, &stack, m_data->layermodel, canvas); + stack.editor().restoreSavepoint(m_data->canvas); + SnapshotLoader loader(contextId, &stack, canvas); return loader.loadInitCommands(); } -void ToolContext::updateFromToolchange(const protocol::ToolChange &cmd) -{ - layer_id = cmd.layer(); - brush.setBlendingMode(paintcore::BlendMode::Mode(cmd.blend())); - brush.setSubpixel(cmd.mode() & protocol::TOOL_MODE_SUBPIXEL); - brush.setIncremental(cmd.mode() & protocol::TOOL_MODE_INCREMENTAL); - brush.setSpacing(cmd.spacing()); - brush.setSize(qMax(1, (int)cmd.size_h())); - brush.setSize2(qMax(1, (int)cmd.size_l())); - brush.setHardness(cmd.hard_h() / 255.0); - brush.setHardness2(cmd.hard_l() / 255.0); - brush.setOpacity(cmd.opacity_h() / 255.0); - brush.setOpacity2(cmd.opacity_l() / 255.0); - brush.setColor(cmd.color()); - brush.setSmudge(cmd.smudge_h() / 255.0); - brush.setSmudge2(cmd.smudge_l() / 255.0); - brush.setResmudge(cmd.resmudge()); -} - /** * @brief Construct a state tracker instance * @@ -156,9 +137,9 @@ void ToolContext::updateFromToolchange(const protocol::ToolChange &cmd) * @param myId ID of the local user * @param parent */ -StateTracker::StateTracker(paintcore::LayerStack *image, LayerListModel *layerlist, int myId, QObject *parent) +StateTracker::StateTracker(paintcore::LayerStack *image, LayerListModel *layerlist, uint8_t myId, QObject *parent) : QObject(parent), - _image(image), + m_layerstack(image), m_layerlist(layerlist), m_myId(myId), m_myLastLayer(-1), @@ -194,7 +175,6 @@ void StateTracker::reset() m_msgqueue.clear(); m_localfork.clear(); m_layerlist->clear(); - m_myLastLayer = _contexts[m_myId].tool.layer_id; // Make sure there is always a savepoint in the history makeSavepoint(m_history.end()-1); @@ -214,6 +194,21 @@ void StateTracker::localCommand(protocol::MessagePtr msg) m_localfork.addLocalMessage(msg, affectedArea(msg)); + // Remember last used layer + switch(msg->type()) { + using namespace protocol; + case MSG_DRAWDABS_CLASSIC: + case MSG_DRAWDABS_PIXEL: + case MSG_DRAWDABS_PIXEL_SQUARE: + case MSG_LAYER_CREATE: + case MSG_PUTIMAGE: + case MSG_FILLRECT: + case MSG_REGION_MOVE: + m_myLastLayer = msg->layer(); + break; + default: break; + } + // for the future: handle undo messages in the local fork too if(msg->type() != protocol::MSG_UNDO && msg->type() != protocol::MSG_UNDOPOINT) { int pos = m_history.end() - 1; @@ -255,7 +250,7 @@ void StateTracker::processQueuedCommands() void StateTracker::receiveCommand(protocol::MessagePtr msg) { - static const uint HISTORY_SIZE_LIMIT = 10 * 1024*1024; + static const uint HISTORY_SIZE_LIMIT = 60 * 1024*1024; if(msg->type() == protocol::MSG_INTERNAL) { const auto &ci = msg.cast(); @@ -263,6 +258,9 @@ void StateTracker::receiveCommand(protocol::MessagePtr msg) emit catchupProgress(ci.value()); else if(ci.internalType() == protocol::ClientInternal::Type::SequencePoint) emit sequencePoint(ci.value()); + else if(ci.internalType() == protocol::ClientInternal::Type::TruncateHistory) + handleTruncateHistory(); + return; } @@ -377,11 +375,10 @@ void StateTracker::handleCommand(protocol::MessagePtr msg, bool replay, int pos) case MSG_LAYER_DELETE: handleLayerDelete(msg.cast()); break; - case MSG_TOOLCHANGE: - handleToolChange(msg.cast()); - break; - case MSG_PEN_MOVE: - handlePenMove(msg.cast()); + case MSG_DRAWDABS_CLASSIC: + case MSG_DRAWDABS_PIXEL: + case MSG_DRAWDABS_PIXEL_SQUARE: + handleDrawDabs(*msg); break; case MSG_PEN_UP: handlePenUp(msg.cast()); @@ -413,6 +410,12 @@ void StateTracker::handleCommand(protocol::MessagePtr msg, bool replay, int pos) case MSG_REGION_MOVE: handleMoveRegion(msg.cast()); break; + case MSG_PUTTILE: + handlePutTile(msg.cast()); + break; + case MSG_CANVAS_BACKGROUND: + handleCanvasBackground(msg.cast()); + break; default: qWarning() << "Unhandled drawing command" << msg->type() << msg->messageName(); return; @@ -431,16 +434,11 @@ void StateTracker::endRemoteContexts() for(protocol::MessagePtr m : localfork) m_history.append(m); - // End drawing contexts - QHashIterator iter(_contexts); - while(iter.hasNext()) { - iter.next(); - if(iter.key() != localId()) { - // Simulate pen-up - if(iter.value().pendown) - receiveQueuedCommand(protocol::MessagePtr(new protocol::PenUp(iter.key()))); - } - } + // Make sure there are no lingering indirect strokes + // TODO this should probably be done with an InternalMsg, + // in case there is still stuff in the queue + auto layers = m_layerstack->editor(); + layers.mergeAllSublayers(); m_myLastLayer = -1; } @@ -450,28 +448,47 @@ void StateTracker::endRemoteContexts() */ void StateTracker::endPlayback() { - QHashIterator iter(_contexts); - while(iter.hasNext()) { - iter.next(); - if(iter.value().pendown) - receiveQueuedCommand(protocol::MessagePtr(new protocol::PenUp(iter.key()))); - } + auto layers = m_layerstack->editor(); + layers.mergeAllSublayers(); } void StateTracker::handleCanvasResize(const protocol::CanvasResize &cmd, int pos) { - _image->resize(cmd.top(), cmd.right(), cmd.bottom(), cmd.left()); + { + auto layers = m_layerstack->editor(); + layers.resize(cmd.top(), cmd.right(), cmd.bottom(), cmd.left()); + } // Generate the initial savepoint, just in case makeSavepoint(pos); } +void StateTracker::handleCanvasBackground(const protocol::CanvasBackground &cmd) +{ + paintcore::Tile t; + if(cmd.isSolidColor()) { + t = paintcore::Tile(QColor::fromRgba(cmd.color())); + + } else { + QByteArray data = qUncompress(cmd.image()); + if(data.length() != paintcore::Tile::BYTES) { + qWarning() << "Invalid canvas background: Expected" << paintcore::Tile::BYTES << "bytes, but got" << data.length(); + return; + } + + t = paintcore::Tile(data); + } + m_layerstack->editor().setBackground(t); +} + void StateTracker::handleLayerCreate(const protocol::LayerCreate &cmd) { - paintcore::Layer *layer = _image->createLayer( - cmd.id(), + auto layers = m_layerstack->editor(); + + auto layer = layers.createLayer( + cmd.layer(), cmd.source(), QColor::fromRgba(cmd.fill()), (cmd.flags() & protocol::LayerCreate::FLAG_INSERT), @@ -479,50 +496,65 @@ void StateTracker::handleLayerCreate(const protocol::LayerCreate &cmd) cmd.title() ); - if(layer) { - // Note: layers are listed bottom-first in the stack, - // but topmost first in the view - m_layerlist->createLayer( - cmd.id(), - _image->layerCount() - _image->indexOf(layer->id()) - 1, - cmd.title() - ); + if(layer.isNull()) { + qWarning("Layer creation failed (id=%d, source=%d)", cmd.layer(), cmd.source()); + return; + } - // Auto-select layers we create - // During the startup phase, autoselect new layers or if a default one is set, - // just the default one. If there is a remembered layer selection, it takes precedence - // over others. - if( - // Autoselect layers created by me - (m_hasParticipated && cmd.contextId() == localId()) || - // If this user has not yet drawn anything... - (!m_hasParticipated && ( - // ... and if there is no remembered layer... - ((m_myLastLayer <= 0) && ( // ...select default layer or if not selected, any new layer - cmd.id() == m_layerlist->defaultLayer() || - !m_layerlist->defaultLayer() - )) || - // ... and if there is a remembered layer, select only that one - (m_myLastLayer>0 && cmd.id() == m_myLastLayer) - )) - ) - { - emit layerAutoselectRequest(cmd.id()); - } + // Note: layers are listed bottom-first in the stack, + // but topmost first in the view + m_layerlist->createLayer( + cmd.layer(), + layers->layerCount() - layers->indexOf(layer->id()) - 1, + cmd.title() + ); + + // Auto-select layers we create + // During the startup phase, autoselect new layers or if a default one is set, + // just the default one. If there is a remembered layer selection, it takes precedence + // over others. + if( + // Autoselect layers created by me + (m_hasParticipated && cmd.contextId() == localId()) || + // If this user has not yet drawn anything... + (!m_hasParticipated && ( + // ... and if there is no remembered layer... + ((m_myLastLayer <= 0) && ( // ...select default layer or if not selected, any new layer + layer->id() == m_layerlist->defaultLayer() || + !m_layerlist->defaultLayer() + )) || + // ... and if there is a remembered layer, select only that one + (m_myLastLayer>0 && layer->id() == m_myLastLayer) + )) + ) + { + emit layerAutoselectRequest(layer->id()); } } void StateTracker::handleLayerAttributes(const protocol::LayerAttributes &cmd) { - paintcore::Layer *layer = _image->getLayer(cmd.id()); - if(!layer) { - qWarning() << "received layer attributes for non-existent layer" << cmd.id(); + auto layers = m_layerstack->editor(); + auto layer = layers.getEditableLayer(cmd.layer()); + if(layer.isNull()) { + qWarning("Received layer attributes for non-existent layer #%d", cmd.layer()); return; } - - layer->setOpacity(cmd.opacity()); - layer->setBlend(paintcore::BlendMode::Mode(cmd.blend())); - m_layerlist->changeLayer(cmd.id(), cmd.opacity() / 255.0, paintcore::BlendMode::Mode(cmd.blend())); + + const auto bm = paintcore::BlendMode::Mode(cmd.blend()); + + if(cmd.sublayer()>0) { + auto sl = layer.getEditableSubLayer(cmd.sublayer(), bm, cmd.opacity()); + // getSubLayer does not touch the attributes if the sublayer already exists + sl.setBlend(bm); + sl.setOpacity(cmd.opacity()); + + } else { + layer.setBlend(bm); + layer.setOpacity(cmd.opacity()); + layer.setCensored(cmd.isCensored()); + m_layerlist->changeLayer(layer->id(), cmd.isCensored(), cmd.opacity() / 255.0, paintcore::BlendMode::Mode(cmd.blend())); + } } void StateTracker::handleLayerVisibility(const protocol::LayerVisibility &cmd) @@ -532,43 +564,50 @@ void StateTracker::handleLayerVisibility(const protocol::LayerVisibility &cmd) if(cmd.contextId() != localId()) return; - paintcore::Layer *layer = _image->getLayer(cmd.id()); - if(!layer) { - qWarning() << "received layer visibility for non-existent layer" << cmd.id(); + auto layers = m_layerstack->editor(); + auto layer = layers.getEditableLayer(cmd.layer()); + if(layer.isNull()) { + qWarning("Received layer visibility for non-existent layer #%d", cmd.layer()); return; } - layer->setHidden(!cmd.visible()); - m_layerlist->setLayerHidden(cmd.id(), !cmd.visible()); + layer.setHidden(!cmd.visible()); + m_layerlist->setLayerHidden(layer->id(), !cmd.visible()); } void StateTracker::previewLayerOpacity(int id, float opacity) { - paintcore::Layer *layer = _image->getLayer(id); - if(!layer) { + auto layers = m_layerstack->editor(); + auto layer = layers.getEditableLayer(id); + + if(layer.isNull()) { qWarning("previewLayerOpacity(%d): no such layer!", id); return; } - layer->setOpacity(opacity*255); + layer.setOpacity(opacity*255); } void StateTracker::handleLayerTitle(const protocol::LayerRetitle &cmd) { - paintcore::Layer *layer = _image->getLayer(cmd.id()); - if(!layer) { - qWarning() << "received layer title for non-existent layer" << cmd.id(); + auto layers = m_layerstack->editor(); + auto layer = layers.getEditableLayer(cmd.layer()); + + if(layer.isNull()) { + qWarning() << "received layer title for non-existent layer" << cmd.layer(); return; } - layer->setTitle(cmd.title()); - m_layerlist->retitleLayer(cmd.id(), cmd.title()); + layer.setTitle(cmd.title()); + m_layerlist->retitleLayer(layer->id(), cmd.title()); } void StateTracker::handleLayerOrder(const protocol::LayerOrder &cmd) { + auto layers = m_layerstack->editor(); + QList currentOrder; - for(int i=0;i<_image->layerCount();++i) - currentOrder.append(_image->getLayerByIndex(i)->id()); + for(int i=0;ilayerCount();++i) + currentOrder.append(layers->getLayerByIndex(i)->id()); QList newOrder = cmd.sanitizedOrder(currentOrder); @@ -579,124 +618,112 @@ void StateTracker::handleLayerOrder(const protocol::LayerOrder &cmd) qWarning() << " fixed order is:" << newOrder; } - _image->reorderLayers(newOrder); + layers.reorderLayers(newOrder); m_layerlist->reorderLayers(newOrder); } void StateTracker::handleLayerDelete(const protocol::LayerDelete &cmd) { - if(cmd.merge()) - _image->mergeLayerDown(cmd.id()); - _image->deleteLayer(cmd.id()); - m_layerlist->deleteLayer(cmd.id()); -} + auto layers = m_layerstack->editor(); -void StateTracker::handleToolChange(const protocol::ToolChange &cmd) -{ - DrawingContext &ctx = _contexts[cmd.contextId()]; - ctx.tool.updateFromToolchange(cmd); - - paintcore::Layer *layer = _image->getLayer(ctx.tool.layer_id); - QString layername; - if(layer) - layername = layer->title(); - else - layername = QStringLiteral("???"); - - emit userMarkerAttribs(cmd.contextId(), ctx.tool.brush.color(), layername); + if(cmd.merge()) + layers.mergeLayerDown(cmd.layer()); + layers.deleteLayer(cmd.layer()); + m_layerlist->deleteLayer(cmd.layer()); } -void StateTracker::handlePenMove(const protocol::PenMove &cmd) +void StateTracker::handleDrawDabs(const protocol::Message &cmd) { - DrawingContext &ctx = _contexts[cmd.contextId()]; - paintcore::Layer *layer = _image->getLayer(ctx.tool.layer_id); - if(!layer) { - qWarning() << "penMove by user" << cmd.contextId() << "on non-existent layer" << ctx.tool.layer_id; - return; - } - - for(const protocol::PenPoint &pp : cmd.points()) { - paintcore::Point p(pp.x / 4.0, pp.y / 4.0, pp.p/qreal(0xffff)); - const int r = ctx.tool.brush.fsize(p.pressure())/2 + 1; - - if(ctx.pendown) { - layer->drawLine(cmd.contextId(), ctx.tool.brush, ctx.lastpoint, p, ctx.stroke); - ctx.boundingRect |= QRect(p.x() - r, p.y() - r, r*2, r*2); + auto layers = m_layerstack->editor(); - } else { - ctx.pendown = true; - ctx.stroke = paintcore::StrokeState(ctx.tool.brush); - ctx.boundingRect = QRect(p.x() - r, p.y() - r, r*2, r*2); - layer->dab(cmd.contextId(), ctx.tool.brush, p, ctx.stroke); - } - ctx.lastpoint = p; - } + brushes::drawBrushDabs(cmd, layers); if(_showallmarkers || cmd.contextId() != localId()) - emit userMarkerMove(cmd.contextId(), ctx.lastpoint, 0); + emit userMarkerMove(cmd.contextId(), cmd.layer(), static_cast(cmd).lastPoint()); } void StateTracker::handlePenUp(const protocol::PenUp &cmd) { - DrawingContext &ctx = _contexts[cmd.contextId()]; - paintcore::Layer *layer = _image->getLayer(ctx.tool.layer_id); - if(!layer) { - qWarning() << "penUp by user" << cmd.contextId() << "on non-existent layer" << ctx.tool.layer_id; - return; - } - // This ends an indirect stroke. In incremental mode, this does nothing. - layer->mergeSublayer(cmd.contextId()); - - ctx.pendown = false; + m_layerstack->editor().mergeSublayers(cmd.contextId()); emit userMarkerHide(cmd.contextId()); } void StateTracker::handlePutImage(const protocol::PutImage &cmd) { - paintcore::Layer *layer = _image->getLayer(cmd.layer()); - if(!layer) { - qWarning() << "putImage on non-existent layer" << cmd.layer(); + auto layers = m_layerstack->editor(); + auto layer = layers.getEditableLayer(cmd.layer()); + if(layer.isNull()) { + qWarning("PutImage on non-existent layer #%d", cmd.layer()); return; } + const int expectedLen = cmd.width() * cmd.height() * 4; QByteArray data = qUncompress(cmd.image()); if(data.length() != expectedLen) { qWarning() << "Invalid putImage: Expected" << expectedLen << "bytes, but got" << data.length(); return; } - QImage img(reinterpret_cast(data.constData()), cmd.width(), cmd.height(), QImage::Format_ARGB32); - layer->putImage(cmd.x(), cmd.y(), img, paintcore::BlendMode::Mode(cmd.blendmode())); + QImage img(reinterpret_cast(data.constData()), cmd.width(), cmd.height(), QImage::Format_ARGB32_Premultiplied); + layer.putImage(cmd.x(), cmd.y(), img, paintcore::BlendMode::Mode(cmd.blendmode())); if(_showallmarkers || cmd.contextId() != m_myId) - emit userMarkerMove(cmd.contextId(), QPointF(cmd.x() + cmd.width()/2, cmd.y()+cmd.height()/2), 0); + emit userMarkerMove(cmd.contextId(), layer->id(), QPoint(cmd.x() + cmd.width()/2, cmd.y()+cmd.height()/2)); +} + +void StateTracker::handlePutTile(const protocol::PutTile &cmd) +{ + auto layers = m_layerstack->editor(); + auto layer = layers.getEditableLayer(cmd.layer()); + if(layer.isNull()) { + qWarning("PutTile on non-existent layer #%d", cmd.layer()); + return; + } + + paintcore::Tile t; + if(cmd.isSolidColor()) { + t = paintcore::Tile(QColor::fromRgba(cmd.color())); + + } else { + QByteArray data = qUncompress(cmd.image()); + if(data.length() != paintcore::Tile::BYTES) { + qWarning() << "Invalid putTile: Expected" << paintcore::Tile::BYTES << "bytes, but got" << data.length(); + return; + } + + t = paintcore::Tile(data); + } + + layer.putTile(cmd.column(), cmd.row(), cmd.repeat(), t, cmd.sublayer()); } void StateTracker::handleFillRect(const protocol::FillRect &cmd) { - paintcore::Layer *layer = _image->getLayer(cmd.layer()); - if(!layer) { - qWarning("fillRect on non-existent layer %d", cmd.layer()); + auto layers = m_layerstack->editor(); + auto layer = layers.getEditableLayer(cmd.layer()); + if(layer.isNull()) { + qWarning("FillRect on non-existent layer #%d", cmd.layer()); return; } - layer->fillRect(QRect(cmd.x(), cmd.y(), cmd.width(), cmd.height()), QColor::fromRgba(cmd.color()), paintcore::BlendMode::Mode(cmd.blend())); + layer.fillRect(QRect(cmd.x(), cmd.y(), cmd.width(), cmd.height()), QColor::fromRgba(cmd.color()), paintcore::BlendMode::Mode(cmd.blend())); if(_showallmarkers || cmd.contextId() != m_myId) - emit userMarkerMove(cmd.contextId(), QPointF(cmd.x() + cmd.width()/2, cmd.y()+cmd.height()/2), 0); + emit userMarkerMove(cmd.contextId(), layer->id(), QPoint(cmd.x() + cmd.width()/2, cmd.y()+cmd.height()/2)); } void StateTracker::handleMoveRegion(const protocol::MoveRegion &cmd) { - paintcore::Layer *layer = _image->getLayer(cmd.layer()); - if(!layer) { - qWarning("moveRegion on non-existent layer %d", cmd.layer()); + auto layers = m_layerstack->editor(); + auto layer = layers.getEditableLayer(cmd.layer()); + if(layer.isNull()) { + qWarning("MoveRegion on non-existent layer #%d", cmd.layer()); return; } if(cmd.contextId() == m_myId) { // Moving the layer for real: make sure my preview is removed - layer->removeSublayer(-1); + layer.removeSublayer(-1); } // Source region bounding rectangle @@ -712,7 +739,7 @@ void StateTracker::handleMoveRegion(const protocol::MoveRegion &cmd) // Sanity check: without a size limit, a user could create huge temporary images and potentially other clients const int targetArea = target.boundingRect().size().width() * target.boundingRect().size().height(); - if(targetArea > _image->width() * _image->height()) { + if(targetArea > m_layerstack->width() * m_layerstack->height()) { qWarning("moveRegion: cannot scale beyond image size"); return; } @@ -730,7 +757,7 @@ void StateTracker::handleMoveRegion(const protocol::MoveRegion &cmd) mask = QImage(reinterpret_cast(maskData.constData()), cmd.bw(), cmd.bh(), QImage::Format_Mono); mask.setColor(0, 0); mask.setColor(1, 0xffffffff); - mask = mask.convertToFormat(QImage::Format_ARGB32); + mask = mask.convertToFormat(QImage::Format_ARGB32_Premultiplied); } // Extract selected pixels @@ -763,15 +790,15 @@ void StateTracker::handleMoveRegion(const protocol::MoveRegion &cmd) // Erase selection mask and draw transformed image if(mask.isNull()) { - layer->fillRect(bounds, Qt::transparent, paintcore::BlendMode::MODE_REPLACE); + layer.fillRect(bounds, Qt::transparent, paintcore::BlendMode::MODE_REPLACE); } else { - layer->putImage(bounds.x(), bounds.y(), mask, paintcore::BlendMode::MODE_ERASE); + layer.putImage(bounds.x(), bounds.y(), mask, paintcore::BlendMode::MODE_ERASE); } - layer->putImage(offset.x(), offset.y(), transformed, paintcore::BlendMode::MODE_NORMAL); + layer.putImage(offset.x(), offset.y(), transformed, paintcore::BlendMode::MODE_NORMAL); if(_showallmarkers || cmd.contextId() != m_myId) - emit userMarkerMove(cmd.contextId(), target.boundingRect().center(), 0); + emit userMarkerMove(cmd.contextId(), layer->id(), target.boundingRect().center()); } void StateTracker::handleUndoPoint(const protocol::UndoPoint &cmd, bool replay, int pos) @@ -948,8 +975,7 @@ StateSavepoint StateTracker::createSavepoint(int pos) StateSavepoint savepoint; savepoint->timestamp = QDateTime::currentMSecsSinceEpoch(); savepoint->streampointer = pos<0 ? m_history.end() : pos; - savepoint->canvas = _image->makeSavepoint(); - savepoint->ctxstate = _contexts; + savepoint->canvas = m_layerstack->makeSavepoint(); savepoint->layermodel = m_layerlist->getLayers(); return savepoint; @@ -957,10 +983,6 @@ StateSavepoint StateTracker::createSavepoint(int pos) void StateTracker::makeSavepoint(int pos) { - // Make sure there is something in the message stream buffer - if(m_history.end() <= m_history.offset()) - return; - // Don't make savepoints while a local fork exists, since // there will be stuff on the canvas that is not yet in // the mainline session history @@ -991,8 +1013,7 @@ void StateTracker::resetToSavepoint(const StateSavepoint savepoint) m_history.resetTo(savepoint->streampointer); m_savepoints.clear(); - _image->restoreSavepoint(savepoint->canvas); - _contexts = savepoint->ctxstate; + m_layerstack->editor().restoreSavepoint(savepoint->canvas); m_layerlist->setLayers(savepoint->layermodel); m_savepoints.append(savepoint); @@ -1011,8 +1032,7 @@ void StateTracker::revertSavepointAndReplay(const StateSavepoint savepoint) return; } - _image->restoreSavepoint(savepoint->canvas); - _contexts = savepoint->ctxstate; + m_layerstack->editor().restoreSavepoint(savepoint->canvas); m_layerlist->setLayers(savepoint->layermodel); // Reverting a savepoint destroys all newer savepoints @@ -1040,21 +1060,40 @@ void StateTracker::revertSavepointAndReplay(const StateSavepoint savepoint) } } +void StateTracker::handleTruncateHistory() +{ + int pos = m_history.end()-1; + int upCount = 0; + + qWarning("Truncating undo history at %d", pos); + while(m_history.isValidIndex(pos) && upCount <= protocol::UNDO_DEPTH_LIMIT) { + protocol::MessagePtr msg = m_history.at(pos); + + if(msg->type() == protocol::MSG_UNDOPOINT) { + ++upCount; + msg->setUndoState(protocol::GONE); + } + + --pos; + } + qWarning("Marked %d UPs", upCount); +} + void StateTracker::handleAnnotationCreate(const protocol::AnnotationCreate &cmd) { - _image->annotations()->addAnnotation(cmd.id(), QRect(cmd.x(), cmd.y(), cmd.w(), cmd.h())); + m_layerstack->annotations()->addAnnotation(cmd.id(), QRect(cmd.x(), cmd.y(), cmd.w(), cmd.h())); if(cmd.contextId() == localId()) emit myAnnotationCreated(cmd.id()); } void StateTracker::handleAnnotationReshape(const protocol::AnnotationReshape &cmd) { - _image->annotations()->reshapeAnnotation(cmd.id(), QRect(cmd.x(), cmd.y(), cmd.w(), cmd.h())); + m_layerstack->annotations()->reshapeAnnotation(cmd.id(), QRect(cmd.x(), cmd.y(), cmd.w(), cmd.h())); } void StateTracker::handleAnnotationEdit(const protocol::AnnotationEdit &cmd) { - _image->annotations()->changeAnnotation( + m_layerstack->annotations()->changeAnnotation( cmd.id(), cmd.text(), cmd.flags() & protocol::AnnotationEdit::FLAG_PROTECT, @@ -1065,7 +1104,7 @@ void StateTracker::handleAnnotationEdit(const protocol::AnnotationEdit &cmd) void StateTracker::handleAnnotationDelete(const protocol::AnnotationDelete &cmd) { - _image->annotations()->deleteAnnotation(cmd.id()); + m_layerstack->annotations()->deleteAnnotation(cmd.id()); } void StateSavepoint::toDatastream(QDataStream &out) const @@ -1076,37 +1115,11 @@ void StateSavepoint::toDatastream(QDataStream &out) const // Write stream pointer out << quint32(d->streampointer); - // Write drawing contexts - out << quint8(d->ctxstate.size()); - for(const quint8 ctxid : d->ctxstate.keys()) { - const DrawingContext &ctx = d->ctxstate[ctxid]; - - // write context ID - out << ctxid; - - // write tool context - protocol::MessagePtr tc = net::command::brushToToolChange(ctxid, ctx.tool.layer_id, ctx.tool.brush); - QByteArray tcb(tc->length(), '\0'); - tc->serialize(tcb.data()); - out.writeBytes(tcb.data(), tcb.length()); - - // write last point - out << ctx.lastpoint.x(); - out << ctx.lastpoint.y(); - out << ctx.lastpoint.pressure(); - - // write pendown bit - out << ctx.pendown; - - // write stroke state - out << ctx.stroke.distance << ctx.stroke.smudgeDistance << ctx.stroke.smudgeColor; - } - // Write layer model out << quint8(d->layermodel.size()); for(const LayerListItem &layer : d->layermodel) { // Write layer ID - out << qint32(layer.id); + out << layer.id; // Write layer title out << layer.title; @@ -1114,17 +1127,15 @@ void StateSavepoint::toDatastream(QDataStream &out) const // Write layer opacity and flags out << layer.opacity; out << quint8(layer.blend); - out << layer.hidden << layer.locked; - - // Write layer ACL - out << layer.exclusive; + out << layer.hidden; + out << layer.censored; } // Write layer stack d->canvas->toDatastream(out); } -StateSavepoint StateSavepoint::fromDatastream(QDataStream &in, StateTracker *owner) +StateSavepoint StateSavepoint::fromDatastream(QDataStream &in) { StateSavepoint sp; sp.m_data = new StateSavepoint::Data; @@ -1135,54 +1146,12 @@ StateSavepoint StateSavepoint::fromDatastream(QDataStream &in, StateTracker *own in >> sptr; d->streampointer = sptr; - // Read drawing contexts - quint8 contexts; - in >> contexts; - while(contexts--) { - DrawingContext ctx; - - // Read context id - quint8 ctxid; - in >> ctxid; - - // Read tool context - char *msgbuf; - unsigned int msglen; - in.readBytes(msgbuf, msglen); - - protocol::Message *tc = protocol::Message::deserialize((const uchar*)msgbuf, msglen, true); - delete [] msgbuf; - if(!tc) { - qWarning() << "invalid tool change message in snapshot!"; - return StateSavepoint(); - } - ctx.tool.updateFromToolchange(static_cast(*tc)); - delete tc; - - // Read last point - qreal lpx, lpy, lpp; - in >> lpx >> lpy >> lpp; - ctx.lastpoint = paintcore::Point(lpx, lpy, lpp); - - // Read pendown bit - in >> ctx.pendown; - - // Read stroke state - in >> ctx.stroke.distance >> ctx.stroke.smudgeDistance >> ctx.stroke.smudgeColor; - - // Note: ctx.bounds is used only for retconning during online drawing - // so we don't need to restore it here, since saved snapshots are currently - // used only for session playback. - - d->ctxstate[ctxid] = ctx; - } - // Read layer list quint8 layercount; in >> layercount; while(layercount--) { // Read layer ID - qint32 layerid; + quint16 layerid; in >> layerid; // Read layer title @@ -1199,12 +1168,8 @@ StateSavepoint StateSavepoint::fromDatastream(QDataStream &in, StateTracker *own bool hidden; in >> hidden; - bool locked; - in >> locked; - - // Read layer ACL - QList acls; - in >> acls; + bool censored; + in >> censored; sp->layermodel.append(LayerListItem { layerid, @@ -1212,28 +1177,16 @@ StateSavepoint StateSavepoint::fromDatastream(QDataStream &in, StateTracker *own opacity, paintcore::BlendMode::Mode(blend), hidden, - locked, - acls + censored }); } // Read layerstack snapshot - d->canvas = paintcore::Savepoint::fromDatastream(in, owner->image()); + d->canvas = paintcore::Savepoint::fromDatastream(in); return sp; } -bool StateTracker::isLayerLocked(int id) const -{ - for(const LayerListItem &l : m_layerlist->getLayers()) { - if(l.id == id) - return l.isLockedFor(localId()); - } - - qWarning("isLayerLocked(%d): no such layer!", id); - return false; -} - /** * @brief Get the affected area of the given message * @@ -1248,59 +1201,52 @@ AffectedArea StateTracker::affectedArea(protocol::MessagePtr msg) const switch(msg->type()) { using namespace protocol; - case MSG_LAYER_CREATE: return AffectedArea(AffectedArea::LAYERATTRS, msg.cast().id()); - case MSG_LAYER_ATTR: return AffectedArea(AffectedArea::LAYERATTRS, msg.cast().id()); - case MSG_LAYER_RETITLE: return AffectedArea(AffectedArea::LAYERATTRS, msg.cast().id()); + case MSG_LAYER_CREATE: + case MSG_LAYER_ATTR: + case MSG_LAYER_RETITLE: + return AffectedArea(AffectedArea::LAYERATTRS, msg->layer()); case MSG_LAYER_VISIBILITY: return AffectedArea(AffectedArea::USERATTRS, 0); case MSG_PUTIMAGE: { const PutImage &m = msg.cast(); return AffectedArea(AffectedArea::PIXELS, m.layer(), QRect(m.x(), m.y(), m.width(), m.height())); } - case MSG_TOOLCHANGE: return AffectedArea(AffectedArea::USERATTRS, 0); - case MSG_PEN_MOVE: { - const DrawingContext &ctx = _contexts.value(msg->contextId()); - - // Non-incremental brush draws on a private layer: we must check ordering in PenUp - if(!ctx.tool.brush.incremental()) - return AffectedArea(AffectedArea::USERATTRS, 0); - - const PenMove &m = msg.cast(); - - // Find the bounding rectangle of the received piece of the stroke. - QRect bounds; + case MSG_PUTTILE: { + const PutTile &m = msg.cast(); + return AffectedArea(AffectedArea::PIXELS, m.layer(), QRect( + m.column() * paintcore::Tile::SIZE, + m.row() * paintcore::Tile::SIZE, + paintcore::Tile::SIZE, paintcore::Tile::SIZE)); + } - if(ctx.pendown) - bounds = QRect(ctx.lastpoint.toPoint(), QSize(1,1)); - else - bounds = QRect(m.points().first().x/4, m.points().first().y/4, 1, 1); + case MSG_DRAWDABS_CLASSIC: + case MSG_DRAWDABS_PIXEL: + case MSG_DRAWDABS_PIXEL_SQUARE: { + const DrawDabs &dd = msg.cast(); - for(const PenPoint &pp : m.points()) { - bounds |= QRect(pp.x/4, pp.y/4, 1, 1); - } + // Indirect drawing mode: check bounds in PenUp + if(dd.isIndirect()) + return AffectedArea(AffectedArea::USERATTRS, 0); - const int r = qMax(ctx.tool.brush.size1(), ctx.tool.brush.size2()) / 2 + 1; - bounds.adjust(-r, -r, r, r); - return AffectedArea(AffectedArea::PIXELS, ctx.tool.layer_id, bounds); + return AffectedArea(AffectedArea::PIXELS, dd.layer(), dd.bounds()); } case MSG_PEN_UP: { - const DrawingContext &ctx = _contexts.value(msg->contextId()); - if(ctx.tool.brush.incremental()) + QPair bounds = m_layerstack->findChangeBounds(msg->contextId()); + if(bounds.first) + return AffectedArea(AffectedArea::PIXELS, bounds.first, bounds.second); + else return AffectedArea(AffectedArea::USERATTRS, 0); - - // Non-incremental brushes get composited only at pen-up. - // We need the bounding rectangle of the entire stroke. - return AffectedArea(AffectedArea::PIXELS, ctx.tool.layer_id, ctx.boundingRect); } case MSG_FILLRECT: { const FillRect &fr = msg.cast(); return AffectedArea(AffectedArea::PIXELS, fr.layer(), QRect(fr.x(), fr.y(), fr.width(), fr.height())); } - case MSG_ANNOTATION_CREATE: return AffectedArea(AffectedArea::ANNOTATION, msg.cast().id()); - case MSG_ANNOTATION_RESHAPE: return AffectedArea(AffectedArea::ANNOTATION, msg.cast().id()); - case MSG_ANNOTATION_EDIT: return AffectedArea(AffectedArea::ANNOTATION, msg.cast().id()); - case MSG_ANNOTATION_DELETE: return AffectedArea(AffectedArea::ANNOTATION, msg.cast().id()); + case MSG_ANNOTATION_CREATE: + case MSG_ANNOTATION_RESHAPE: + case MSG_ANNOTATION_EDIT: + case MSG_ANNOTATION_DELETE: + return AffectedArea(AffectedArea::ANNOTATION, msg->layer()); case MSG_REGION_MOVE: { const MoveRegion &mr = msg.cast(); @@ -1309,6 +1255,8 @@ AffectedArea StateTracker::affectedArea(protocol::MessagePtr msg) const case MSG_UNDOPOINT: return AffectedArea(AffectedArea::USERATTRS, 0); + case MSG_CANVAS_BACKGROUND: return AffectedArea(AffectedArea::PIXELS, -1, QRect(0, 0, 1, 1)); + default: #ifndef NDEBUG qWarning("%s: affects EVERYTHING", qPrintable(msg->messageName())); diff --git a/src/client/canvas/statetracker.h b/src/client/canvas/statetracker.h index da1370e0a..f5857929f 100644 --- a/src/client/canvas/statetracker.h +++ b/src/client/canvas/statetracker.h @@ -19,16 +19,15 @@ #ifndef DP_STATETRACKER_H #define DP_STATETRACKER_H -#include -#include - #include "retcon.h" #include "history.h" -#include "core/brush.h" #include "core/point.h" +#include + namespace protocol { class CanvasResize; + class CanvasBackground; class LayerCreate; class LayerAttributes; class LayerVisibility; @@ -39,7 +38,10 @@ namespace protocol { class ToolChange; class PenMove; class PenUp; + class DrawDabsClassic; + class DrawDabsPixel; class PutImage; + class PutTile; class FillRect; class UndoPoint; class Undo; @@ -59,49 +61,6 @@ class QTimer; namespace canvas { -struct ToolContext { - ToolContext() : layer_id(-1) {} - ToolContext(int layer, const paintcore::Brush &b) : layer_id(layer), brush(b) { } - - int layer_id; - paintcore::Brush brush; - - void updateFromToolchange(const protocol::ToolChange &cmd); - - bool operator==(const ToolContext &other) const { - return layer_id == other.layer_id && brush == other.brush; - } - bool operator!=(const ToolContext &other) const { - return layer_id != other.layer_id || brush != other.brush; - } -}; - -/** - * \brief User state - * - * The drawing context captures the state needed by a single user for drawing. - */ -struct DrawingContext { - DrawingContext() : pendown(false) {} - - //! Currently selected tool - ToolContext tool; - - //! Last pen-move point - paintcore::Point lastpoint; - - //! Is the stroke currently in progress? - bool pendown; - - //! State of the current stroke - paintcore::StrokeState stroke; - - //! Bounding rectangle of current/last stroke - // This is used to determine if strokes (potentially) - // intersect and a canvas rollback/replay is needed. - QRect boundingRect; -}; - class StateTracker; class CanvasModel; @@ -119,7 +78,7 @@ class StateSavepoint { ~StateSavepoint(); void toDatastream(QDataStream &ds) const; - static StateSavepoint fromDatastream(QDataStream &ds, StateTracker *owner); + static StateSavepoint fromDatastream(QDataStream &ds); bool operator!() const { return !m_data; } bool operator==(const StateSavepoint &sp) const { return m_data == sp.m_data; } @@ -170,7 +129,7 @@ class LayerListModel; class StateTracker : public QObject { Q_OBJECT public: - StateTracker(paintcore::LayerStack *image, LayerListModel *layerlist, int myId, QObject *parent=0); + StateTracker(paintcore::LayerStack *image, LayerListModel *layerlist, uint8_t myId, QObject *parent=nullptr); StateTracker(const StateTracker &) = delete; ~StateTracker(); @@ -187,8 +146,6 @@ class StateTracker : public QObject { bool hasFullHistory() const { return m_fullhistory; } const History &getHistory() const { return m_history; } - const QHash &drawingContexts() const { return _contexts; } - /** * @brief Set if all user markers (own included) should be shown * @param showall @@ -199,30 +156,18 @@ class StateTracker : public QObject { * @brief Get the local user's ID * @return */ - int localId() const { return m_myId; } + uint8_t localId() const { return m_myId; } /** * @brief Set the local user's ID */ - void setLocalId(int id) { m_myId = id; } + void setLocalId(uint8_t id) { m_myId = id; } /** * @brief Get the paint canvas * @return */ - paintcore::LayerStack *image() const { return _image; } - - /** - * @brief Check if the given layer is locked. - * - * Note. This information should only be used for the UI and not - * for filtering events! Any command sent by the server should be - * executed, even if we think the target layer is locked! - * - * @param id layer ID - * @return true if the layer is locked - */ - bool isLayerLocked(int id) const; + paintcore::LayerStack *image() const { return m_layerstack; } //! Has the local user participated in the session yet? bool hasParticipated() const { return m_hasParticipated; } @@ -254,8 +199,7 @@ class StateTracker : public QObject { void myAnnotationCreated(int id); void layerAutoselectRequest(int); - void userMarkerAttribs(int id, const QColor &color, const QString &layer); - void userMarkerMove(int id, const QPointF &point, int trail); + void userMarkerMove(int id, int layerId, const QPoint &point); void userMarkerHide(int id); void catchupProgress(int percent); @@ -286,6 +230,7 @@ private slots: // Layer related commands void handleCanvasResize(const protocol::CanvasResize &cmd, int pos); + void handleCanvasBackground(const protocol::CanvasBackground &cmd); void handleLayerCreate(const protocol::LayerCreate &cmd); void handleLayerAttributes(const protocol::LayerAttributes &cmd); void handleLayerVisibility(const protocol::LayerVisibility &cmd); @@ -295,10 +240,10 @@ private slots: void handleLayerDefault(const protocol::DefaultLayer &cmd); // Drawing related commands - void handleToolChange(const protocol::ToolChange &cmd); - void handlePenMove(const protocol::PenMove &cmd); + void handleDrawDabs(const protocol::Message &msg); void handlePenUp(const protocol::PenUp &cmd); void handlePutImage(const protocol::PutImage &cmd); + void handlePutTile(const protocol::PutTile &cmd); void handleFillRect(const protocol::FillRect &cmd); void handleMoveRegion(const protocol::MoveRegion &cmd); @@ -307,6 +252,7 @@ private slots: void handleUndo(protocol::Undo &cmd); void makeSavepoint(int pos); void revertSavepointAndReplay(const StateSavepoint savepoint); + void handleTruncateHistory(); // Annotation related commands void handleAnnotationCreate(const protocol::AnnotationCreate &cmd); @@ -314,13 +260,11 @@ private slots: void handleAnnotationEdit(const protocol::AnnotationEdit &cmd); void handleAnnotationDelete(const protocol::AnnotationDelete &cmd); - QHash _contexts; - - paintcore::LayerStack *_image; + paintcore::LayerStack *m_layerstack; LayerListModel *m_layerlist; QString _title; - int m_myId; + uint8_t m_myId; int m_myLastLayer; History m_history; diff --git a/src/client/canvas/usercursormodel.cpp b/src/client/canvas/usercursormodel.cpp index c423140e6..cb93fdc0b 100644 --- a/src/client/canvas/usercursormodel.cpp +++ b/src/client/canvas/usercursormodel.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2015 Calle Laakkonen + Copyright (C) 2015-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ */ #include "usercursormodel.h" +#include "layerlist.h" #include #include @@ -26,7 +27,7 @@ namespace canvas { UserCursorModel::UserCursorModel(QObject *parent) - : QAbstractListModel(parent) + : QAbstractListModel(parent), m_layerlist(nullptr) { m_timerId = startTimer(1000, Qt::VeryCoarseTimer); } @@ -52,6 +53,7 @@ QVariant UserCursorModel::data(const QModelIndex &index, int role) const const UserCursor &uc = m_cursors.at(index.row()); switch(role) { case Qt::DisplayRole: return uc.name; + case Qt::DecorationRole: return uc.avatar; case IdRole: return uc.id; case PositionRole: return uc.pos; case LayerRole: return uc.layer; @@ -67,6 +69,7 @@ QHash UserCursorModel::roleNames() const { QHash roles; roles[Qt::DisplayRole] = "display"; + roles[Qt::DecorationRole] = "decoration"; roles[IdRole] = "id"; roles[PositionRole] = "pos"; roles[LayerRole] = "layer"; @@ -86,18 +89,27 @@ void UserCursorModel::setCursorName(int id, const QString &name) emit dataChanged(index, index, QVector() << Qt::DisplayRole); } -void UserCursorModel::setCursorAttributes(int id, const QColor &color, const QString &layer) +void UserCursorModel::setCursorAvatar(int id, const QPixmap &avatar) +{ + QModelIndex index; + UserCursor *uc = getOrCreate(id, index); + + uc->avatar = avatar; + + emit dataChanged(index, index, QVector() << Qt::DecorationRole); +} + +void UserCursorModel::setCursorColor(int id, const QColor &color) { QModelIndex index; UserCursor *uc = getOrCreate(id, index); uc->color = color; - uc->layer = layer; - emit dataChanged(index, index, QVector() << LayerRole << ColorRole); + emit dataChanged(index, index, QVector() << ColorRole); } -void UserCursorModel::setCursorPosition(int id, const QPointF &pos) +void UserCursorModel::setCursorPosition(int id, int layerId, const QPoint &pos) { QModelIndex index; UserCursor *uc = getOrCreate(id, index); @@ -111,6 +123,18 @@ void UserCursorModel::setCursorPosition(int id, const QPointF &pos) roles << VisibleRole; } + if(layerId>0 && layerId != uc->layerId) { + uc->layerId = layerId; + QString layerName = QStringLiteral("???"); + if(m_layerlist) { + QVariant ln = m_layerlist->layerIndex(layerId).data(LayerListModel::TitleRole); + if(!ln.isNull()) + layerName = ln.toString(); + } + uc->layer = layerName; + roles << LayerRole; + } + emit dataChanged(index, index, roles); } @@ -143,7 +167,17 @@ UserCursor *UserCursorModel::getOrCreate(int id, QModelIndex &idx) } beginInsertRows(QModelIndex(), m_cursors.size(), m_cursors.size()); - m_cursors.append(UserCursor { id, false, QDateTime::currentMSecsSinceEpoch(), QPointF(), QStringLiteral("#%1").arg(id), QString(), QColor(Qt::black)}); + m_cursors.append(UserCursor { + id, + false, + QDateTime::currentMSecsSinceEpoch(), + 0, + QPoint(), + QStringLiteral("#%1").arg(id), + QString(), + QColor(Qt::black), + QPixmap() + }); endInsertRows(); idx = index(m_cursors.size()-1); diff --git a/src/client/canvas/usercursormodel.h b/src/client/canvas/usercursormodel.h index d20668cb1..7cd2d036b 100644 --- a/src/client/canvas/usercursormodel.h +++ b/src/client/canvas/usercursormodel.h @@ -24,18 +24,23 @@ #include #include #include +#include namespace canvas { +class LayerListModel; + struct UserCursor { int id; bool visible; qint64 lastMoved; + int layerId; - QPointF pos; + QPoint pos; QString name; QString layer; QColor color; + QPixmap avatar; }; class UserCursorModel : public QAbstractListModel @@ -44,6 +49,7 @@ class UserCursorModel : public QAbstractListModel public: enum UserCursorRoles { // DisplayRole is used to get the name + // DecorationRole is used to get the avatar IdRole = Qt::UserRole + 10, PositionRole, LayerRole, @@ -53,6 +59,13 @@ class UserCursorModel : public QAbstractListModel explicit UserCursorModel(QObject *parent=nullptr); + /** + * @brief Set the layer list model. + * + * Layer names are read from here + */ + void setLayerList(LayerListModel *layers) { m_layerlist = layers; } + int rowCount(const QModelIndex &parent=QModelIndex()) const; QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const; @@ -62,8 +75,9 @@ class UserCursorModel : public QAbstractListModel public slots: void setCursorName(int id, const QString &name); - void setCursorAttributes(int id, const QColor &color, const QString &layer); - void setCursorPosition(int id, const QPointF &pos); + void setCursorColor(int id, const QColor &color); + void setCursorPosition(int id, int layerId, const QPoint &pos); + void setCursorAvatar(int id, const QPixmap &avatar); void hideCursor(int id); void clear(); @@ -75,6 +89,7 @@ public slots: UserCursor *getOrCreate(int id, QModelIndex &index); QList m_cursors; + LayerListModel *m_layerlist; int m_timerId; }; diff --git a/src/client/canvas/userlist.cpp b/src/client/canvas/userlist.cpp index 611cb5a39..dc8b813bb 100644 --- a/src/client/canvas/userlist.cpp +++ b/src/client/canvas/userlist.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2007-2017 Calle Laakkonen + Copyright (C) 2007-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -23,6 +23,7 @@ #include #include +#include namespace canvas { @@ -35,8 +36,21 @@ UserListModel::UserListModel(QObject *parent) QVariant UserListModel::data(const QModelIndex& index, int role) const { if(index.isValid() && index.row() >= 0 && index.row() < m_users.size()) { - if(role == Qt::DisplayRole) - return QVariant::fromValue(m_users.at(index.row())); + const User &u = m_users.at(index.row()); + switch(role) { + case IdRole: return u.id; + case Qt::DisplayRole: + case NameRole: return u.name; + case Qt::DecorationRole: + case AvatarRole: return u.avatar; + case IsOpRole: return u.isOperator; + case IsTrustedRole: return u.isTrusted; + case IsModRole: return u.isMod; + case IsAuthRole: return u.isAuth; + case IsBotRole: return u.isBot; + case IsLockedRole: return u.isLocked; + case IsMutedRole: return u.isMuted; + } } return QVariant(); @@ -57,9 +71,11 @@ void UserListModel::addUser(const User &user) if(u.id == user.id) { qWarning() << "replacing user" << u.id << u.name << "with" << user.name; u.name = user.name; + u.avatar = user.avatar; u.isLocal = user.isLocal; u.isAuth = user.isAuth; u.isMod = user.isMod; + u.isBot = user.isBot; u.isMuted = user.isMuted; QModelIndex idx = index(i); @@ -88,6 +104,20 @@ void UserListModel::updateOperators(const QList ids) } } +void UserListModel::updateTrustedUsers(const QList trustedIds) +{ + for(int i=0;i ids) { for(int i=0;i UserListModel::lockList() const return locks; } +QList UserListModel::trustedList() const +{ + QList ids; + for(int i=0;i0 && userId<255); + QList ids = lockList(); if(lock) { if(!ids.contains(userId)) @@ -220,13 +262,29 @@ protocol::MessagePtr UserListModel::getOpUserCommand(int localId, int userId, bo Q_ASSERT(userId>0 && userId<255); QList ops = operatorList(); - - if(op) - ops.append(userId); - else + if(op) { + if(!ops.contains(userId)) + ops.append(userId); + } else { ops.removeOne(userId); + } return protocol::MessagePtr(new protocol::SessionOwner(localId, ops)); } +protocol::MessagePtr UserListModel::getTrustUserCommand(int localId, int userId, bool trust) const +{ + Q_ASSERT(userId>0 && userId<255); + + QList trusted = trustedList(); + if(trust) { + if(!trusted.contains(userId)) + trusted.append(userId); + } else { + trusted.removeOne(userId); + } + + return protocol::MessagePtr(new protocol::TrustedUsers(localId, trusted)); +} + } diff --git a/src/client/canvas/userlist.h b/src/client/canvas/userlist.h index 1cb2e5c3f..990556a8a 100644 --- a/src/client/canvas/userlist.h +++ b/src/client/canvas/userlist.h @@ -21,6 +21,7 @@ #include #include +#include class QJsonArray; @@ -32,16 +33,14 @@ namespace canvas { * @brief Information about a user */ struct User { - User() : User(0, QString(), false, false, false) {} - User(int id_, const QString &name_, bool local, bool auth, bool mod) - : id(id_), name(name_), isLocal(local), isOperator(false), isMod(mod), isAuth(auth), isLocked(false), isMuted(false) - {} - int id; QString name; + QPixmap avatar; bool isLocal; bool isOperator; + bool isTrusted; bool isMod; + bool isBot; bool isAuth; bool isLocked; bool isMuted; @@ -52,69 +51,96 @@ struct User { */ class UserListModel : public QAbstractListModel { Q_OBJECT - public: - UserListModel(QObject *parent=0); - - QVariant data(const QModelIndex& index, int role=Qt::DisplayRole) const; - int rowCount(const QModelIndex& parent=QModelIndex()) const; - - void addUser(const User &user); - void removeUser(int id); - void clearUsers(); - - /** - * @brief Get user info by ID - * - * This will return info about past users as well. - * @param id user id - * @return - */ - User getUserById(int id) const; - - /** - * @brief Get the name of the user with the given context ID - * - * If no such user exists, "User #X" is returned, where X is the ID number. - * @param id - * @return user name - */ - QString getUsername(int id) const; - - //! Get a list of users with operator privileges - QList operatorList() const; - - //! Get a list of users who are locked - QList lockList() const; - - //! Get the ID of the operator with the lowest ID number - int getPrimeOp() const; - - /** - * @brief Get the command for (un)locking a single user - * @param localId - * @param userId - * @param lock - * @return - */ - protocol::MessagePtr getLockUserCommand(int localId, int userId, bool lock) const; - - /** - * @brief Get the command for granting or revoking operator privileges - * @param localId - * @param userId - * @param op - * @return - */ - protocol::MessagePtr getOpUserCommand(int localId, int userId, bool op) const; - - public slots: - void updateOperators(const QList operatorIds); - void updateLocks(const QList lockedUserIds); - void updateMuteList(const QJsonArray &mutedUserIds); - - private: - QVector m_users; - QHash m_pastUsers; +public: + enum UserListRoles { + IdRole = Qt::UserRole + 1, + NameRole, + AvatarRole, + IsOpRole, + IsTrustedRole, + IsModRole, + IsAuthRole, + IsBotRole, + IsLockedRole, + IsMutedRole + }; + + UserListModel(QObject *parent=nullptr); + + QVariant data(const QModelIndex& index, int role=Qt::DisplayRole) const; + int rowCount(const QModelIndex& parent=QModelIndex()) const; + + void addUser(const User &user); + void removeUser(int id); + void clearUsers(); + + /** + * @brief Get user info by ID + * + * This will return info about past users as well. + * @param id user id + * @return + */ + User getUserById(int id) const; + + /** + * @brief Get the name of the user with the given context ID + * + * If no such user exists, "User #X" is returned, where X is the ID number. + * @param id + * @return user name + */ + QString getUsername(int id) const; + + //! Get a list of users with operator privileges + QList operatorList() const; + + //! Get a list of users who are locked + QList lockList() const; + + //! Get a list of trusted users + QList trustedList() const; + + //! Get the ID of the operator with the lowest ID number + // TODO replace this by serverside auto-resetter selection + int getPrimeOp() const; + + /** + * @brief Get the command for (un)locking a single user + * @param localId + * @param userId + * @param lock + * @return + */ + protocol::MessagePtr getLockUserCommand(int localId, int userId, bool lock) const; + + /** + * @brief Get the command for granting or revoking operator privileges + * @param localId + * @param userId + * @param op + * @return + */ + protocol::MessagePtr getOpUserCommand(int localId, int userId, bool op) const; + + /** + * @brief Get the command for granting or revoking trusted status + * @param localId + * @param userId + * @param trusted + * @return + */ + protocol::MessagePtr getTrustUserCommand(int localId, int userId, bool op) const; + +public slots: + void updateOperators(const QList operatorIds); + void updateTrustedUsers(const QList trustedIds); + void updateLocks(const QList lockedUserIds); + void updateMuteList(const QJsonArray &mutedUserIds); + +private: + QVector m_users; + QHash m_pastUsers; }; } diff --git a/src/client/core/annotationmodel.cpp b/src/client/core/annotationmodel.cpp index e0222fffc..efc6cc059 100644 --- a/src/client/core/annotationmodel.cpp +++ b/src/client/core/annotationmodel.cpp @@ -85,12 +85,12 @@ void AnnotationModel::addAnnotation(const Annotation &annotation) endInsertRows(); } -void AnnotationModel::addAnnotation(int id, const QRect &rect) +void AnnotationModel::addAnnotation(uint16_t id, const QRect &rect) { addAnnotation(Annotation {id, QString(), rect, QColor(Qt::transparent), false, 0}); } -void AnnotationModel::deleteAnnotation(int id) +void AnnotationModel::deleteAnnotation(uint16_t id) { int idx = findById(id); if(idx<0) { @@ -103,7 +103,7 @@ void AnnotationModel::deleteAnnotation(int id) endRemoveRows(); } -void AnnotationModel::reshapeAnnotation(int id, const QRect &newrect) +void AnnotationModel::reshapeAnnotation(uint16_t id, const QRect &newrect) { int idx = findById(id); if(idx<0) { @@ -115,7 +115,7 @@ void AnnotationModel::reshapeAnnotation(int id, const QRect &newrect) emit dataChanged(index(idx), index(idx), QVector() << RectRole); } -void AnnotationModel::changeAnnotation(int id, const QString &newtext, bool protect, int valign, const QColor &bgcolor) +void AnnotationModel::changeAnnotation(uint16_t id, const QString &newtext, bool protect, int valign, const QColor &bgcolor) { int idx = findById(id); if(idx<0) { @@ -137,7 +137,7 @@ void AnnotationModel::setAnnotations(const QList &annotations) endResetModel(); } -const Annotation *AnnotationModel::getById(int id) const +const Annotation *AnnotationModel::getById(uint16_t id) const { for(const Annotation &a : m_annotations) if(a.id == id) @@ -145,7 +145,7 @@ const Annotation *AnnotationModel::getById(int id) const return nullptr; } -int AnnotationModel::findById(int id) const +int AnnotationModel::findById(uint16_t id) const { for(int i=0;i AnnotationModel::getEmptyIds() const +QList AnnotationModel::getEmptyIds() const { - QList ids; + QList ids; for(const Annotation &a : m_annotations) { if(a.isEmpty()) ids << a.id; @@ -239,7 +239,7 @@ void Annotation::paint(QPainter *painter, const QRectF &paintrect) const QImage Annotation::toImage() const { - QImage img(rect.size(), QImage::Format_ARGB32); + QImage img(rect.size(), QImage::Format_ARGB32_Premultiplied); img.fill(0); QPainter painter(&img); paint(&painter, QRectF(0, 0, rect.width(), rect.height())); diff --git a/src/client/core/annotationmodel.h b/src/client/core/annotationmodel.h index 751d16845..f1d73f71f 100644 --- a/src/client/core/annotationmodel.h +++ b/src/client/core/annotationmodel.h @@ -30,12 +30,12 @@ class QImage; namespace paintcore { struct Annotation { - int id; + uint16_t id; QString text; QRect rect; QColor background; bool protect; - int valign; + uint8_t valign; enum Handle {OUTSIDE, TRANSLATE, RS_TOPLEFT, RS_TOPRIGHT, RS_BOTTOMRIGHT, RS_BOTTOMLEFT, RS_TOP, RS_RIGHT, RS_BOTTOM, RS_LEFT}; static const int HANDLE_SIZE = 10; @@ -93,30 +93,30 @@ class AnnotationModel : public QAbstractListModel { bool isEmpty() const { return m_annotations.isEmpty(); } void addAnnotation(const Annotation &annotation); - void addAnnotation(int id, const QRect &rect); - void deleteAnnotation(int id); - void reshapeAnnotation(int id, const QRect &newrect); - void changeAnnotation(int id, const QString &newtext, bool protect, int valign, const QColor &bgcolor); + void addAnnotation(uint16_t id, const QRect &rect); + void deleteAnnotation(uint16_t id); + void reshapeAnnotation(uint16_t id, const QRect &newrect); + void changeAnnotation(uint16_t id, const QString &newtext, bool protect, int valign, const QColor &bgcolor); void setAnnotations(const QList &list); QList getAnnotations() const { return m_annotations; } const Annotation *annotationAtPos(const QPoint &pos, qreal zoom) const; - Annotation::Handle annotationHandleAt(int id, const QPoint &point, qreal zoom) const; - Annotation::Handle annotationAdjustGeometry(int id, Annotation::Handle handle, const QPoint &delta); + Annotation::Handle annotationHandleAt(uint16_t id, const QPoint &point, qreal zoom) const; + Annotation::Handle annotationAdjustGeometry(uint16_t id, Annotation::Handle handle, const QPoint &delta); - const Annotation *getById(int id) const; + const Annotation *getById(uint16_t id) const; //! Return the IDs of annotations that have no text content - QList getEmptyIds() const; + QList getEmptyIds() const; void clear(); private: AnnotationModel(const AnnotationModel *orig, QObject *newParent); - int findById(int id) const; + int findById(uint16_t id) const; QList m_annotations; }; diff --git a/src/client/core/brush.cpp b/src/client/core/brush.cpp deleted file mode 100644 index f9e663d89..000000000 --- a/src/client/core/brush.cpp +++ /dev/null @@ -1,140 +0,0 @@ -/* - Drawpile - a collaborative drawing program. - - Copyright (C) 2006-2013 Calle Laakkonen - - Drawpile 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 3 of the License, or - (at your option) any later version. - - Drawpile 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 Drawpile. If not, see . -*/ - -#include -#include "brush.h" - -namespace paintcore { - -/** - * \brief Linear interpolation - * This is used to figure out correct size, opacity, hardness and - * color values. - */ -inline qreal interpolate(qreal a, qreal b, qreal alpha) -{ - Q_ASSERT(alpha>=0 && alpha<=1); - //return a*alpha + b*(1-alpha); - return (a-b) * alpha + b; -} - -/** - * A brush with the specified settings is constructed. The brush is - * pressure insensitive by default. - * @param size brush size. Zero means a single pixel brush - * @param hardness brush hardness - * @param opacity brush opacity - * @param color brush color - * @param spacing brush spacing hint, as percentage of brush size - */ -Brush::Brush(int size, qreal hardness, qreal opacity, const QColor& color, int spacing) - : _size1(size), _size2(size), - _hardness1(hardness), _hardness2(hardness), - _opacity1(opacity), _opacity2(opacity), - _smudge1(0), _smudge2(0), - _color(color),_spacing(spacing), _resmudge(0), _blend(BlendMode::MODE_NORMAL), - _subpixel(false), _incremental(true) -{ - Q_ASSERT(size>0); - Q_ASSERT(hardness>=0 && hardness <=1); - Q_ASSERT(opacity>=0 && opacity <=1); - Q_ASSERT(spacing>=0 && spacing <=100); -} - -/** - * @brief Get the real size for the given pressure - * @param pressure - * @return size - * @pre 0 <= pressure <= 1 - * @post 0.5 <= RESULT - */ -qreal Brush::fsize(qreal pressure) const -{ - //return qMax(0.5, interpolate(size1(), size2(), pressure)); - return interpolate(size1(), size2(), pressure); -} - -/** - * Get the brush hardness for certain pressure. - * @param pressure pen pressure - * @return hardness - * @pre 0 <= pressure <= 1 - * @post 0 <= RESULT <= 1 - */ -qreal Brush::hardness(qreal pressure) const -{ - return interpolate(hardness1(), hardness2(), pressure); -} - -/** - * Get the brush opacity for certain pressure. - * @param pressure pen pressure - * @return opacity - * @pre 0 <= pressure <= 1 - * @post 0 <= RESULT <= 1 - */ -qreal Brush::opacity(qreal pressure) const -{ - return interpolate(opacity1(), opacity2(), pressure); -} - -/** - * Get color smudging pressure for the given stylus pressure - * @param pressure pen pressure - * @return opacity - * @pre 0 <= pressure <= 1 - * @post 0 <= RESULT <= 1 - */ -qreal Brush::smudge(qreal pressure) const -{ - return interpolate(smudge1(), smudge2(), pressure); -} - -qreal Brush::spacingDist(qreal pressure) const -{ - return spacing() / 100.0 * fsize(pressure); -} - -bool Brush::isOpacityVariable() const -{ - return qAbs(opacity1() - opacity2()) > (1/256.0); -} - -bool Brush::operator==(const Brush& brush) const -{ - return size1() == brush.size1() && size2() == brush.size2() && - qAbs(hardness1() - brush.hardness1()) <= 1.0/256.0 && - qAbs(hardness2() - brush.hardness2()) <= 1.0/256.0 && - qAbs(opacity1() - brush.opacity1()) <= 1.0/256.0 && - qAbs(opacity2() - brush.opacity2()) <= 1.0/256.0 && - qAbs(smudge1() - brush.smudge1()) <= 1.0/256.0 && - qAbs(smudge2() - brush.smudge2()) <= 1.0/256.0 && - color() == brush.color() && - spacing() == brush.spacing() && - subpixel() == brush.subpixel() && - incremental() == brush.incremental() && - blendingMode() == brush.blendingMode(); -} - -bool Brush::operator!=(const Brush& brush) const -{ - return !(*this == brush); -} - -} diff --git a/src/client/core/brush.h b/src/client/core/brush.h deleted file mode 100644 index b2ac13fb4..000000000 --- a/src/client/core/brush.h +++ /dev/null @@ -1,148 +0,0 @@ -/* - Drawpile - a collaborative drawing program. - - Copyright (C) 2006-2013 Calle Laakkonen - - Drawpile 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 3 of the License, or - (at your option) any later version. - - Drawpile 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 Drawpile. If not, see . -*/ -#ifndef BRUSH_H -#define BRUSH_H - -#include - -#include "blendmodes.h" - -namespace paintcore { - -/** - * @brief A brush for drawing onto a layer - * - * This class contains the parameters defining a brush. - */ -class Brush -{ -public: - //! Construct a brush - Brush(int size=1, qreal hardness=1.0, qreal opacity=1.0, - const QColor& color=Qt::black, int spacing=25); - - //! Set size for heavy brush - void setSize(int size) { Q_ASSERT(size>0); _size1 = size; } - //! Set size for light brush - void setSize2(int size) { Q_ASSERT(size>0); _size2 = size; } - - int size1() const { return _size1; } - int size2() const { return _size2; } - - //! Set hardness for heavy brush - void setHardness(qreal hardness) { Q_ASSERT(hardness>=0 && hardness<=1); _hardness1 = hardness; } - //! Set hardness for light brush - void setHardness2(qreal hardness) { Q_ASSERT(hardness>=0 && hardness<=1); _hardness2 = hardness; } - - qreal hardness1() const { return _hardness1; } - qreal hardness2() const { return _hardness2; } - - //! Set opacity for heavy brush - void setOpacity(qreal opacity) { Q_ASSERT(opacity>=0 && opacity<=1); _opacity1 = opacity; } - //! Set opacity for light brush - void setOpacity2(qreal opacity) { Q_ASSERT(opacity>=0 && opacity<=1); _opacity2 = opacity; } - - qreal opacity1() const { return _opacity1; } - qreal opacity2() const { return _opacity2; } - - //! Set color for heavy brush - void setColor(const QColor& color) { _color = color; } - - //! Set smudging pressure for heavy brush - void setSmudge(qreal smudge) { Q_ASSERT(smudge>=0 && smudge<=1); _smudge1 = smudge; } - //! Set smudging pressure for light brush - void setSmudge2(qreal smudge) { Q_ASSERT(smudge>=0 && smudge<=1); _smudge2 = smudge; } - - qreal smudge1() const { return _smudge1; } - qreal smudge2() const { return _smudge2; } - - const QColor &color() const { return _color; } - - void setSpacing(int spacing) { Q_ASSERT(spacing >= 0 && spacing <= 100); _spacing = spacing; } - int spacing() const { return _spacing; } - - //! Set smudge colir resampling frequency (0 resamples on every dab) - void setResmudge(int resmudge) { Q_ASSERT(resmudge >= 0); _resmudge = resmudge; } - int resmudge() const { return _resmudge; } - - void setSubpixel(bool sp) { _subpixel = sp; } - bool subpixel() const { return _subpixel; } - - void setIncremental(bool incremental) { _incremental = incremental; } - bool incremental() const { return _incremental; } - - void setBlendingMode(BlendMode::Mode mode) { _blend = mode; } - BlendMode::Mode blendingMode() const { return _blend; } - - //! Get interpolated size - qreal fsize(qreal pressure) const; - //! Get interpolated hardness - qreal hardness(qreal pressure) const; - //! Get interpolated opacity - qreal opacity(qreal pressure) const; - //! Get interpolated smudging pressure - qreal smudge(qreal pressure) const; - //! Get the dab spacing distance - qreal spacingDist(qreal pressure) const; - - //! Does opacity vary with pressure? - bool isOpacityVariable() const; - - //! Get a color with transparency for approximating strokes with solid lines - QColor approxColor() const; - - bool operator==(const Brush& brush) const; - bool operator!=(const Brush& brush) const; - -private: - int _size1, _size2; - qreal _hardness1, _hardness2; - qreal _opacity1, _opacity2; - qreal _smudge1, _smudge2; - QColor _color; - int _spacing; - int _resmudge; - BlendMode::Mode _blend; - bool _subpixel; - bool _incremental; -}; - -struct StrokeState { - // stroked distance - qreal distance; - - // number of dabs since last smudge color sampling - int smudgeDistance; - - // the smudged color. This is used instead of the normal - // brush color when smudging is enabled, so initialize it to the brush - // color at the start of the stroke. - QColor smudgeColor; - - StrokeState() : distance(0), smudgeDistance(0) { } - explicit StrokeState(const Brush &b) : distance(0), smudgeDistance(0), smudgeColor(b.color()) { } -}; - -} - -Q_DECLARE_TYPEINFO(paintcore::Brush, Q_MOVABLE_TYPE); - -#endif - - diff --git a/src/client/core/brushmask.cpp b/src/client/core/brushmask.cpp index 75803b152..dfb6b5636 100644 --- a/src/client/core/brushmask.cpp +++ b/src/client/core/brushmask.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2013-2014 Calle Laakkonen + Copyright (C) 2013-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,253 +19,53 @@ #include "brushmask.h" -#include - #include namespace paintcore { -namespace { - -template T square(T x) { return x*x; } +template static T square(T x) { return x*x; } -typedef QVector LUT; static const int LUT_RADIUS = 128; -static QCache LUT_CACHE; - -// Generate a lookup table for Gimp style exponential brush shape -// The value at r² (where r is distance from brush center, scaled to LUT_RADIUS) is -// the opaqueness of the pixel. -LUT makeGimpStyleBrushLUT(float hardness) -{ - qreal exponent; - if ((1.0 - hardness) < 0.0000004) - exponent = 1000000.0; - else - exponent = 0.4 / (1.0 - hardness); - LUT lut(square(LUT_RADIUS)); - for(int i=0;i=0 && h<=100); - if(!LUT_CACHE.contains(h)) - LUT_CACHE.insert(h, new LUT(makeGimpStyleBrushLUT(hardness))); - - return *LUT_CACHE[h]; -} - -BrushStamp makeMask(const Brush &brush, float pressure) -{ - const float r = brush.fsize(pressure) / 2.0f; - const float opacity = brush.opacity(pressure) * 255; - - // generate mask - QVector data; - int diameter; - int stampOffset; - - if(r<1) { - // special case for single pixel brush - diameter=3; - stampOffset = -1; - data.resize(3*3); - data.fill(0); - data[4] = opacity; - - } else { - const LUT lut = cachedGimpStyleBrushLUT(brush.hardness(pressure)); - const float lut_scale = square((LUT_RADIUS-1) / r); - - float offset; - float fudge=1; - diameter = ceil(r*2) + 2; - - if(diameter%2==0) { - ++diameter; - offset = -1.0; - - if(r<8) - fudge = 0.9; - } else { - offset = -0.5; - } - stampOffset = -diameter/2; - - // empirically determined fudge factors to make small brushes look nice - if(r<2.5) - fudge=0.8; - - else if(r<4) - fudge=0.8; - - data.resize(square(diameter)); - uchar *ptr = data.data(); - - for(int y=0;y lut; + if(lut.isEmpty()) { + // Generate a lookup table for a Gimp style exponential brush shape + const qreal hardness = 0.5; + const qreal exponent = 0.4 / (1.0 - hardness); + lut.resize(square(LUT_RADIUS)); + for(int i=0;i data(square(diameter)); + QVector data(square(diameter), 0); uchar *ptr = data.data(); for(int y=0;y1 || yfrac<0 || yfrac>1) - qWarning("offsetMask(mask, %f, %f): offset out of bounds!", xfrac, yfrac); -#endif - - const int diameter = mask.diameter(); - - const qreal kernel[] = { - xfrac*yfrac, - (1.0-xfrac)*yfrac, - xfrac*(1.0-yfrac), - (1.0-xfrac)*(1.0-yfrac) - }; -#ifndef NDEBUG - const qreal kernelsum = fabs(kernel[0]+kernel[1]+kernel[2]+kernel[3]-1.0); - if(kernelsum>0.001) - qWarning("offset kernel sum error=%f", kernelsum); -#endif - - const uchar *src = mask.data(); - - QVector data(square(diameter)); - uchar *ptr = data.data(); - -#if 0 - for(int y=-1;y0); - if(radius < 8) - s = makeHighresMask(brush, point.pressure()); - else - s = makeMask(brush, point.pressure()); - - const float fx = floor(point.x()); - const float fy = floor(point.y()); - s.left += fx; - s.top += fy; - - float xfrac = point.x()-fx; - float yfrac = point.y()-fy; - - if(xfrac<0.5) { - xfrac += 0.5; - s.left--; - } else - xfrac -= 0.5; - - if(yfrac<0.5) { - yfrac += 0.5; - s.top--; - } else - yfrac -= 0.5; - - s.mask = offsetMask(s.mask, xfrac, yfrac); - - } else { - s = makeMask(brush, point.pressure()); - s.left += point.x(); - s.top += point.y(); - } + // Sampling mask doesn't change size very often + static BrushMask mask; + if(mask.diameter() != radius*2) + mask = makeColorSamplingStamp(radius); - return s; + return BrushStamp { point.x() - radius, point.y() - radius, mask }; } } diff --git a/src/client/core/brushmask.h b/src/client/core/brushmask.h index 6efc52acf..1a9f50014 100644 --- a/src/client/core/brushmask.h +++ b/src/client/core/brushmask.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2013-2014 Calle Laakkonen + Copyright (C) 2013-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,9 +19,7 @@ #ifndef PAINTCORE_BRUSHMASK_H #define PAINTCORE_BRUSHMASK_H -#include "brush.h" -#include "point.h" - +#include #include namespace paintcore { @@ -29,15 +27,15 @@ namespace paintcore { class BrushMask { public: - BrushMask() : _diameter(0) {} - BrushMask(int dia, const QVector data) : _diameter(dia), _data(data) {} + BrushMask() : m_diameter(0) {} + BrushMask(int dia, const QVector data) : m_diameter(dia), m_data(data) {} /** * @brief Get the brush mask diameter. * * @return */ - int diameter() const { return _diameter; } + int diameter() const { return m_diameter; } /** * @brief Get brush mask data @@ -47,23 +45,21 @@ class BrushMask * @return data * @pre diameter() > 0 */ - const uchar *data() const { return _data.data(); } + const uchar *data() const { return m_data.data(); } private: - int _diameter; - QVector _data; + int m_diameter; + QVector m_data; }; struct BrushStamp { int left; int top; BrushMask mask; - - BrushStamp() : left(0), top(0) { } - BrushStamp(int x, int y, const BrushMask &m) : left(x), top(y), mask(m) { } }; -BrushStamp makeGimpStyleBrushStamp(const Brush &brush, const Point &point); +//! Make a brush stamp for area color picking +BrushStamp makeColorSamplingStamp(int radius, const QPoint &point); } diff --git a/src/client/core/floodfill.cpp b/src/client/core/floodfill.cpp index 061cdc756..1c87682b1 100644 --- a/src/client/core/floodfill.cpp +++ b/src/client/core/floodfill.cpp @@ -33,8 +33,8 @@ class Floodfill { public: Floodfill(const LayerStack *image, int sourceLayer, bool merge, const QColor &color, int colorTolerance, unsigned int sizelimit) : source(image), - scratch(0, 0, QString(), Qt::transparent, image->size()), - fill(0, 0, QString(), Qt::transparent, image->size()), + scratch(0, QString(), Qt::transparent, image->size()), + fill(0, QString(), Qt::transparent, image->size()), layer(sourceLayer), merge(merge), fillColor(color.rgba()), @@ -45,7 +45,7 @@ class Floodfill { Tile &scratchTile(int x, int y) { - Tile &t = scratch.rtile(x, y); + Tile &t = EditableLayer(&scratch, nullptr).rtile(x, y); if(t.isNull()) { if(merge) { t = source->getFlatTile(x, y); @@ -64,7 +64,7 @@ class Floodfill { } Tile &fillTile(int x, int y) { - Tile &t = fill.rtile(x, y); + Tile &t = EditableLayer(&fill, nullptr).rtile(x, y); if(t.isNull()) t = Tile(Qt::transparent); @@ -80,7 +80,7 @@ class Floodfill { const Tile &t = scratchTile(tx, ty); - return t.data()[y*Tile::SIZE + x]; + return t.constData()[y*Tile::SIZE + x]; } void setPixel(int x, int y) { @@ -96,9 +96,6 @@ class Floodfill { bool isSameColor(QRgb c1, QRgb c2) { // TODO better color distance function - c1 = qPremultiply(c1); - c2 = qPremultiply(c2); - int r = (c1 & 0xff) - (signed int)(c2 & 0xff); int g = (c1>>8 & 0xff) - (signed int)(c2>>8 & 0xff); int b = (c1>>16 & 0xff) - (signed int)(c2>>16 & 0xff); @@ -278,7 +275,7 @@ FillResult expandFill(const FillResult &input, int expansion, const QColor &colo if(input.image.isNull() || expansion<1) return input; - Q_ASSERT(input.image.format() == QImage::Format_ARGB32); + Q_ASSERT(input.image.format() == QImage::Format_ARGB32_Premultiplied); FillResult out; diff --git a/src/client/core/layer.cpp b/src/client/core/layer.cpp index a78bfbb75..76195739f 100644 --- a/src/client/core/layer.cpp +++ b/src/client/core/layer.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2008-2017 Calle Laakkonen + Copyright (C) 2008-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -16,22 +16,21 @@ You should have received a copy of the GNU General Public License along with Drawpile. If not, see . */ -#include -#include -#include -#include -#include #include "layerstack.h" #include "layer.h" #include "tile.h" -#include "brush.h" #include "brushmask.h" #include "point.h" #include "blendmodes.h" #include "rasterop.h" #include "concurrent.h" +#include +#include +#include +#include + namespace paintcore { namespace { @@ -91,7 +90,7 @@ QColor _sampleEdgeColors(const Layer *layer, bool top, bool right, bool bottom, } } - return QColor::fromRgba(color); + return QColor::fromRgba(qUnpremultiply(color)); } } @@ -103,29 +102,31 @@ QColor _sampleEdgeColors(const Layer *layer, bool top, bool right, bool bottom, * @param color layer color * @parma size layer size */ -Layer::Layer(LayerStack *owner, int id, const QString& title, const QColor& color, const QSize& size) - : m_owner(owner), m_info({id, title, 255, false, BlendMode::MODE_NORMAL}), - m_width(0), m_height(0), m_xtiles(0), m_ytiles(0) +Layer::Layer(int id, const QString& title, const QColor& color, const QSize& size) + : m_info({id, title, 255, false, false, BlendMode::MODE_NORMAL}), + m_width(size.width()), + m_height(size.height()), + m_xtiles(Tile::roundTiles(size.width())), + m_ytiles(Tile::roundTiles(size.height())) { - resize(0, size.width(), size.height(), 0); - - if(color.alpha() > 0) { - m_tiles.fill(Tile(color)); - } + m_tiles = QVector( + m_xtiles * m_ytiles, + color.alpha() > 0 ? Tile(color) : Tile() + ); } -Layer::Layer(LayerStack *owner, int id, const QSize &size) - : Layer(owner, id, QString(), Qt::transparent, size) +Layer::Layer(int id, const QSize &size) + : Layer(id, QString(), Qt::transparent, size) { // sublayers are used for indirect drawing and previews } -Layer::Layer(const Layer &layer, LayerStack *newOwner) - : m_owner(newOwner ? newOwner : layer.m_owner), - m_info(layer.m_info), +Layer::Layer(const Layer &layer) + : m_info(layer.m_info), + m_changeBounds(layer.m_changeBounds), + m_tiles(layer.m_tiles), m_width(layer.m_width), m_height(layer.m_height), - m_xtiles(layer.m_xtiles), m_ytiles(layer.m_ytiles), - m_tiles(layer.m_tiles) + m_xtiles(layer.m_xtiles), m_ytiles(layer.m_ytiles) { // Hidden and ephemeral layers are not copied, since hiding a sublayer is // effectively the same as deleting it and ephemeral layers are not considered @@ -141,122 +142,8 @@ Layer::~Layer() { delete sl; } -void Layer::resize(int top, int right, int bottom, int left) -{ - // Minimize amount of data that needs to be copied - optimize(); - - // Resize sublayers - for(Layer *sl : m_sublayers) - sl->resize(top, right, bottom, left); - - // Calculate new size - int width = left + m_width + right; - int height = top + m_height + bottom; - - int xtiles = Tile::roundTiles(width); - int ytiles = Tile::roundTiles(height); - QVector tiles(xtiles * ytiles); - - // if there is no old content, resizing is simple - bool hascontent = false; - for(int i=0;i0, right>0, bottom>0, left>0); - if(bgcolor.alpha()>0) - bgtile = Tile(bgcolor); - } - - if((left % Tile::SIZE) || (top % Tile::SIZE)) { - // If top/left adjustment is not divisble by tile size, - // we need to move the layer content - - QImage oldcontent = toImage(); - - m_width = width; - m_height = height; - m_xtiles = xtiles; - m_ytiles = ytiles; - m_tiles = tiles; - if(left<0 || top<0) { - int cropx = 0; - if(left<0) { - cropx = -left; - left = 0; - } - int cropy = 0; - if(top<0) { - cropy = -top; - top = 0; - } - oldcontent = oldcontent.copy(cropx, cropy, oldcontent.width()-cropx, oldcontent.height()-cropy); - } - - m_tiles.fill(bgtile); - - putImage(left, top, oldcontent, BlendMode::MODE_REPLACE); - - } else { - // top/left offset is aligned at tile boundary: - // existing tile content can be reused - - const int firstrow = Tile::roundTiles(-top); - const int firstcol = Tile::roundTiles(-left); - - int oldy = firstrow; - for(int y=0;y=m_ytiles || oldx<0 || oldx>=m_xtiles) { - tiles[i] = bgtile; - - } else { - const int oldi = oldyy + oldx; - tiles[i] = m_tiles.at(oldi); - } - } - } - - m_width = width; - m_height = height; - m_xtiles = xtiles; - m_ytiles = ytiles; - m_tiles = tiles; - } -} - -void Layer::setTitle(const QString& title) -{ - if(m_info.title != title) { - m_info.title = title; - if(m_owner) - m_owner->notifyLayerInfoChange(this); - } -} - QImage Layer::toImage() const { - QImage image(m_width, m_height, QImage::Format_ARGB32); + QImage image(m_width, m_height, QImage::Format_ARGB32_Premultiplied); int i=0; for(int y=0;y=m_width || y>=m_height) return QColor(); - if(dia<=1) { - quint32 c = pixelAt(x, y); - if(qAlpha(c)==0) + if(dia<2) { + const quint32 c = pixelAt(x, y); + if(c==0) return QColor(); - return QColor::fromRgb(c); + return QColor::fromRgb(qUnpremultiply(c)); + } else { - Brush b(dia, 0.9); - BrushStamp bs = makeGimpStyleBrushStamp(b, Point(x, y, 1)); - return getDabColor(bs); + return getDabColor(makeColorSamplingStamp(dia/2, QPoint(x,y))); } } @@ -343,43 +229,6 @@ QRgb Layer::pixelAt(int x, int y) const return tile(xindex, yindex).pixel(x-xindex*Tile::SIZE, y-yindex*Tile::SIZE); } -/** - * @param opacity - */ -void Layer::setOpacity(int opacity) -{ - Q_ASSERT(opacity>=0 && opacity<256); - if(m_info.opacity != opacity) { - m_info.opacity = opacity; - markOpaqueDirty(true); - if(m_owner) - m_owner->notifyLayerInfoChange(this); - } -} - -void Layer::setBlend(BlendMode::Mode blend) -{ - if(m_info.blend != blend) { - m_info.blend = blend; - markOpaqueDirty(); - if(m_owner) - m_owner->notifyLayerInfoChange(this); - } -} - -/** - * @param hide new status - */ -void Layer::setHidden(bool hide) -{ - if(m_info.hidden != hide) { - m_info.hidden = hide; - markOpaqueDirty(true); - if(m_owner) - m_owner->notifyLayerInfoChange(this); - } -} - /** * Return a temporary layer with the original image padded and composited with the * content of this layer. @@ -405,7 +254,7 @@ Layer Layer::padImageToTileBoundary(int xpos, int ypos, const QImage &original, image = original; } else { - image = QImage(w, h, QImage::Format_ARGB32); + image = QImage(w, h, QImage::Format_ARGB32_Premultiplied); QPainter painter(&image); if(mode == BlendMode::MODE_REPLACE) { @@ -434,7 +283,7 @@ Layer Layer::padImageToTileBoundary(int xpos, int ypos, const QImage &original, } // Create scratch layers and composite - Layer scratch(nullptr, 0, QString(), Qt::transparent, QSize(w,h)); + Layer scratch(0, QString(), Qt::transparent, QSize(w,h)); Layer imglayer = scratch; // Copy image pixels to image layer @@ -457,324 +306,486 @@ Layer Layer::padImageToTileBoundary(int xpos, int ypos, const QImage &original, } // Merge image using standard layer compositing ops - imglayer.setBlend(mode); + EditableLayer(&imglayer, nullptr).setBlend(mode); + EditableLayer(&scratch, nullptr).merge(&imglayer); - scratch.merge(&imglayer); return scratch; } /** - * @param x x coordinate - * @param y y coordinate - * @param image the image to draw - * @param mode blending/compositing mode (see protocol::PutImage) + * @brief Get a weighted average of the layer's color, using the given brush mask as the weight + * + * @param stamp + * @return color average */ -void Layer::putImage(int x, int y, QImage image, BlendMode::Mode mode) -{ - Q_ASSERT(image.format() == QImage::Format_ARGB32); - - // Check if the image is completely outside the layer - if(x >= m_width || y >= m_height || x+image.width() < 0 || y+image.height() < 0) - return; - - // Crop image if x or y are negative - if(x<0 || y<0) { - const int xCrop = x<0 ? -x : 0; - const int yCrop = y<0 ? -y : 0; - - image = image.copy(xCrop, yCrop, image.width()-xCrop, image.height()-yCrop); - x = qMax(0, x); - y = qMax(0, y); - } - - const int x0 = Tile::roundDown(x); - const int y0 = Tile::roundDown(y); - - Layer imageLayer = padImageToTileBoundary(x, y, image, mode); - - // Replace this layer's tiles with the scratch tiles - const int tx0 = x0 / Tile::SIZE; - const int ty0 = y0 / Tile::SIZE; - const int tx1 = qMin((x0 + imageLayer.width() - 1) / Tile::SIZE, m_xtiles-1); - const int ty1 = qMin((y0 + imageLayer.height() - 1) / Tile::SIZE, m_ytiles-1); - - for(int ty=ty0;ty<=ty1;++ty) { - for(int tx=tx0;tx<=tx1;++tx) { - rtile(tx, ty) = imageLayer.tile(tx-tx0, ty-ty0); - } - } - - if(m_owner && isVisible()) { - m_owner->markDirty(QRect(x, y, image.width(), image.height())); - m_owner->notifyAreaChanged(); - } -} - -void Layer::fillRect(const QRect &rectangle, const QColor &color, BlendMode::Mode blendmode) +QColor Layer::getDabColor(const BrushStamp &stamp) const { - const QRect canvas(0, 0, m_width, m_height); - - if(rectangle.contains(canvas) && blendmode==BlendMode::MODE_REPLACE) { - // Special case: overwrite whole layer - m_tiles.fill(Tile(color)); - - } else { - // The usual case: only a portion of the layer is filled or pixel blending is needed - QRect rect = rectangle.intersected(canvas); - - uchar mask[Tile::LENGTH]; - if(blendmode==255) - memset(mask, 0xff, Tile::LENGTH); - else - memset(mask, color.alpha(), Tile::LENGTH); + // This is very much like directDab, instead we only read pixel values + const uchar *weights = stamp.mask.data(); - const int size = Tile::SIZE; - const int bottom = rect.y() + rect.height(); - const int right = rect.x() + rect.width(); + // The mask can overlap multiple tiles + const int dia = stamp.mask.diameter(); + const int bottom = qMin(stamp.top + dia, m_height); + const int right = qMin(stamp.left + dia, m_width); - const int tx0 = rect.x() / size; - const int tx1 = (right-1) / size; - const int ty0 = rect.y() / size; - const int ty1 = (bottom-1) / size; + int y = qMax(0, stamp.top); + int yb = stamp.top<0?-stamp.top:0; // y in relation to brush origin + const int x0 = qMax(0, stamp.left); + const int xb0 = stamp.left<0?-stamp.left:0; - bool canIncrOpacity; - if(blendmode==255 && color.alpha()==0) - canIncrOpacity = false; - else - canIncrOpacity = findBlendMode(blendmode).flags.testFlag(BlendMode::IncrOpacity); + qreal weight=0, red=0, green=0, blue=0, alpha=0; - for(int ty=ty0;ty<=ty1;++ty) { - for(int tx=tx0;tx<=tx1;++tx) { - int left = qMax(tx * size, rect.x()) - tx*size; - int top = qMax(ty * size, rect.y()) - ty*size; - int w = qMin((tx+1)*size, right) - tx*size - left; - int h = qMin((ty+1)*size, bottom) - ty*size - top; + // collect weighted color sums + while(y avg = m_tiles.at(i).weightedAverage(weights + yb * dia + xb, xt, yt, wb, hb, dia-wb); + weight += avg[0]; + red += avg[1]; + green += avg[2]; + blue += avg[3]; + alpha += avg[4]; - if(!t.isNull() || canIncrOpacity) - t.composite(blendmode, mask, color, left, top, w, h, 0); - } + x = (xindex+1) * Tile::SIZE; + xb = xb + wb; } + y = (yindex+1) * Tile::SIZE; + yb = yb + hb; } - if(m_owner && isVisible()) { - m_owner->markDirty(rectangle); - m_owner->notifyAreaChanged(); - } -} - -void Layer::dab(int contextId, const Brush &brush, const Point &point, StrokeState &state) -{ - Brush effective_brush = brush; - Layer *l = this; - - if(!brush.incremental()) { - // Indirect brush: use a sublayer - l = getSubLayer(contextId, brush.blendingMode(), brush.opacity(1) * 255); - - effective_brush.setOpacity(1.0); - effective_brush.setOpacity2(brush.isOpacityVariable() ? 0.0 : 1.0); - effective_brush.setBlendingMode(BlendMode::MODE_NORMAL); - - } else if(contextId<0) { - // Special case: negative context IDs are temporary overlay strokes - l = getSubLayer(contextId, brush.blendingMode(), 255); - effective_brush.setBlendingMode(BlendMode::MODE_NORMAL); - } + // Calculate final average + red /= weight; + green /= weight; + blue /= weight; + alpha /= weight; - Point p = point; - if(!effective_brush.subpixel()) { - p.setX(qFloor(p.x())); - p.setY(qFloor(p.y())); + // Unpremultiply + if(alpha>0) { + red = qMin(1.0, red/alpha); + green = qMin(1.0, green/alpha); + blue = qMin(1.0, blue/alpha); } - l->directDab(effective_brush, p, state); - - if(m_owner) - m_owner->notifyAreaChanged(); + return QColor::fromRgbF(red, green, blue, alpha); } /** - * Draw a line using either drawSoftLine or drawHardLine, depending on - * the subpixel hint of the brush. - * @param context drawing context id (needed for indirect drawing) + * Free all tiles that are completely transparent */ -void Layer::drawLine(int contextId, const Brush& brush, const Point& from, const Point& to, StrokeState &state) +void Layer::optimize() { - Brush effective_brush = brush; - Layer *l = this; - - if(!brush.incremental()) { - // Indirect brush: use a sublayer - l = getSubLayer(contextId, brush.blendingMode(), brush.opacity(1) * 255); - - effective_brush.setOpacity(1.0); - effective_brush.setOpacity2(brush.isOpacityVariable() ? 0.0 : 1.0); - effective_brush.setBlendingMode(BlendMode::MODE_NORMAL); - - } else if(contextId<0) { - // Special case: negative context IDs are temporary overlay strokes - l = getSubLayer(contextId, brush.blendingMode(), 255); - effective_brush.setBlendingMode(BlendMode::MODE_NORMAL); + // Optimize tile memory usage + for(int i=0;idrawSoftLine(effective_brush, from, to, state); - else - l->drawHardLine(effective_brush, from, to, state); - - if(m_owner) - m_owner->notifyAreaChanged(); -} - -/** - * This function is optimized for drawing with subpixel precision. - * @param brush brush to draw the line with - * @param from starting point - * @param to ending point - * @param distance distance from previous dab. - */ -void Layer::drawSoftLine(const Brush& brush, const Point& from, const Point& to, StrokeState &state) + // Delete unused sublayers + QMutableListIterator li(m_sublayers); + while(li.hasNext()) { + Layer *sl = li.next(); + if(sl->isHidden()) { + delete sl; + li.remove(); + } + } +} + +Layer *Layer::getSubLayer(int id, BlendMode::Mode blendmode, uchar opacity) { - qreal dx = to.x() - from.x(); - qreal dy = to.y() - from.y(); - const qreal dist = hypot(dx, dy); - dx = dx / dist; - dy = dy / dist; - const qreal dp = (to.pressure() - from.pressure()) / dist; - - const qreal spacing0 = qMax(1.0, brush.spacingDist(from.pressure())); - qreal i; - if(state.distance>=spacing0) - i = 0; - else if(state.distance==0) - i = spacing0; - else - i = state.distance; - - Point p(from.x() + dx*i, from.y() + dy*i, qBound(0.0, from.pressure() + dp*i, 1.0)); - - while(i<=dist) { - const qreal spacing = qMax(1.0, brush.spacingDist(p.pressure())); - directDab(brush, p, state); - p.rx() += dx * spacing; - p.ry() += dy * spacing; - p.setPressure(qBound(0.0, p.pressure() + dp * spacing, 1.0)); - i += spacing; + Q_ASSERT(id != 0); + + // See if the sublayer exists already + for(Layer *sl : m_sublayers) + if(sl->id() == id) { + if(sl->isHidden()) { + // Hidden, reset properties + sl->m_tiles.fill(Tile()); + sl->m_info.opacity = opacity; + sl->m_info.blend = blendmode; + sl->m_info.hidden = false; + sl->m_changeBounds = QRect(); + } + return sl; + } + + // Okay, try recycling a sublayer + for(Layer *sl : m_sublayers) { + if(sl->isHidden()) { + // Set these flags directly to avoid markDirty call. + // We know the layer is invisible at this point + sl->m_tiles.fill(Tile()); + sl->m_info.id = id; + sl->m_info.opacity = opacity; + sl->m_info.blend = blendmode; + sl->m_info.hidden = false; + return sl; + } } - state.distance = i-dist; + + // No available sublayers, create a new one + Layer *sl = new Layer(id, QSize(m_width, m_height)); + sl->m_info.opacity = opacity; + sl->m_info.blend = blendmode; + m_sublayers.append(sl); + return sl; } -/** - * This line drawing function is optimized for drawing with no subpixel - * precision. - * The last point is not drawn, so successive lines can be drawn blotches. - */ -void Layer::drawHardLine(const Brush &brush, const Point& from, const Point& to, StrokeState &state) { - const qreal dp = (to.pressure()-from.pressure()) / hypot(to.x()-from.x(), to.y()-from.y()); - - int x0 = qFloor(from.x()); - int y0 = qFloor(from.y()); - qreal p = from.pressure(); - int x1 = qFloor(to.x()); - int y1 = qFloor(to.y()); - int dy = y1 - y0; - int dx = x1 - x0; - int stepx, stepy; - - if (dy < 0) { - dy = -dy; - stepy = -1; - } else { - stepy = 1; +void Layer::toDatastream(QDataStream &out) const +{ + // Write Layer metadata + out << qint32(id()); + out << quint32(width()) << quint32(height()); + out << m_info.title; + out << m_info.opacity; + out << quint8(m_info.blend); + out << m_info.hidden; + + // Write layer content + for(const Tile &t : m_tiles) + out << t; + + // Write sublayers + out << quint8(m_sublayers.size()); + for(const Layer *sl : m_sublayers) { + sl->toDatastream(out); } - if (dx < 0) { - dx = -dx; - stepx = -1; - } else { - stepx = 1; +} + +Layer *Layer::fromDatastream(QDataStream &in) +{ + // Read metadata + qint32 id; + in >> id; + + quint32 lw, lh; + in >> lw >> lh; + + QString title; + in >> title; + + uchar opacity; + uchar blend; + bool hidden; + in >> opacity >> blend >> hidden; + + // Read tiles + Layer *layer = new Layer(id, title, Qt::transparent, QSize(lw, lh)); + + for(Tile &t : layer->m_tiles) + in >> t; + + layer->m_info.opacity = opacity; + layer->m_info.blend = BlendMode::Mode(blend); + layer->m_info.hidden = hidden; + + // Read sublayers + quint8 sublayers; + in >> sublayers; + while(sublayers--) { + Layer *sl = Layer::fromDatastream(in); + if(!sl) { + delete layer; + return 0; + } + layer->m_sublayers.append(sl); } - dy *= 2; - dx *= 2; + return layer; +} + +void EditableLayer::resize(int top, int right, int bottom, int left) +{ + Q_ASSERT(d); + + // Minimize amount of data that needs to be copied + d->optimize(); + + // Resize sublayers + for(Layer *sl : d->m_sublayers) + EditableLayer(sl, nullptr).resize(top, right, bottom, left); - qreal distance = state.distance; + // Calculate new size + int width = left + d->m_width + right; + int height = top + d->m_height + bottom; - if (dx > dy) { - int fraction = dy - (dx >> 1); - while (x0 != x1) { - const qreal spacing = brush.spacingDist(p); - if (fraction >= 0) { - y0 += stepy; - fraction -= dx; + int xtiles = Tile::roundTiles(width); + int ytiles = Tile::roundTiles(height); + QVector tiles(xtiles * ytiles); + + // if there is no old content, resizing is simple + bool hascontent = false; + for(int i=0;im_tiles.size();++i) { + if(!d->m_tiles.at(i).isBlank()) { + hascontent = true; + break; + } + } + if(!hascontent) { + d->m_width = width; + d->m_height = height; + d->m_xtiles = xtiles; + d->m_ytiles = ytiles; + d->m_tiles = tiles; + return; + } + + // Sample colors around the layer edges to determine fill color + // for the new tiles + Tile bgtile; + { + QColor bgcolor = _sampleEdgeColors(d, top>0, right>0, bottom>0, left>0); + if(bgcolor.alpha()>0) + bgtile = Tile(bgcolor); + } + + if((left % Tile::SIZE) || (top % Tile::SIZE)) { + // If top/left adjustment is not divisble by tile size, + // we need to move the layer content + + QImage oldcontent = d->toImage(); + + d->m_width = width; + d->m_height = height; + d->m_xtiles = xtiles; + d->m_ytiles = ytiles; + d->m_tiles = tiles; + if(left<0 || top<0) { + int cropx = 0; + if(left<0) { + cropx = -left; + left = 0; } - x0 += stepx; - fraction += dy; - if(++distance >= spacing) { - directDab(brush, Point(x0, y0, p), state); - distance = 0; + int cropy = 0; + if(top<0) { + cropy = -top; + top = 0; } - p += dp; + oldcontent = oldcontent.copy(cropx, cropy, oldcontent.width()-cropx, oldcontent.height()-cropy); } + + d->m_tiles.fill(bgtile); + + putImage(left, top, oldcontent, BlendMode::MODE_REPLACE); + } else { - int fraction = dx - (dy >> 1); - while (y0 != y1) { - const qreal spacing = brush.spacingDist(p); - if (fraction >= 0) { - x0 += stepx; - fraction -= dy; - } - y0 += stepy; - fraction += dx; - if(++distance >= spacing) { - directDab(brush, Point(x0, y0, p), state); - distance = 0; + // top/left offset is aligned at tile boundary: + // existing tile content can be reused + + const int firstrow = Tile::roundTiles(-top); + const int firstcol = Tile::roundTiles(-left); + + int oldy = firstrow; + for(int y=0;ym_xtiles * oldy; + const int yy = xtiles * y; + for(int x=0;x=d->m_ytiles || oldx<0 || oldx>=d->m_xtiles) { + tiles[i] = bgtile; + + } else { + const int oldi = oldyy + oldx; + tiles[i] = d->m_tiles.at(oldi); + } } - p += dp; } + + d->m_width = width; + d->m_height = height; + d->m_xtiles = xtiles; + d->m_ytiles = ytiles; + d->m_tiles = tiles; } +} - state.distance = distance; +/** + * @param opacity + */ +void EditableLayer::setOpacity(int opacity) +{ + Q_ASSERT(d); + Q_ASSERT(opacity>=0 && opacity<256); + if(d->m_info.opacity != opacity) { + d->m_info.opacity = opacity; + markOpaqueDirty(true); + } +} + +void EditableLayer::setBlend(BlendMode::Mode blend) +{ + Q_ASSERT(d); + if(d->m_info.blend != blend) { + d->m_info.blend = blend; + markOpaqueDirty(); + } +} + +void EditableLayer::setCensored(bool censor) +{ + Q_ASSERT(d); + if(d->m_info.censored != censor) { + d->m_info.censored = censor; + markOpaqueDirty(true); + } } /** - * Apply a single dab of the brush to the layer - * @param brush brush to use - * @param point where to dab. May be outside the image. - * @param sampleSmudgeColor if true (and smudging is enabled for the brush), sample the layer color before applying dab - * @param state stroke state (used for the smudge color) + * @param hide new status */ -void Layer::directDab(const Brush &brush, const Point& point, StrokeState &state) +void EditableLayer::setHidden(bool hide) { - // Render the brush - const BrushStamp bs = makeGimpStyleBrushStamp(brush, point); - const int top=bs.top, left=bs.left; - const int dia = bs.mask.diameter(); - const int bottom = qMin(top + dia, m_height); - const int right = qMin(left + dia, m_width); + Q_ASSERT(d); + if(d->m_info.hidden != hide) { + d->m_info.hidden = hide; + markOpaqueDirty(true); + } +} - if(left+dia<=0 || top+dia<=0 || left>=m_width || top>=m_height) +/** + * @param x x coordinate + * @param y y coordinate + * @param image the image to draw + * @param mode blending/compositing mode (see protocol::PutImage) + */ +void EditableLayer::putImage(int x, int y, QImage image, BlendMode::Mode mode) +{ + Q_ASSERT(d); + Q_ASSERT(image.format() == QImage::Format_ARGB32_Premultiplied); + + // Check if the image is completely outside the layer + if(x >= d->m_width || y >= d->m_height || x+image.width() < 0 || y+image.height() < 0) return; - const qreal smudge = brush.smudge(point.pressure()); + // Crop image if x or y are negative + if(x<0 || y<0) { + const int xCrop = x<0 ? -x : 0; + const int yCrop = y<0 ? -y : 0; + + image = image.copy(xCrop, yCrop, image.width()-xCrop, image.height()-yCrop); + x = qMax(0, x); + y = qMax(0, y); + } - if(++state.smudgeDistance > brush.resmudge() && smudge>0) { - const QColor sampled = getDabColor(bs); + const int x0 = Tile::roundDown(x); + const int y0 = Tile::roundDown(y); + + Layer imageLayer = d->padImageToTileBoundary(x, y, image, mode); - const qreal a = sampled.alphaF() * smudge; + // Replace this layer's tiles with the scratch tiles + const int tx0 = x0 / Tile::SIZE; + const int ty0 = y0 / Tile::SIZE; + const int tx1 = qMin((x0 + imageLayer.width() - 1) / Tile::SIZE, d->m_xtiles-1); + const int ty1 = qMin((y0 + imageLayer.height() - 1) / Tile::SIZE, d->m_ytiles-1); - state.smudgeColor = QColor::fromRgbF( - state.smudgeColor.redF() * (1-a) + sampled.redF() * a, - state.smudgeColor.greenF() * (1-a) + sampled.greenF() * a, - state.smudgeColor.blueF() * (1-a) + sampled.blueF() * a - ); - state.smudgeDistance = 0; + for(int ty=ty0;ty<=ty1;++ty) { + for(int tx=tx0;tx<=tx1;++tx) { + d->rtile(tx, ty) = imageLayer.tile(tx-tx0, ty-ty0); + } } + + if(owner && d->isVisible()) + owner->markDirty(QRect(x, y, image.width(), image.height())); +} + +void EditableLayer::putTile(int col, int row, int repeat, const Tile &tile, int sublayer) +{ + Q_ASSERT(d); + if(col<0 || col >= d->m_xtiles || row<0 || row >= d->m_ytiles) + return; + + if(sublayer != 0) { + // LayerAttributes command can be used to set the sublayer's blendmode + // and opacity before calling putTile, if necessary. + getEditableSubLayer(sublayer, BlendMode::MODE_NORMAL, 255).putTile(col, row, repeat, tile, 0); + return; + } + + int i=row*d->m_xtiles+col; + const int end = qMin(i+repeat, d->m_tiles.size()-1); + for(;i<=end;++i) { + d->m_tiles[i] = tile; + if(owner && d->isVisible()) + owner->markDirty(i); + } +} + +void EditableLayer::fillRect(const QRect &rectangle, const QColor &color, BlendMode::Mode blendmode) +{ + const QRect canvas(0, 0, d->m_width, d->m_height); + + if(rectangle.contains(canvas) && blendmode==BlendMode::MODE_REPLACE) { + // Special case: overwrite whole layer + d->m_tiles.fill(Tile(color)); + + } else { + // The usual case: only a portion of the layer is filled or pixel blending is needed + QRect rect = rectangle.intersected(canvas); + + uchar mask[Tile::LENGTH]; + if(blendmode==255) + memset(mask, 0xff, Tile::LENGTH); + else + memset(mask, color.alpha(), Tile::LENGTH); + + const int size = Tile::SIZE; + const int bottom = rect.y() + rect.height(); + const int right = rect.x() + rect.width(); + + const int tx0 = rect.x() / size; + const int tx1 = (right-1) / size; + const int ty0 = rect.y() / size; + const int ty1 = (bottom-1) / size; + + bool canIncrOpacity; + if(blendmode==255 && color.alpha()==0) + canIncrOpacity = false; + else + canIncrOpacity = findBlendMode(blendmode).flags.testFlag(BlendMode::IncrOpacity); + + for(int ty=ty0;ty<=ty1;++ty) { + for(int tx=tx0;tx<=tx1;++tx) { + int left = qMax(tx * size, rect.x()) - tx*size; + int top = qMax(ty * size, rect.y()) - ty*size; + int w = qMin((tx+1)*size, right) - tx*size - left; + int h = qMin((ty+1)*size, bottom) - ty*size - top; + + Tile &t = d->m_tiles[ty*d->m_xtiles+tx]; + + if(!t.isNull() || canIncrOpacity) + t.composite(blendmode, mask, color, left, top, w, h, 0); + } + } + } + + if(owner && d->isVisible()) + owner->markDirty(rectangle); +} + +void EditableLayer::putBrushStamp(const BrushStamp &bs, const QColor &color, BlendMode::Mode blendmode) +{ + Q_ASSERT(d); + const int top=bs.top, left=bs.left; + const int dia = bs.mask.diameter(); + const int bottom = qMin(top + dia, d->m_height); + const int right = qMin(left + dia, d->m_width); + + if(left+dia<=0 || top+dia<=0 || left>=d->m_width || top>=d->m_height) + return; // Composite the brush mask onto the layer const uchar *values = bs.mask.data(); - QColor color = smudge > 0 ? state.smudgeColor : brush.color(); // A single dab can (and often does) span multiple tiles. int y = top<0?0:top; @@ -791,9 +802,9 @@ void Layer::directDab(const Brush &brush, const Point& point, StrokeState &state const int xindex = x / Tile::SIZE; const int xt = x - xindex * Tile::SIZE; const int wb = xt+dia-xb < Tile::SIZE ? dia-xb : Tile::SIZE-xt; - const int i = m_xtiles * yindex + xindex; - m_tiles[i].composite( - brush.blendingMode(), + const int i = d->m_xtiles * yindex + xindex; + d->m_tiles[i].composite( + blendmode, values + yb * dia + xb, color, xt, yt, @@ -808,216 +819,79 @@ void Layer::directDab(const Brush &brush, const Point& point, StrokeState &state yb = yb + hb; } - if(m_owner && isVisible()) - m_owner->markDirty(QRect(left, top, right-left, bottom-top)); - -} - -/** - * @brief Get a weighted average of the layer's color, using the given brush mask as the weight - * @param stamp - * @return color average - */ -QColor Layer::getDabColor(const BrushStamp &stamp) const -{ - // This is very much like directDab, instead we only read pixel values - const uchar *weights = stamp.mask.data(); - - // The mask can overlap multiple tiles - const int dia = stamp.mask.diameter(); - const int bottom = qMin(stamp.top + dia, m_height); - const int right = qMin(stamp.left + dia, m_width); - - int y = qMax(0, stamp.top); - int yb = stamp.top<0?-stamp.top:0; // y in relation to brush origin - const int x0 = qMax(0, stamp.left); - const int xb0 = stamp.left<0?-stamp.left:0; - - qreal weight=0, red=0, green=0, blue=0, alpha=0; - - // collect weighted color sums - while(y avg = m_tiles.at(i).weightedAverage(weights + yb * dia + xb, xt, yt, wb, hb, dia-wb); - weight += avg[0]; - red += avg[1]; - green += avg[2]; - blue += avg[3]; - alpha += avg[4]; - - x = (xindex+1) * Tile::SIZE; - xb = xb + wb; - } - y = (yindex+1) * Tile::SIZE; - yb = yb + hb; - } - - // Calculate final average - red /= weight; - green /= weight; - blue /= weight; - alpha /= weight; - - // Unpremultiply - if(alpha>0) { - red = qMin(1.0, red/alpha); - green = qMin(1.0, green/alpha); - blue = qMin(1.0, blue/alpha); - } - - return QColor::fromRgbF(red, green, blue, alpha); + if(owner && d->isVisible()) + owner->markDirty(QRect(left, top, right-left, bottom-top)); } /** - * @param layer the layer that will be merged to this - * @param sublayers merge sublayers as well + * @brief Merge another layer to this layer + * + * Both layer must be the same size + * + * @param layer the source layer */ -void Layer::merge(const Layer *layer, bool sublayers) +void EditableLayer::merge(const Layer *layer) { - Q_ASSERT(layer->m_xtiles == m_xtiles); - Q_ASSERT(layer->m_ytiles == m_ytiles); + Q_ASSERT(d); + Q_ASSERT(layer); + Q_ASSERT(layer->m_xtiles == d->m_xtiles); + Q_ASSERT(layer->m_ytiles == d->m_ytiles); - // Gather a list of non-null tiles to merge + // Gather a list of non-null source tiles to merge QList mergeidx; - mergeidx.reserve(m_tiles.size()); - for(int i=0;im_tiles[i].isNull(); - - if(isnull && sublayers) { - for(Layer *sl : m_sublayers) { - if(sl->m_tiles[i].isNull()) { - isnull = false; - break; - } - } - } - - if(!isnull) + for(int i=0;im_tiles.size();++i) { + if(!layer->m_tiles.at(i).isNull()) mergeidx.append(i); } // Detach tile vector explicitly to make sure concurrent modifications // are all done to the same vector - m_tiles.detach(); + d->m_tiles.detach(); // Merge tiles - concurrentForEach(mergeidx, [this, layer, sublayers](int idx) { - if(sublayers) { - Tile t = layer->m_tiles.at(idx); - - for(const Layer *sl : layer->m_sublayers) { - if(sl->isVisible()) { - t.merge(sl->m_tiles.at(idx), sl->opacity(), sl->blendmode()); - } - } - m_tiles[idx].merge(t, layer->opacity(), layer->blendmode()); - - } else { - m_tiles[idx].merge(layer->m_tiles.at(idx), layer->opacity(), layer->blendmode()); - } + concurrentForEach(mergeidx, [this, layer](int idx) { + d->m_tiles[idx].merge(layer->m_tiles.at(idx), layer->opacity(), layer->blendmode()); }); // Merging a layer does not cause an immediate visual change, so we don't // mark the area as dirty here. } -/** - * Free all tiles that are completely transparent - */ -void Layer::optimize() -{ - // Optimize tile memory usage - for(int i=0;i li(m_sublayers); - while(li.hasNext()) { - Layer *sl = li.next(); - if(sl->isHidden()) { - delete sl; - li.remove(); - } - } -} - -void Layer::makeBlank() +void EditableLayer::makeBlank() { - m_tiles.fill(Tile()); + Q_ASSERT(d); + d->m_tiles.fill(Tile()); - if(m_owner && isVisible()) - m_owner->markDirty(); + if(owner && d->isVisible()) + owner->markDirty(); } /** - * @brief Get or create a new sublayer - * - * Sublayers are temporary layers used for indirect drawing. - * - * Negative IDs are used for ephemeral preview layers. - * - * @param id sublayer ID (unique to parent layer only) - * @param opacity layer opacity (set when creating the layer) - * @return sublayer + * This is used to end an indirect stroke. + * If a sublayer with the given ID does not exist, this function does nothing. + * @param id */ -Layer *Layer::getSubLayer(int id, BlendMode::Mode blendmode, uchar opacity) +void EditableLayer::mergeSublayer(int id) { - // See if the sublayer exists already - for(Layer *sl : m_sublayers) + Q_ASSERT(d); + for(Layer *sl : d->m_sublayers) { if(sl->id() == id) { - if(sl->isHidden()) { - // Hidden, reset properties - sl->makeBlank(); - sl->m_info.opacity = opacity; - sl->m_info.blend = blendmode; - sl->m_info.hidden = false; + if(!sl->isHidden()) { + merge(sl); + // Set hidden flag directly to avoid markDirty call. + // The merge should cause no visual change. + sl->m_info.hidden = true; } - return sl; - } - - // Okay, try recycling a sublayer - for(Layer *sl : m_sublayers) { - if(sl->isHidden()) { - // Set these flags directly to avoid markDirty call. - // We know the layer is invisible at this point - sl->makeBlank(); - sl->m_info.id = id; - sl->m_info.opacity = opacity; - sl->m_info.blend = blendmode; - sl->m_info.hidden = false; - return sl; + return; } } - - // No available sublayers, create a new one - Layer *sl = new Layer(m_owner, id, QSize(m_width, m_height)); - sl->m_info.opacity = opacity; - sl->m_info.blend = blendmode; - m_sublayers.append(sl); - return sl; } -/** - * This is used to end an indirect stroke. - * If a sublayer with the given ID does not exist, this function does nothing. - * @param id - */ -void Layer::mergeSublayer(int id) +void EditableLayer::mergeAllSublayers() { - for(Layer *sl : m_sublayers) { - if(sl->id() == id) { + Q_ASSERT(d); + for(Layer *sl : d->m_sublayers) { + if(sl->id() > 0) { if(!sl->isHidden()) { merge(sl); // Set hidden flag directly to avoid markDirty call. @@ -1036,115 +910,36 @@ void Layer::mergeSublayer(int id) * Call optimize() to clean up removed sublayers. * @param id */ -void Layer::removeSublayer(int id) +void EditableLayer::removeSublayer(int id) { - for(Layer *sl : m_sublayers) { + Q_ASSERT(d); + for(Layer *sl : d->m_sublayers) { if(sl->id() == id) { - sl->setHidden(true); + EditableLayer(sl, owner).setHidden(true); return; } } } -void Layer::removePreviews() +void EditableLayer::removePreviews() { - for(Layer *sl : m_sublayers) { + Q_ASSERT(d); + for(Layer *sl : d->m_sublayers) { if(sl->id() < 0) - sl->setHidden(true); + EditableLayer(sl, owner).setHidden(true); } } -void Layer::markOpaqueDirty(bool forceVisible) +void EditableLayer::markOpaqueDirty(bool forceVisible) { - if(!m_owner || !(forceVisible || isVisible())) + if(!owner || !(forceVisible || d->isVisible())) return; - for(int i=0;imarkDirty(i); - } - m_owner->notifyAreaChanged(); -} - -QColor Layer::isSolidColor() const -{ - if(m_width==0 || m_height==0) - return QColor(); - - const QColor c = m_tiles.at(0).solidColor(); - if(!c.isValid()) - return QColor(); - - for(int i=1;im_tiles.size();++i) { + if(!d->m_tiles.at(i).isNull()) + owner->markDirty(i); } - - return c; } -void Layer::toDatastream(QDataStream &out) const -{ - // Write Layer metadata - out << qint32(id()); - out << quint32(width()) << quint32(height()); - out << m_info.title; - out << m_info.opacity; - out << quint8(m_info.blend); - out << m_info.hidden; - - // Write layer content - for(const Tile &t : m_tiles) - out << t; - - // Write sublayers - out << quint8(m_sublayers.size()); - for(const Layer *sl : m_sublayers) { - sl->toDatastream(out); - } } -Layer *Layer::fromDatastream(LayerStack *owner, QDataStream &in) -{ - // Read metadata - qint32 id; - in >> id; - - quint32 lw, lh; - in >> lw >> lh; - - QString title; - in >> title; - - uchar opacity; - uchar blend; - bool hidden; - in >> opacity >> blend >> hidden; - - // Read tiles - Layer *layer = new Layer(owner, id, title, Qt::transparent, QSize(lw, lh)); - - for(Tile &t : layer->m_tiles) - in >> t; - - layer->m_info.opacity = opacity; - layer->m_info.blend = BlendMode::Mode(blend); - layer->m_info.hidden = hidden; - - // Read sublayers - quint8 sublayers; - in >> sublayers; - while(sublayers--) { - Layer *sl = Layer::fromDatastream(owner, in); - if(!sl) { - delete layer; - return 0; - } - layer->m_sublayers.append(sl); - } - - return layer; -} - -} diff --git a/src/client/core/layer.h b/src/client/core/layer.h index 7c6552fb7..18b846157 100644 --- a/src/client/core/layer.h +++ b/src/client/core/layer.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2008-2017 Calle Laakkonen + Copyright (C) 2008-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -16,13 +16,15 @@ You should have received a copy of the GNU General Public License along with Drawpile. If not, see . */ -#ifndef LAYER_H -#define LAYER_H - -#include +#ifndef PAINTCORE_LAYER_H +#define PAINTCORE_LAYER_H #include "tile.h" +#include +#include +#include + class QImage; class QSize; class QDataStream; @@ -40,12 +42,15 @@ struct StrokeState; */ struct LayerInfo { // Identification + // Note: In the protocol, layer IDs are 16bit, but for internal (ephemeral) layers, + // we use IDs outside that range. int id; QString title; // Rendering controls uchar opacity; bool hidden; + bool censored; BlendMode::Mode blend; }; @@ -55,195 +60,266 @@ struct LayerInfo { * A layer is made up of multiple tiles. * Although images of arbitrary size can be created, the true layer size is * always a multiple of Tile::SIZE. + * + * Layer editing functions are provided via the EditableLayer wrapper class. + * However, you should typically not instantiate this class yourself. Instead, + * use the LayerStackWriteSequence class. + * + * Exceptions: when you're dealing with free layers (not part of any LayerStack) */ class Layer { - public: - //! Construct a layer filled with solid color - Layer(LayerStack *owner, int id, const QString& title, const QColor& color, const QSize& size); - - //! Construct a copy of this layer - Layer(const Layer &layer, LayerStack *newOwner=nullptr); - - ~Layer(); - - //! Get the layer width in pixels - int width() const { return m_width; } - - //! Get the layer height in pixels - int height() const { return m_height; } - - //! Get the layer ID - int id() const { return m_info.id; } - - //! Change layer ID - void setId(int id) { m_info.id = id; } - - //! Get the layer name - const QString& title() const { return m_info.title; } - - //! Set the layer name - void setTitle(const QString& title); - - //! Get the layer as an image - QImage toImage() const; - - //! Get the layer as an image with excess transparency cropped away - QImage toCroppedImage(int *xOffset, int *yOffset) const; - - //! Adjust layer size - void resize(int top, int right, int bottom, int left); - - //! Get the color at the specified coordinates - QColor colorAt(int x, int y, int dia=0) const; - - //! Get the raw pixel value at the specified coordinates - QRgb pixelAt(int x, int y) const; - - //! Get layer opacity - int opacity() const { return m_info.opacity; } + friend class EditableLayer; +public: + //! Construct a layer filled with solid color + Layer(int id, const QString& title, const QColor& color, const QSize& size); + + //! Construct a copy of this layer + Layer(const Layer &layer); + ~Layer(); + + //! Get the layer width in pixels + int width() const { return m_width; } + + //! Get the layer height in pixels + int height() const { return m_height; } + + //! Get the layer ID + int id() const { return m_info.id; } + + //! Get the layer name + const QString& title() const { return m_info.title; } + + //! Get the layer as an image + QImage toImage() const; + + //! Get the layer as an image with excess transparency cropped away + QImage toCroppedImage(int *xOffset, int *yOffset) const; + + //! Get the color at the specified coordinates + QColor colorAt(int x, int y, int dia=0) const; + + //! Get the raw pixel value at the specified coordinates + QRgb pixelAt(int x, int y) const; + + //! Get layer opacity + int opacity() const { return m_info.opacity; } + + //! Get the effective layer opacity (0 if hidden) + int effectiveOpacity() const { return isHidden() ? 0 : m_info.opacity; } + + /** + * @brief Get the layer blending mode + * @return blending mode number + */ + BlendMode::Mode blendmode() const { return m_info.blend; } + + /** + * @brief Is this layer hidden? + * Hiding a layer is slightly different than setting its opacity + * to zero, although the end result is the same. The hidden status + * is purely local: setting it will not hide the layer for other + * users. + */ + bool isHidden() const { return m_info.hidden; } + + //! Is this layer flagged for censoring + bool isCensored() const { return m_info.censored; } + + /** + * @brief Get a sublayer + * + * Sublayers are used for indirect drawing and previews. + * Positive IDs should correspond to context IDs: they are used for indirect + * painting. + * Negative IDs are local preview layers. + * + * ID 0 should not be used. + * + * The blendmode and opacity are set only when the layer is created. + * + * @param id + * @param blendmode + * @param opacity + * @return + */ + Layer *getSubLayer(int id, BlendMode::Mode blendmode, uchar opacity); + + //! Get a tile + const Tile &tile(int x, int y) const { + Q_ASSERT(x>=0 && x=0 && y=0 && index &sublayers() const { return m_sublayers; } + + /** + * @brief Is this layer visible + * A layer is visible when its opacity is greater than zero AND + * it is not explicitly hidden. + * @return true if layer is visible + */ + bool isVisible() const { return m_info.opacity > 0 && !m_info.hidden; } + + /** + * @brief Get the non-pixeldata related properties + */ + const LayerInfo &info() const { return m_info; } + + // Disable assignment operator + Layer& operator=(const Layer&) = delete; + + void toDatastream(QDataStream &out) const; + static Layer *fromDatastream(QDataStream &in); + + //! Get this layer's tile vector + const QVector tiles() const { return m_tiles; } + + /** + * @brief Get the layer's change bounds + */ + QRect changeBounds() const { return m_changeBounds; } + + /** + * @brief Get the change bounds of a sublayer + * @param contextId sublayer ID + */ + QRect changeBounds(int contextId) const { + for(const Layer *l : m_sublayers) { + if(l->id() == contextId && l->isVisible()) + return l->changeBounds(); + } + return QRect(); + } - //! Get the effective layer opacity (0 if hidden) - int effectiveOpacity() const { return isHidden() ? 0 : m_info.opacity; } + //! Optimize layer memory usage + void optimize(); - //! Set layer opacity - void setOpacity(int opacity); +private: + //! Construct a sublayer + Layer(int id, const QSize& size); + Layer padImageToTileBoundary(int leftpad, int toppad, const QImage &original, BlendMode::Mode mode) const; + QColor getDabColor(const BrushStamp &stamp) const; - //! Set layer blending mode - void setBlend(BlendMode::Mode blend); + Tile &rtile(int x, int y) { + Q_ASSERT(x>=0 && x=0 && y - - - ColorButton - QToolButton -
    widgets/colorbutton.h
    -
    -
    diff --git a/src/desktop/utils/actionbuilder.h b/src/desktop/utils/actionbuilder.h new file mode 100644 index 000000000..8ad9d18a1 --- /dev/null +++ b/src/desktop/utils/actionbuilder.h @@ -0,0 +1,112 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#ifndef DP_ACTIONBUILDER_H +#define DP_ACTIONBUILDER_H + +#include +#include "utils/icon.h" +#include "utils/customshortcutmodel.h" + +/** + * @brief A helper class for configuring QActions + */ +class ActionBuilder +{ +public: + explicit ActionBuilder(QAction *action) : m_action(action) { Q_ASSERT(m_action); } + + operator QAction*() { + // If an action is tagged as "remembered", it should be checkable as well + Q_ASSERT(m_action->isCheckable() || !m_action->property("remembered").toBool()); + + return m_action; + } + + ActionBuilder &icon(const QString &name) + { + if(name.startsWith(QStringLiteral("builtin:"))) + m_action->setIcon(QIcon(name)); + else + m_action->setIcon(icon::fromTheme(name)); + return *this; + } + + ActionBuilder &shortcut(const QString &key) { return shortcut(QKeySequence(key)); } + + ActionBuilder &shortcut(const QKeySequence &shortcut) + { + Q_ASSERT(!m_action->objectName().isEmpty()); + m_action->setShortcut(shortcut); + CustomShortcutModel::registerCustomizableAction(m_action->objectName(), m_action->text().remove('&'), shortcut); + return *this; + } + + ActionBuilder &checkable() + { + m_action->setCheckable(true); + return *this; + } + + ActionBuilder &checked() + { + m_action->setCheckable(true); + m_action->setChecked(true); + m_action->setProperty("defaultValue", true); + return *this; + } + + ActionBuilder &statusTip(const QString &tip) + { + m_action->setStatusTip(tip); + return *this; + } + + ActionBuilder &disabled() + { + m_action->setEnabled(false); + return *this; + } + + ActionBuilder &menuRole(QAction::MenuRole role) + { + m_action->setMenuRole(role); + return *this; + } + + ActionBuilder &property(const char *name, const QVariant &value) + { + m_action->setProperty(name, value); + return *this; + } + + ActionBuilder &remembered() + { + // Tag this (checkable) action so that its state will be + // saved and loaded. + Q_ASSERT(!m_action->objectName().isEmpty()); + m_action->setProperty("remembered", true); + return *this; + } + +private: + QAction *m_action; +}; + +#endif // ACTIONBUILDER_H diff --git a/src/desktop/utils/mandatoryfields.cpp b/src/desktop/utils/mandatoryfields.cpp index 7995fdac7..66f6098b3 100644 --- a/src/desktop/utils/mandatoryfields.cpp +++ b/src/desktop/utils/mandatoryfields.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2007 Calle Laakkonen + Copyright (C) 2007-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,7 +18,6 @@ */ #include "mandatoryfields.h" -#include #include #include #include @@ -28,14 +27,15 @@ * When constructed, all mandatory fields are searched recursively from * the parent object. * @param parent parent object of the mandatory fields - * @param button button to disable when a field is blank + * @param okButton button to disable when a field is blank */ -MandatoryFields::MandatoryFields(QWidget *parent, QWidget *button) - : QObject(parent), button_(button) +MandatoryFields::MandatoryFields(QWidget *parent, QWidget *okButton) + : QObject(parent), m_okButton(okButton) { + Q_ASSERT(parent); + Q_ASSERT(okButton); collectFields(parent); - // Collect a list of mandatory input fields - changed(); + update(); } //! Recursively collect mandatory fields @@ -44,22 +44,25 @@ void MandatoryFields::collectFields(QObject *parent) for(QObject *obj : parent->children()) { if(obj->property("mandatoryfield").isValid()) { if(obj->inherits("QLineEdit")) { - connect(obj, SIGNAL(textChanged(QString)), this, SLOT(changed())); + connect(obj, SIGNAL(textChanged(QString)), this, SLOT(update())); } else if(obj->inherits("QComboBox")) { - connect(obj, SIGNAL(editTextChanged(QString)), this, SLOT(changed())); + connect(obj, SIGNAL(editTextChanged(QString)), this, SLOT(update())); } else { qWarning() << "unhandled mandatory field" << obj->metaObject()->className(); } - widgets_.append(obj); + m_widgets.append(obj); } collectFields(obj); } } -void MandatoryFields::changed() +void MandatoryFields::update() { bool enable = true; - for(QObject *obj : widgets_) { + for(QObject *obj : m_widgets) { + if(!obj->property("mandatoryfield").toBool()) + continue; + if(obj->inherits("QLineEdit")) { if(static_cast(obj)->text().isEmpty()) { enable = false; @@ -72,6 +75,6 @@ void MandatoryFields::changed() } } } - button_->setEnabled(enable); + m_okButton->setEnabled(enable); } diff --git a/src/desktop/utils/mandatoryfields.h b/src/desktop/utils/mandatoryfields.h index be10c443e..ed0e4a797 100644 --- a/src/desktop/utils/mandatoryfields.h +++ b/src/desktop/utils/mandatoryfields.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2007 Calle Laakkonen + Copyright (C) 2007-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -30,18 +30,18 @@ class QWidget; */ class MandatoryFields : public QObject { Q_OBJECT - public: - //! Construct the mandatory field monitor - MandatoryFields(QWidget *parent, QWidget *button); +public: + MandatoryFields(QWidget *parent, QWidget *okButton); - private slots: - void changed(); +public slots: + //! Check each registered widget and update the OK button state + void update(); - private: - void collectFields(QObject *parent); +private: + void collectFields(QObject *parent); - QList widgets_; - QWidget *button_; + QList m_widgets; + QWidget *m_okButton; }; #endif diff --git a/src/desktop/utils/netfiles.cpp b/src/desktop/utils/netfiles.cpp index 36dc16b5f..a1b0d401a 100644 --- a/src/desktop/utils/netfiles.cpp +++ b/src/desktop/utils/netfiles.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2014-2017 Calle Laakkonen + Copyright (C) 2014-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,10 +28,11 @@ #include #include #include +#include namespace networkaccess { -void getFile(const QUrl &url, const QString &expectType, widgets::NetStatus *netstatus, std::function callback) +void getFile(const QUrl &url, const QString &expectType, widgets::NetStatus *netstatus, const QObject *context, std::function callback) { QString fileExt = ".tmp"; QString filename = url.path(); @@ -39,10 +40,9 @@ void getFile(const QUrl &url, const QString &expectType, widgets::NetStatus *net if(fileExtIndex>0) fileExt = filename.mid(fileExtIndex); - QTemporaryFile *tempfile = new QTemporaryFile(QDir::tempPath() + QStringLiteral("/drawpile_XXXXXX-download") + fileExt); + QSharedPointer tempfile { new QTemporaryFile(QDir::tempPath() + QStringLiteral("/drawpile_XXXXXX-download") + fileExt) }; if(!tempfile->open()) { callback(*tempfile, tempfile->errorString()); - delete tempfile; return; } @@ -57,7 +57,7 @@ void getFile(const QUrl &url, const QString &expectType, widgets::NetStatus *net tempfile->write(reply->readAll()); }); - reply->connect(reply, &QNetworkReply::finished, [reply, expectType, callback, tempfile]() { + reply->connect(reply, &QNetworkReply::finished, context, [reply, expectType, callback, tempfile]() { QString errormsg; if(reply->error()) { @@ -73,15 +73,14 @@ void getFile(const QUrl &url, const QString &expectType, widgets::NetStatus *net tempfile->seek(0); callback(*tempfile, errormsg); - delete tempfile; reply->deleteLater(); }); } -void getImage(const QUrl &url, widgets::NetStatus *netstatus, std::function callback) +void getImage(const QUrl &url, widgets::NetStatus *netstatus, const QObject *context, std::function callback) { - getFile(url, "image/", netstatus, [callback](QFile &file, const QString &error) { + getFile(url, "image/", netstatus, context, [callback](QFile &file, const QString &error) { if(!error.isEmpty()) { callback(QImage(), error); } else { diff --git a/src/desktop/utils/netfiles.h b/src/desktop/utils/netfiles.h index 712b496e2..88480cbc2 100644 --- a/src/desktop/utils/netfiles.h +++ b/src/desktop/utils/netfiles.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2014-2017 Calle Laakkonen + Copyright (C) 2014-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -28,6 +28,7 @@ class QString; class QUrl; class QImage; class QFile; +class QObject; namespace widgets { class NetStatus; @@ -42,9 +43,10 @@ namespace networkaccess { * * @param url * @param expectType + * @param context object which must exist for callback to be called * @param callback */ -void getFile(const QUrl &url, const QString &expectType, widgets::NetStatus *netstatus, std::function callback); +void getFile(const QUrl &url, const QString &expectType, widgets::NetStatus *netstatus, const QObject *context, std::function callback); /** * @brief A convenience wrapepr aaround get() that expects an image in response @@ -53,9 +55,10 @@ void getFile(const QUrl &url, const QString &expectType, widgets::NetStatus *net * * @param url the URL to fetch * @param netstatus the status widget whose progress meter to update + * @param context object which must exist for callback to be called * @param callback the callback to call with the returned image or error message */ -void getImage(const QUrl &url, widgets::NetStatus *netstatus, std::function callback); +void getImage(const QUrl &url, widgets::NetStatus *netstatus, const QObject *context, std::function callback); } diff --git a/src/desktop/widgets/brushpreview.cpp b/src/desktop/widgets/brushpreview.cpp index 702dd6614..045faeacd 100644 --- a/src/desktop/widgets/brushpreview.cpp +++ b/src/desktop/widgets/brushpreview.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2006-2016 Calle Laakkonen + Copyright (C) 2006-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -16,14 +16,18 @@ You should have received a copy of the GNU General Public License along with Drawpile. If not, see . */ -#include +#include "brushpreview.h" + +#ifndef DESIGNER_PLUGIN #include "core/point.h" #include "core/layerstack.h" #include "core/layer.h" -#include "core/shapes.h" #include "core/floodfill.h" -#include "brushpreview.h" +#include "brushes/shapes.h" +#include "brushes/brushengine.h" +#include "brushes/brushpainter.h" +#endif #include #include @@ -50,7 +54,9 @@ BrushPreview::BrushPreview(QWidget *parent, Qt::WindowFlags f) } BrushPreview::~BrushPreview() { +#ifndef DESIGNER_PLUGIN delete m_preview; +#endif } void BrushPreview::notifyBrushChange() @@ -126,6 +132,7 @@ void BrushPreview::changeEvent(QEvent *event) void BrushPreview::paintEvent(QPaintEvent *event) { +#ifndef DESIGNER_PLUGIN if(m_needupdate) updatePreview(); @@ -136,17 +143,24 @@ void BrushPreview::paintEvent(QPaintEvent *event) QPainter painter(this); painter.drawPixmap(event->rect(), m_previewCache, event->rect()); +#endif } void BrushPreview::updatePreview() { - if(!m_preview) { +#ifndef DESIGNER_PLUGIN + if(!m_preview) m_preview = new paintcore::LayerStack; - QSize size = contentsRect().size(); - m_preview->resize(0, size.width(), size.height(), 0); - m_preview->createLayer(0, 0, QColor(0,0,0), false, false, QString()); + + auto layerstack = m_preview->editor(); + + if(m_preview->width() == 0) { + const QSize size = contentsRect().size(); + layerstack.resize(0, size.width(), size.height(), 0); + layerstack.createLayer(0, 0, QColor(0,0,0), false, false, QString()); + } else if(m_preview->width() != contentsRect().width() || m_preview->height() != contentsRect().height()) { - m_preview->resize(0, contentsRect().width() - m_preview->width(), contentsRect().height() - m_preview->height(), 0); + layerstack.resize(0, contentsRect().width() - m_preview->width(), contentsRect().height() - m_preview->height(), 0); } QRectF previewRect( @@ -158,28 +172,28 @@ void BrushPreview::updatePreview() paintcore::PointVector pointvector; switch(_shape) { - case Stroke: pointvector = paintcore::shapes::sampleStroke(previewRect); break; + case Stroke: pointvector = brushes::shapes::sampleStroke(previewRect); break; case Line: pointvector << paintcore::Point(previewRect.left(), previewRect.top(), 1.0) << paintcore::Point(previewRect.right(), previewRect.bottom(), 1.0); break; - case Rectangle: pointvector = paintcore::shapes::rectangle(previewRect); break; - case Ellipse: pointvector = paintcore::shapes::ellipse(previewRect); break; + case Rectangle: pointvector = brushes::shapes::rectangle(previewRect); break; + case Ellipse: pointvector = brushes::shapes::ellipse(previewRect); break; case FloodFill: - case FloodErase: pointvector = paintcore::shapes::sampleBlob(previewRect); break; + case FloodErase: pointvector = brushes::shapes::sampleBlob(previewRect); break; } QColor bgcolor = m_bg; - paintcore::Brush brush = m_brush; + brushes::ClassicBrush brush = m_brush; // Special handling for some blending modes // TODO this could be implemented in some less ad-hoc way - if(brush.blendingMode() == 11) { + if(brush.blendingMode() == paintcore::BlendMode::MODE_BEHIND) { // "behind" mode needs a transparent layer for anything to show up brush.setBlendingMode(paintcore::BlendMode::MODE_NORMAL); - } else if(brush.blendingMode() == 12) { + } else if(brush.blendingMode() == paintcore::BlendMode::MODE_COLORERASE) { // Color-erase mode: use fg color as background bgcolor = m_color; } @@ -188,30 +202,38 @@ void BrushPreview::updatePreview() brush.setColor(bgcolor); } - paintcore::Layer *layer = m_preview->getLayerByIndex(0); - layer->fillRect(QRect(0, 0, layer->width(), layer->height()), isTransparentBackground() ? QColor(Qt::transparent) : bgcolor, paintcore::BlendMode::MODE_REPLACE); + auto layer = layerstack.getEditableLayerByIndex(0); + layer.putTile(0, 0, 99999, isTransparentBackground() ? paintcore::Tile() : paintcore::Tile(bgcolor)); + + brushes::BrushEngine brushengine; + brushengine.setBrush(1, 1, brush); - paintcore::StrokeState ss(brush); - for(int i=1;idrawLine(0, brush, pointvector[i-1], pointvector[i], ss); + for(int i=0;imergeSublayer(0); + const auto dabs = brushengine.takeDabs(); + for(int i=0;i0) fr = paintcore::expandFill(fr, _fillExpansion, m_color); if(!fr.image.isNull()) - layer->putImage(fr.x, fr.y, fr.image, _shape == FloodFill ? (_underFill ? paintcore::BlendMode::MODE_BEHIND : paintcore::BlendMode::MODE_NORMAL) : paintcore::BlendMode::MODE_ERASE); + layer.putImage(fr.x, fr.y, fr.image, _shape == FloodFill ? (_underFill ? paintcore::BlendMode::MODE_BEHIND : paintcore::BlendMode::MODE_NORMAL) : paintcore::BlendMode::MODE_ERASE); } m_needupdate=false; +#endif } /** * @param brush brush to set */ -void BrushPreview::setBrush(const paintcore::Brush& brush) +void BrushPreview::setBrush(const brushes::ClassicBrush& brush) { m_brush = brush; notifyBrushChange(); diff --git a/src/desktop/widgets/brushpreview.h b/src/desktop/widgets/brushpreview.h index fdede6e7a..ef0d62bff 100644 --- a/src/desktop/widgets/brushpreview.h +++ b/src/desktop/widgets/brushpreview.h @@ -21,7 +21,7 @@ #include -#include "core/brush.h" +#include "brushes/brush.h" #include "core/blendmodes.h" class QMenu; @@ -60,7 +60,7 @@ class PLUGIN_EXPORT BrushPreview : public QFrame { PreviewShape previewShape() const { return _shape; } //! Get the displayed brush - const paintcore::Brush &brush() const { return m_brush; } + const brushes::ClassicBrush &brush() const { return m_brush; } bool isTransparentBackground() const { return _tranparentbg; } @@ -69,7 +69,7 @@ class PLUGIN_EXPORT BrushPreview : public QFrame { /** * @param brush brush to set */ - void setBrush(const paintcore::Brush& brush); + void setBrush(const brushes::ClassicBrush& brush); //! Set preview brush size void setSize(int size); @@ -130,7 +130,7 @@ class PLUGIN_EXPORT BrushPreview : public QFrame { signals: void requestColorChange(); - void brushChanged(const paintcore::Brush&); + void brushChanged(const brushes::ClassicBrush&); protected: void paintEvent(QPaintEvent *event); @@ -144,7 +144,7 @@ class PLUGIN_EXPORT BrushPreview : public QFrame { void updatePreview(); void updateBackground(); - paintcore::Brush m_brush; + brushes::ClassicBrush m_brush; paintcore::LayerStack *m_preview; QPixmap m_previewCache; diff --git a/src/desktop/widgets/chatwidget.cpp b/src/desktop/widgets/chatwidget.cpp index dc0500a10..a7005a3ac 100644 --- a/src/desktop/widgets/chatwidget.cpp +++ b/src/desktop/widgets/chatwidget.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2007-2018 Calle Laakkonen + Copyright (C) 2007-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -24,6 +24,7 @@ #include "notifications.h" #include "../shared/net/meta.h" +#include "canvas/userlist.h" #include #include @@ -31,203 +32,550 @@ #include #include #include +#include +#include +#include +#include namespace widgets { +struct Chat { + QTextDocument *doc; + int lastAppendedId = 0; + qint64 lastMessageTs = 0; + int scrollPosition = 0; + + Chat() : doc(nullptr) { } + explicit Chat(QObject *parent) + : doc(new QTextDocument(parent)) + { + doc->setDefaultStyleSheet( + ".sep { background: #4d4d4d }" + ".notification { background: #232629 }" + ".message, .notification {" + "color: #eff0f1;" + "margin: 6px 0 6px 0" + "}" + ".shout { background: #34292c }" + ".shout .tab { background: #da4453 }" + ".action { font-style: italic }" + ".username { font-weight: bold }" + ".trusted { color: #27ae60 }" + ".registered { color: #16a085 }" + ".op { color: #f47750 }" + ".mod { color: #ed1515 }" + ".timestamp { color #4d4d4d }" + "a:link { color: #1d99f3 }" + ); + } + + void appendSeparator(QTextCursor &cursor); + void appendMessage(int userId, const QString &usernameSpan, const QString &message, bool shout); + void appendAction(const QString &usernameSpan, const QString &message); + void appendNotification(const QString &message); +}; + +struct ChatBox::Private { + Private(ChatBox *parent) : chatbox(parent) { } + + ChatBox * const chatbox; + QTextBrowser *view = nullptr; + ChatLineEdit *myline = nullptr; + QLabel *pinned = nullptr; + QTabBar *tabs = nullptr; + + QList announcedUsers; + canvas::UserListModel *userlist = nullptr; + QHash chats; + + int myId = 0; + int currentChat = 0; + + bool wasCollapsed = false; + bool preserveChat = true; + + QString usernameSpan(int userId); + + void scrollToEnd(int ifCurrentId) { + if(ifCurrentId == tabs->tabData(tabs->currentIndex()).toInt()) + view->verticalScrollBar()->setValue(view->verticalScrollBar()->maximum()); + } + + inline Chat &publicChat() + { + Q_ASSERT(chats.contains(0)); + return chats[0]; + } + + bool ensurePrivateChatExists(int userId, QObject *parent); + + void updatePreserveModeUi(); +}; + ChatBox::ChatBox(QWidget *parent) - : QWidget(parent), - m_wasCollapsed(false), - m_preserveChat(false), - m_myId(1) + : QWidget(parent), d(new Private(this)) { QVBoxLayout *layout = new QVBoxLayout(this); layout->setSpacing(0); layout->setMargin(0); - m_pinned = new QLabel(this); - m_pinned->setVisible(false); - m_pinned->setOpenExternalLinks(true); - m_pinned->setStyleSheet(QStringLiteral( - "background: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 #8d8d8d, stop:1 #31363b);" + d->tabs = new QTabBar(this); + d->tabs->addTab(QString()); + d->tabs->setTabIcon(0, QIcon("builtin:chat.svg")); + d->tabs->setAutoHide(true); + d->tabs->setDocumentMode(true); + d->tabs->setTabsClosable(true); + d->tabs->setMovable(true); + d->tabs->setTabData(0, 0); // context id 0 is used for the public chat + + // The public chat cannot be closed + if(d->tabs->tabButton(0, QTabBar::LeftSide)) { + d->tabs->tabButton(0, QTabBar::LeftSide)->deleteLater(); + d->tabs->setTabButton(0, QTabBar::LeftSide, nullptr); + } + if(d->tabs->tabButton(0, QTabBar::RightSide)) { + d->tabs->tabButton(0, QTabBar::RightSide)->deleteLater(); + d->tabs->setTabButton(0, QTabBar::RightSide, nullptr); + } + + connect(d->tabs, &QTabBar::currentChanged, this, &ChatBox::chatTabSelected); + connect(d->tabs, &QTabBar::tabCloseRequested, this, &ChatBox::chatTabClosed); + layout->addWidget(d->tabs, 0); + + d->pinned = new QLabel(this); + d->pinned->setVisible(false); + d->pinned->setOpenExternalLinks(true); + d->pinned->setStyleSheet(QStringLiteral( + "background: #232629;" + "border-bottom: 1px solid #2980b9;" "color: #eff0f1;" "padding: 3px;" )); - layout->addWidget(m_pinned, 0); + layout->addWidget(d->pinned, 0); - m_view = new QTextBrowser(this); - m_view->setOpenExternalLinks(true); + d->view = new QTextBrowser(this); + d->view->setOpenExternalLinks(true); - layout->addWidget(m_view, 1); + layout->addWidget(d->view, 1); - m_myline = new ChatLineEdit(this); - layout->addWidget(m_myline); + d->myline = new ChatLineEdit(this); + layout->addWidget(d->myline); setLayout(layout); - connect(m_myline, &ChatLineEdit::returnPressed, this, &ChatBox::sendMessage); - - m_view->document()->setDefaultStyleSheet( - "p { margin: 5px 0; }" - ".ts { color: #95a5a6 }" - ".marker { color: #da4453 }" - ".sysmsg { color: #fdbc4b }" - ".announcement { color: #fcfcfc }" - ".nick { font-weight: bold }" - ".nick.me { color: #fcfcfc }" - ".log { color: #aaaaaa; font-style: italic }" - ".action { color: #fcfcfc }" - "a:link { color: #1d99f3 }" - ); + connect(d->myline, &ChatLineEdit::returnPressed, this, &ChatBox::sendMessage); + + d->chats[0] = Chat(this); + d->view->setDocument(d->chats[0].doc); setPreserveMode(false); } -void ChatBox::setPreserveMode(bool preservechat) +void ChatBox::Private::updatePreserveModeUi() { - QString placeholder, color; - - m_preserveChat = preservechat; + const bool preserve = preserveChat && currentChat == 0; - if(preservechat) { + QString placeholder, color; + if(preserve) { placeholder = tr("Chat (recorded)..."); color = "#da4453"; } else { placeholder = tr("Chat..."); - color = "#1d99f3"; + color = "#4d4d4d"; } // Set placeholder text and window style based on the mode - m_myline->setPlaceholderText(placeholder); - setStyleSheet(QStringLiteral( + myline->setPlaceholderText(placeholder); + + chatbox->setStyleSheet( +#ifdef Q_OS_OSX // QTBUG-61092 (close button not visible on macOS) + QStringLiteral("QTabBar::close-button{ background-position: center; background-image: url(\"builtin:dock-close.svg\"); }") + +#endif + QStringLiteral( "QTextEdit, QLineEdit {" + "background-color: #313438;" "border: none;" - "background-color: #232629;" - "color: #bdc3c7;" - "font-family: Monospace" + "color: #eff0f1" "}" "QLineEdit {" - "border-top: 2px solid %1" + "border-top: 1px solid %1;" + "padding: 4px" "}" ).arg(color) ); + +} +void ChatBox::setPreserveMode(bool preservechat) +{ + d->preserveChat = preservechat; + d->updatePreserveModeUi(); } void ChatBox::loggedIn(int myId) { - m_myId = myId; - m_usernames.clear(); + d->myId = myId; + d->announcedUsers.clear(); } void ChatBox::focusInput() { - m_myline->setFocus(); + d->myline->setFocus(); +} + +void ChatBox::setUserList(canvas::UserListModel *userlist) +{ + d->userlist = userlist; } void ChatBox::clear() { - m_view->clear(); + Chat &chat = d->chats[d->currentChat]; + chat.doc->clear(); + chat.lastAppendedId = 0; + + // Re-add avatars + if(d->userlist) { + for(int i=0;iuserlist->rowCount();++i) { + const QModelIndex idx = d->userlist->index(i); + chat.doc->addResource( + QTextDocument::ImageResource, + QUrl(QStringLiteral("avatar://%1").arg(idx.data(canvas::UserListModel::IdRole).toInt())), + idx.data(canvas::UserListModel::AvatarRole) + ); + } + } +} + +bool ChatBox::Private::ensurePrivateChatExists(int userId, QObject *parent) +{ + if(userId < 1 || userId > 255) { + qWarning("ChatBox::openPrivateChat(%d): Invalid user ID", userId); + return false; + } + if(userId == myId) { + qWarning("ChatBox::openPrivateChat(%d): this is me...", userId); + return false; + } + + if(!chats.contains(userId)) { + chats[userId] = Chat(parent); + const int newTab = tabs->addTab(userlist->getUsername(userId)); + tabs->setTabData(newTab, userId); + + chats[userId].doc->addResource( + QTextDocument::ImageResource, + QUrl(QStringLiteral("avatar://%1").arg(userId)), + userlist->getUserById(userId).avatar + ); + chats[userId].doc->addResource( + QTextDocument::ImageResource, + QUrl(QStringLiteral("avatar://%1").arg(myId)), + userlist->getUserById(myId).avatar + ); + } + + return true; +} + +void ChatBox::openPrivateChat(int userId) +{ + if(!d->ensurePrivateChatExists(userId, this)) + return; + + for(int i=d->tabs->count()-1;i>=0;--i) { + if(d->tabs->tabData(i).toInt() == userId) { + d->tabs->setCurrentIndex(i); + break; + } + } } static QString timestamp() { - return "" + QDateTime::currentDateTime().toString("HH:mm:ss") + ""; + return QStringLiteral("%1").arg( + QDateTime::currentDateTime().toString("HH:mm") + ); +} + +QString ChatBox::Private::usernameSpan(int userId) +{ + const canvas::User user = userlist ? userlist->getUserById(userId) : canvas::User(); + + QString userclass; + if(user.isMod) + userclass = QStringLiteral("mod"); + else if(user.isOperator) + userclass = QStringLiteral("op"); + else if(user.isTrusted) + userclass = QStringLiteral("trusted"); + else if(user.isAuth) + userclass = QStringLiteral("registered"); + + return QStringLiteral("%2").arg( + userclass, + user.name.isEmpty() ? QStringLiteral("User #%1").arg(userId) : user.name.toHtmlEscaped() + ); +} + +void Chat::appendSeparator(QTextCursor &cursor) +{ + cursor.insertHtml(QStringLiteral( + "
    " + )); +} + +void Chat::appendMessage(int userId, const QString &usernameSpan, const QString &message, bool shout) +{ + QTextCursor cursor(doc); + cursor.movePosition(QTextCursor::End); + + const qint64 ts = QDateTime::currentMSecsSinceEpoch(); + + if(shout) { + lastAppendedId = -2; + + } else if(lastAppendedId != userId) { + appendSeparator(cursor); + lastAppendedId = userId; + + } else if(ts - lastMessageTs < 60000) { + QTextBlock b = doc->lastBlock().previous(); + cursor.setPosition(b.position() + b.length() - 1); + + cursor.insertHtml(QStringLiteral("
    ")); + cursor.insertHtml(message); + + return; + } + + // We'll have to make do with a very limited subset of HTML and CSS: + // http://doc.qt.io/qt-5/richtext-html-subset.html + // Embedding a whole browser engine just to render the chat widget would + // be excessive. + cursor.insertHtml(QStringLiteral( + "" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "
    %3%4
    %5
    " + ).arg( + shout ? QStringLiteral(" shout") : QString(), + QString::number(userId), + usernameSpan, + timestamp(), + htmlutils::newlineToBr(message) + ) + ); + lastMessageTs = ts; +} + +void Chat::appendAction(const QString &usernameSpan, const QString &message) +{ + QTextCursor cursor(doc); + cursor.movePosition(QTextCursor::End); + + if(lastAppendedId != -1) { + appendSeparator(cursor); + lastAppendedId = -1; + } + + cursor.insertHtml(QStringLiteral( + "" + "" + "" + "" + "" + "
    %1 %2%3
    " + ).arg( + usernameSpan, + message, + timestamp() + ) + ); +} + +void Chat::appendNotification(const QString &message) +{ + QTextCursor cursor(doc); + cursor.movePosition(QTextCursor::End); + + if(lastAppendedId != 0) { + appendSeparator(cursor); + lastAppendedId = 0; + } + + cursor.insertHtml(QStringLiteral( + "" + "" + "" + "
    %1%2
    " + ).arg( + htmlutils::newlineToBr(message), + timestamp() + ) + ); } void ChatBox::userJoined(int id, const QString &name) { - const QString escapedName = name.toHtmlEscaped(); + Q_UNUSED(name); + + if(d->userlist) { + d->chats[0].doc->addResource( + QTextDocument::ImageResource, + QUrl(QStringLiteral("avatar://%1").arg(id)), + d->userlist->getUserById(id).avatar + ); + if(d->chats.contains(id)) { + d->chats[id].doc->addResource( + QTextDocument::ImageResource, + QUrl(QStringLiteral("avatar://%1").arg(id)), + d->userlist->getUserById(id).avatar + ); + } + + } else { + qWarning("User #%d logged in, but userlist object not assigned to ChatWidget!", id); + } + // The server resends UserJoin messages during session reset. // We don't need to see the join messages again. - if(m_usernames.contains(id) && m_usernames[id] == escapedName) + if(d->announcedUsers.contains(id)) return; - m_usernames[id] = escapedName; - systemMessage(tr("%2 joined the session").arg(name.toHtmlEscaped())); - notification::playSound(notification::Event::LOGIN); -} + d->announcedUsers << id; + const QString msg = tr("%1 joined the session").arg(d->usernameSpan(id)); + d->publicChat().appendNotification(msg); + d->scrollToEnd(0); -QString ChatBox::username(int id) const -{ - if(m_usernames.contains(id)) - return m_usernames[id]; - else - return QStringLiteral("User #%1").arg(id); + if(d->chats.contains(id)) { + d->chats[id].appendNotification(msg); + d->scrollToEnd(id); + } + + notification::playSound(notification::Event::LOGIN); } void ChatBox::userParted(int id) { - systemMessage(tr("%1 left the session").arg(username(id))); + QString msg = tr("%1 left the session").arg(d->usernameSpan(id)); + d->publicChat().appendNotification(msg); + d->scrollToEnd(0); + + if(d->chats.contains(id)) { + d->chats[id].appendNotification(msg); + d->scrollToEnd(id); + } + + d->announcedUsers.removeAll(id); + notification::playSound(notification::Event::LOGOUT); - m_usernames.remove(id); } void ChatBox::kicked(const QString &kickedBy) { - systemMessage(tr("You have been kicked by %1").arg(kickedBy.toHtmlEscaped())); + d->publicChat().appendNotification(tr("You have been kicked by %1").arg(kickedBy.toHtmlEscaped())); + d->scrollToEnd(0); } void ChatBox::receiveMessage(const protocol::MessagePtr &msg) { - if(msg->type() != protocol::MSG_CHAT) { - qWarning("ChatBox::receiveMessage: message type (%d) is not MSG_CHAT!", msg->type()); - return; - } + int chatId = 0; + + if(msg->type() == protocol::MSG_CHAT) { + const protocol::Chat &chat = msg.cast(); + const QString safetext = chat.message().toHtmlEscaped(); + + if(chat.isPin()) { + if(safetext == "-") { + // note: the protocol doesn't allow empty chat messages, + // which is why we have to use a special value like this + // to clear the pinning. + d->pinned->setVisible(false); + d->pinned->setText(QString()); + } else { + d->pinned->setText(htmlutils::linkify(safetext, QStringLiteral("style=\"color:#3daae9\""))); + d->pinned->setVisible(true); + } + + } else if(chat.isAction()) { + d->publicChat().appendAction(d->usernameSpan(msg->contextId()), htmlutils::linkify(safetext)); - const protocol::Chat &chat = msg.cast(); - QString txt = chat.message().toHtmlEscaped(); - - if(chat.isPin()) { - if(txt == "-") { - // note: the protocol doesn't allow empty chat messages, - // which is why we have to use a special value like this - // to clear the pinning. - m_pinned->setVisible(false); - m_pinned->setText(QString()); } else { - m_pinned->setText(htmlutils::linkify(txt, QStringLiteral("style=\"color:#3daae9\""))); - m_pinned->setVisible(true); + d->publicChat().appendMessage(msg->contextId(), d->usernameSpan(msg->contextId()), htmlutils::linkify(safetext), chat.isShout()); } - } else if(chat.isAction()) { - m_view->append(QStringLiteral("

    %1 * %2 %3

    ") - .arg(timestamp(), username(msg->contextId()), htmlutils::linkify(txt)) - ); + } else if(msg->type() == protocol::MSG_PRIVATE_CHAT) { + const protocol::PrivateChat &chat = msg.cast(); + const QString safetext = chat.message().toHtmlEscaped(); + + if(chat.target() != d->myId && chat.contextId() != d->myId) { + qWarning("ChatBox::recivePrivateMessage: message was targeted to user %d, but our ID is %d", chat.target(), d->myId); + return; + } + + // The server echoes back the messages we send + chatId = chat.target() == d->myId ? chat.contextId() : chat.target(); + + if(!d->ensurePrivateChatExists(chatId, this)) + return; + + Chat &c = d->chats[chatId]; + + if(chat.isAction()) { + c.appendAction(d->usernameSpan(msg->contextId()), htmlutils::linkify(safetext)); + + } else { + c.appendMessage(msg->contextId(), d->usernameSpan(msg->contextId()), htmlutils::linkify(safetext), false); + } } else { - m_view->append(QStringLiteral("

    %1 <%3> %5

    ") - .arg( - timestamp(), - chat.contextId() == m_myId ? QStringLiteral("me") : QString(), - username(msg->contextId()), - chat.isShout() ? QStringLiteral("announcement") : QString(), - htmlutils::linkify(txt) - )); + qWarning("ChatBox::receiveMessage: got wrong message type %s!", qPrintable(msg->messageName())); + return; } - if(!m_myline->hasFocus()) - notification::playSound(notification::Event::CHAT); + if(chatId != d->currentChat) { + for(int i=0;itabs->count();++i) { + if(d->tabs->tabData(i).toInt() == chatId) { + if(chatId == 0) + d->tabs->setTabIcon(i, QIcon("builtin:chat-alert.svg")); + else + d->tabs->setTabTextColor(i, QColor(218, 68, 83)); + break; + } + } + } + if(!d->myline->hasFocus() || chatId != d->currentChat) + notification::playSound(notification::Event::CHAT); + d->scrollToEnd(chatId); } void ChatBox::receiveMarker(int id, const QString &message) { - m_view->append( - "

    " + timestamp() + " <" + - username(id) + - "> " + - htmlutils::linkify(message.toHtmlEscaped()) + - "

    " + d->publicChat().appendNotification(QStringLiteral( + " %1: %2" + ).arg( + d->usernameSpan(id), + htmlutils::linkify(message.toHtmlEscaped()) + ) ); + + d->scrollToEnd(0); } -/** - * @param message the message - */ void ChatBox::systemMessage(const QString& message, bool alert) { Q_UNUSED(alert); - m_view->append("

    " + timestamp() + " *** " + message + " ***

    "); + d->publicChat().appendNotification(message.toHtmlEscaped()); + d->scrollToEnd(0); } void ChatBox::sendMessage(const QString &msg) @@ -239,73 +587,112 @@ void ChatBox::sendMessage(const QString &msg) if(split<0) split = msg.length(); - QString cmd = msg.mid(1, split-1).toLower(); - QString params = msg.mid(split).trimmed(); + const QString cmd = msg.mid(1, split-1).toLower(); + const QString params = msg.mid(split).trimmed(); if(cmd == "clear") { - // client side command: clear chat window clear(); return; - } else if(cmd.at(0)=='!') { - // public announcement - emit message(protocol::Chat::announce(m_myId, msg.mid(2))); + } else if(cmd.at(0)=='!' && d->currentChat == 0) { + emit message(protocol::Chat::announce(d->myId, msg.mid(2))); return; } else if(cmd == "me") { - if(!params.isEmpty()) - emit message(protocol::Chat::action(m_myId, msg.mid(msg.indexOf(' ')+1), !m_preserveChat)); + if(!params.isEmpty()) { + if(d->currentChat == 0) + emit message(protocol::Chat::action(d->myId, params, !d->preserveChat)); + else + emit message(protocol::PrivateChat::action(d->myId, d->currentChat, params)); + } return; - } else if(cmd == "pin") { + } else if(cmd == "pin" && d->currentChat == 0) { if(!params.isEmpty()) - emit message(protocol::Chat::pin(m_myId, msg.mid(msg.indexOf(' ')+1))); + emit message(protocol::Chat::pin(d->myId, params)); return; - } else if(cmd == "unpin") { - emit message(protocol::Chat::pin(m_myId, QStringLiteral("-"))); + } else if(cmd == "unpin" && d->currentChat == 0) { + emit message(protocol::Chat::pin(d->myId, QStringLiteral("-"))); return; } else if(cmd == "roll") { - if(params.isEmpty()) - params = "1d6"; - - utils::DiceRoll result = utils::diceRoll(params); - if(result.number>0) - emit message(protocol::Chat::action(m_myId, "rolls " + result.toString(), !m_preserveChat)); - else - systemMessage(tr("Invalid dice roll description: %1").arg(params)); + utils::DiceRoll result = utils::diceRoll(params.isEmpty() ? QStringLiteral("1d6") : params); + if(result.number>0) { + if(d->currentChat == 0) + emit message(protocol::Chat::action(d->myId, "rolls " + result.toString(), !d->preserveChat)); + else + emit message(protocol::PrivateChat::action(d->myId, d->currentChat, "rolls " + result.toString())); + } else + systemMessage(tr("Invalid dice roll description")); return; -#ifndef NDEBUG - } else if(cmd == "rolltest") { - if(params.isEmpty()) - params = "1d6"; - - QList d = utils::diceRollDistribution(params); - QString msg(params + "\n"); - for(int i=0;i - make an announcement (recorded in session history)\n" + "/me - send action type message\n" + "/pin - pin a message to the top of the chat box (Ops only)\n" + "/unpin - remove pinned message\n" + "/roll [AdX] - roll dice" + ); + systemMessage(text); return; -#endif } } // A normal chat message - emit message(protocol::Chat::regular(m_myId, msg, !m_preserveChat)); + if(d->currentChat == 0) + emit message(protocol::Chat::regular(d->myId, msg, !d->preserveChat)); + else + emit message(protocol::PrivateChat::regular(d->myId, d->currentChat, msg)); +} + +void ChatBox::chatTabSelected(int index) +{ + d->chats[d->currentChat].scrollPosition = d->view->verticalScrollBar()->value(); + + const int id = d->tabs->tabData(index).toInt(); + Q_ASSERT(d->chats.contains(id)); + d->view->setDocument(d->chats[id].doc); + d->view->verticalScrollBar()->setValue(d->chats[id].scrollPosition); + + if(id == 0) + d->tabs->setTabIcon(index, QIcon("builtin:chat.svg")); + else + d->tabs->setTabTextColor(index, QColor()); + + d->currentChat = d->tabs->tabData(index).toInt(); + d->updatePreserveModeUi(); +} + +void ChatBox::chatTabClosed(int index) +{ + const int id = d->tabs->tabData(index).toInt(); + Q_ASSERT(d->chats.contains(id)); + if(id == 0) { + // Can't close the public chat + return; + } + + d->tabs->removeTab(index); + + delete d->chats[id].doc; + d->chats.remove(id); } void ChatBox::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); if(event->size().height() == 0) { - if(!m_wasCollapsed) + if(!d->wasCollapsed) emit expanded(false); - m_wasCollapsed = true; - } else if(m_wasCollapsed) { - m_wasCollapsed = false; + d->wasCollapsed = true; + } else if(d->wasCollapsed) { + d->wasCollapsed = false; emit expanded(true); } } diff --git a/src/desktop/widgets/chatwidget.h b/src/desktop/widgets/chatwidget.h index 91d5bdba1..2df80eaca 100644 --- a/src/desktop/widgets/chatwidget.h +++ b/src/desktop/widgets/chatwidget.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2007-2018 Calle Laakkonen + Copyright (C) 2007-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,13 +20,9 @@ #define CHATWIDGET_H #include -#include - -class QTextBrowser; -class ChatLineEdit; -class QLabel; namespace protocol { class MessagePtr; } +namespace canvas { class UserListModel; } namespace widgets { @@ -37,13 +33,15 @@ namespace widgets { */ class ChatBox: public QWidget { -Q_OBJECT + Q_OBJECT public: - explicit ChatBox(QWidget *parent=0); + explicit ChatBox(QWidget *parent=nullptr); //! Focus the text input widget void focusInput(); + void setUserList(canvas::UserListModel *userlist); + public slots: /** * @brief Set default message preservation mode @@ -73,8 +71,13 @@ public slots: //! Initialize the chat box for a new server void loggedIn(int myId); + //! Open a private chat view with this user + void openPrivateChat(int userId); + private slots: void sendMessage(const QString &msg); + void chatTabSelected(int index); + void chatTabClosed(int index); signals: void message(const protocol::MessagePtr &msg); @@ -84,14 +87,8 @@ private slots: void resizeEvent(QResizeEvent *event); private: - QTextBrowser *m_view; - ChatLineEdit *m_myline; - QLabel *m_pinned; - QHash m_usernames; - QString username(int id) const; - bool m_wasCollapsed; - bool m_preserveChat; - int m_myId; + struct Private; + Private *d; }; } diff --git a/src/desktop/widgets/designer_plugin/collection.cpp b/src/desktop/widgets/designer_plugin/collection.cpp index 7c497a40e..f2fcb39df 100644 --- a/src/desktop/widgets/designer_plugin/collection.cpp +++ b/src/desktop/widgets/designer_plugin/collection.cpp @@ -24,6 +24,7 @@ #include "filmstrip_plugin.h" #include "resizer_plugin.h" #include "tablettester_plugin.h" +#include "spinner_plugin.h" DrawpileWidgetCollection::DrawpileWidgetCollection(QObject *parent) : QObject(parent) @@ -35,6 +36,7 @@ DrawpileWidgetCollection::DrawpileWidgetCollection(QObject *parent) : << new FilmstripPlugin(this) << new ResizerPlugin(this) << new TabletTesterPlugin(this) + << new SpinnerPlugin(this) ; } diff --git a/src/desktop/widgets/designer_plugin/drawpile_plugins.pro b/src/desktop/widgets/designer_plugin/drawpile_plugins.pro index efa91425c..aedea7c23 100644 --- a/src/desktop/widgets/designer_plugin/drawpile_plugins.pro +++ b/src/desktop/widgets/designer_plugin/drawpile_plugins.pro @@ -16,14 +16,16 @@ HEADERS += collection.h \ ../groupedtoolbutton.h groupedtoolbutton_plugin.h \ ../filmstrip.h filmstrip_plugin.h \ ../resizerwidget.h resizer_plugin.h \ - ../brushpreview.h brushpreview_plugin.h ../../../client/core/layerstack.h ../../../client/core/annotationmodel.h \ - ../tablettest.h tablettester_plugin.h + ../brushpreview.h brushpreview_plugin.h \ + ../tablettest.h tablettester_plugin.h \ + ../spinner.h spinner_plugin.h SOURCES += collection.cpp \ ../colorbutton.cpp colorbutton_plugin.cpp \ ../groupedtoolbutton.cpp groupedtoolbutton_plugin.cpp \ ../filmstrip.cpp filmstrip_plugin.cpp \ ../resizerwidget.cpp resizer_plugin.cpp \ - ../brushpreview.cpp brushpreview_plugin.cpp ../../../client/core/brush.cpp ../../../client/core/brushmask.cpp ../../../client/core/layer.cpp ../../../client/core/layerstack.cpp ../../../client/core/tile.cpp ../../../client/core/rasterop.cpp ../../../client/core/shapes.cpp ../../../client/core/floodfill.cpp ../../../client/core/annotationmodel.cpp \ - ../tablettest.cpp tablettester_plugin.cpp + ../brushpreview.cpp brushpreview_plugin.cpp \ + ../tablettest.cpp tablettester_plugin.cpp \ + ../spinner.cpp spinner_plugin.cpp diff --git a/src/desktop/widgets/designer_plugin/spinner_plugin.cpp b/src/desktop/widgets/designer_plugin/spinner_plugin.cpp new file mode 100644 index 000000000..5d4509e94 --- /dev/null +++ b/src/desktop/widgets/designer_plugin/spinner_plugin.cpp @@ -0,0 +1,99 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#include + +#include "../spinner.h" +#include "spinner_plugin.h" + +SpinnerPlugin::SpinnerPlugin(QObject *parent) + : QObject(parent) +{ + initialized = false; +} + +void SpinnerPlugin::initialize(QDesignerFormEditorInterface * /* core */) +{ + if (initialized) + return; + + initialized = true; +} + +bool SpinnerPlugin::isInitialized() const +{ + return initialized; +} + +QWidget *SpinnerPlugin::createWidget(QWidget *parent) +{ + return new Spinner(parent); +} + +QString SpinnerPlugin::name() const +{ + return "Spinner"; +} + +QString SpinnerPlugin::group() const +{ + return "Drawpile Widgets"; +} + +QIcon SpinnerPlugin::icon() const +{ + return QIcon(); +} + +QString SpinnerPlugin::toolTip() const +{ + return "A loading spinner"; +} + +QString SpinnerPlugin::whatsThis() const +{ + return ""; +} + +bool SpinnerPlugin::isContainer() const +{ + return false; +} + +QString SpinnerPlugin::domXml() const +{ + return "\n" + "\n" + " \n" + " \n" + " 0\n" + " 0\n" + " 64\n" + " 64\n" + " \n" + " \n" + "\n" + ""; +} + +QString SpinnerPlugin::includeFile() const +{ + return "widgets/spinner.h"; +} + diff --git a/src/desktop/widgets/designer_plugin/spinner_plugin.h b/src/desktop/widgets/designer_plugin/spinner_plugin.h new file mode 100644 index 000000000..6f5e22e29 --- /dev/null +++ b/src/desktop/widgets/designer_plugin/spinner_plugin.h @@ -0,0 +1,50 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#ifndef SPINNERPLUGIN_H +#define SPINNERPLUGIN_H + +#include + +class SpinnerPlugin : public QObject, public QDesignerCustomWidgetInterface +{ +Q_OBJECT +Q_INTERFACES(QDesignerCustomWidgetInterface) + +public: + SpinnerPlugin(QObject *parent=nullptr); + + bool isContainer() const; + bool isInitialized() const; + QIcon icon() const; + QString domXml() const; + QString group() const; + QString includeFile() const; + QString name() const; + QString toolTip() const; + QString whatsThis() const; + QWidget *createWidget(QWidget *parent); + void initialize(QDesignerFormEditorInterface *core); + +private: + bool initialized; +}; + +#endif + diff --git a/src/desktop/widgets/macmenu.cpp b/src/desktop/widgets/macmenu.cpp index bad25fb6c..19adde853 100644 --- a/src/desktop/widgets/macmenu.cpp +++ b/src/desktop/widgets/macmenu.cpp @@ -156,7 +156,7 @@ void MacMenu::openRecent(QAction *action) void MacMenu::joinSession() { auto dlg = new dialogs::JoinDialog(QUrl()); - connect(dlg, &dialogs::JoinDialog::finished, [this, dlg](int i) { + connect(dlg, &dialogs::JoinDialog::finished, [dlg](int i) { if(i==QDialog::Accepted) { QUrl url = dlg->getUrl(); @@ -169,7 +169,7 @@ void MacMenu::joinSession() dlg->rememberSettings(); MainWindow *mw = new MainWindow; - mw->joinSession(url, dlg->recordSession()); + mw->joinSession(url, dlg->autoRecordFilename()); } dlg->deleteLater(); }); diff --git a/src/desktop/widgets/netstatus.cpp b/src/desktop/widgets/netstatus.cpp index b838f1efd..4f16e078c 100644 --- a/src/desktop/widgets/netstatus.cpp +++ b/src/desktop/widgets/netstatus.cpp @@ -115,26 +115,17 @@ NetStatus::NetStatus(QWidget *parent) connect(showNetStats, SIGNAL(triggered()), this, SLOT(showNetStats())); // Security level icon - _security = new QLabel(QString(), this); - _security->setFixedSize(QSize(16, 16)); - _security->hide(); - layout->addWidget(_security); + m_security = new QLabel(QString(), this); + m_security->setFixedSize(QSize(16, 16)); + m_security->hide(); + layout->addWidget(m_security); - _security->setContextMenuPolicy(Qt::ActionsContextMenu); + m_security->setContextMenuPolicy(Qt::ActionsContextMenu); QAction *showcert = new QAction(tr("Show certificate"), this); - _security->addAction(showcert); + m_security->addAction(showcert); connect(showcert, SIGNAL(triggered()), this, SLOT(showCertificate())); - // Low space alert - m_lowspace = new QLabel(tr("Low space!"), this); - m_lowspace->setToolTip(tr("Server is almost out of space for session history! Reset the session to free some up.")); - QPalette lowSpacePalette = m_lowspace->palette(); - lowSpacePalette.setColor(QPalette::WindowText, Qt::red); - m_lowspace->setPalette(lowSpacePalette); - m_lowspace->setVisible(false); - layout->addWidget(m_lowspace); - // Popup label m_popup = new PopupMessage(this); @@ -214,21 +205,16 @@ void NetStatus::setSecurityLevel(net::Server::Security level, const QSslCertific } if(iconname.isEmpty()) { - _security->hide(); + m_security->hide(); } else { - _security->setPixmap(QIcon("builtin:" + iconname + ".svg").pixmap(16, 16)); - _security->setToolTip(tooltip); - _security->show(); + m_security->setPixmap(QIcon("builtin:" + iconname + ".svg").pixmap(16, 16)); + m_security->setToolTip(tooltip); + m_security->show(); } m_certificate = certificate; } -void NetStatus::setLowSpaceAlert(bool lowSpace) -{ - m_lowspace->setVisible(lowSpace); -} - void NetStatus::hostDisconnecting() { m_state = Disconnecting; @@ -253,7 +239,6 @@ void NetStatus::hostDisconnected() message(tr("Disconnected")); setSecurityLevel(net::Server::NO_SECURITY, QSslCertificate()); - setLowSpaceAlert(false); if(_netstats) _netstats->setDisconnected(); diff --git a/src/desktop/widgets/netstatus.h b/src/desktop/widgets/netstatus.h index d01ef6120..5a0111ee9 100644 --- a/src/desktop/widgets/netstatus.h +++ b/src/desktop/widgets/netstatus.h @@ -82,8 +82,6 @@ public slots: //! This user was kicked off the session void kicked(const QString& user); - void setLowSpaceAlert(bool lowSpace); - void copyAddress(); void copyUrl(); @@ -109,7 +107,7 @@ private slots: QPointer _netstats; QProgressBar *m_download; - QLabel *m_label, *_security, *m_lowspace; + QLabel *m_label, *m_security; PopupMessage *m_popup; QString m_address; QString m_roomcode; diff --git a/src/desktop/widgets/spinner.cpp b/src/desktop/widgets/spinner.cpp new file mode 100644 index 000000000..6b6e1a628 --- /dev/null +++ b/src/desktop/widgets/spinner.cpp @@ -0,0 +1,64 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#include "spinner.h" + +#include +#include + +#ifndef DESIGNER_PLUGIN +namespace widgets { +#endif + +Spinner::Spinner(QWidget *parent): + QWidget(parent), m_dots(8), m_currentDot(0) +{ + startTimer(150); +} + +void Spinner::paintEvent(QPaintEvent *) +{ + const int RADIUS = qMin(width(), height()) / 2; + const int DOT_SIZE = (2 * M_PI * RADIUS) / (2 + m_dots*2); + + QPainter painter(this); + + painter.translate(width()/2, height()/2); + painter.setPen(Qt::NoPen); + painter.setRenderHint(QPainter::Antialiasing); + + for(int dot=0;dot= m_dots) + m_currentDot = 0; + update(); +} + +#ifndef DESIGNER_PLUGIN +} +#endif + diff --git a/src/desktop/widgets/spinner.h b/src/desktop/widgets/spinner.h new file mode 100644 index 000000000..fd54ffdef --- /dev/null +++ b/src/desktop/widgets/spinner.h @@ -0,0 +1,56 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#ifndef WIDGET_SPINNER_H +#define WIDGET_SPINNER_H + +#include + +#ifndef DESIGNER_PLUGIN +namespace widgets { +#define PLUGIN_EXPORT +#else +#include +#define PLUGIN_EXPORT QDESIGNER_WIDGET_EXPORT +#endif + +class Spinner : public QWidget { + Q_OBJECT + Q_PROPERTY(int dots READ dots WRITE setDots) +public: + Spinner(QWidget *parent=nullptr); + + int dots() const { return m_dots; } + void setDots(int dots) { m_dots = qBound(2, dots, 32); } + +protected: + void paintEvent(QPaintEvent *); + void timerEvent(QTimerEvent *); + +private: + int m_dots; + int m_currentDot; +}; + +#ifndef DESIGNER_PLUGIN +} +#endif + +#endif + diff --git a/src/desktop/widgets/useritemdelegate.cpp b/src/desktop/widgets/useritemdelegate.cpp new file mode 100644 index 000000000..a3d0dd9c8 --- /dev/null +++ b/src/desktop/widgets/useritemdelegate.cpp @@ -0,0 +1,317 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ +#include "useritemdelegate.h" +#include "canvas/userlist.h" +#include "canvas/canvasmodel.h" +#include "canvas/aclfilter.h" +#include "utils/icon.h" +#include "net/commands.h" +#include "document.h" +#include "../shared/net/undo.h" + +#include +#include +#include +#include + +namespace widgets { + +static const int MARGIN = 4; +static const int PADDING = 8; +static const int AVATAR_SIZE = 42; +static const int STATUS_OVERLAY_SIZE = 16; +static const int BUTTON_WIDTH = 16; + +UserItemDelegate::UserItemDelegate(QObject *parent) + : QAbstractItemDelegate(parent), m_doc(nullptr) +{ + m_userMenu = new QMenu; + m_menuTitle = m_userMenu->addSection("User"); + + m_opAction = m_userMenu->addAction(tr("Operator")); + m_trustAction = m_userMenu->addAction(tr("Trusted")); + + m_userMenu->addSeparator(); + m_lockAction = m_userMenu->addAction(tr("Lock")); + m_muteAction = m_userMenu->addAction(tr("Mute")); + + m_userMenu->addSeparator(); + m_undoAction = m_userMenu->addAction(tr("Undo")); + m_redoAction = m_userMenu->addAction(tr("Redo")); + + m_userMenu->addSeparator(); + m_kickAction = m_userMenu->addAction(tr("Kick")); + m_banAction = m_userMenu->addAction(tr("Kick && Ban")); + + m_userMenu->addSeparator(); + m_chatAction = m_userMenu->addAction(tr("Private Message")); + + m_opAction->setCheckable(true); + m_trustAction->setCheckable(true); + m_lockAction->setCheckable(true); + m_muteAction->setCheckable(true); + + connect(m_opAction, &QAction::triggered, this, &UserItemDelegate::toggleOpMode); + connect(m_trustAction, &QAction::triggered, this, &UserItemDelegate::toggleTrusted); + connect(m_lockAction, &QAction::triggered, this, &UserItemDelegate::toggleLock); + connect(m_muteAction, &QAction::triggered, this, &UserItemDelegate::toggleMute); + connect(m_kickAction, &QAction::triggered, this, &UserItemDelegate::kickUser); + connect(m_banAction, &QAction::triggered, this, &UserItemDelegate::banUser); + connect(m_chatAction, &QAction::triggered, this, &UserItemDelegate::pmUser); + connect(m_undoAction, &QAction::triggered, this, &UserItemDelegate::undoByUser); + connect(m_redoAction, &QAction::triggered, this, &UserItemDelegate::redoByUser); + + m_lockIcon = icon::fromTheme("object-locked").pixmap(16, 16); + m_muteIcon = icon::fromTheme("irc-unvoice").pixmap(16, 16); +} + +UserItemDelegate::~UserItemDelegate() +{ + delete m_userMenu; +} + +QSize UserItemDelegate::sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const +{ + Q_UNUSED(option); + Q_UNUSED(index); + + return QSize(AVATAR_SIZE*4, AVATAR_SIZE+2*MARGIN); +} + +void UserItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + painter->save(); + painter->fillRect(option.rect, option.backgroundBrush); + painter->setRenderHint(QPainter::Antialiasing); + + // Draw avatar + const QRect avatarRect( + option.rect.x() + MARGIN, + option.rect.y() + MARGIN, + AVATAR_SIZE, + AVATAR_SIZE + ); + painter->drawPixmap( + avatarRect, + index.data(canvas::UserListModel::AvatarRole).value() + ); + + // Draw status overlay + const bool isLocked = index.data(canvas::UserListModel::IsLockedRole).toBool(); + if(isLocked || index.data(canvas::UserListModel::IsMutedRole).toBool()) { + const QRect statusOverlayRect( + avatarRect.right() - STATUS_OVERLAY_SIZE, + avatarRect.bottom() - STATUS_OVERLAY_SIZE, + STATUS_OVERLAY_SIZE, + STATUS_OVERLAY_SIZE + ); + + painter->setBrush(option.palette.color(QPalette::AlternateBase)); + painter->drawEllipse(statusOverlayRect.adjusted(-2, -2, 2, 2)); + if(isLocked) + painter->drawPixmap(statusOverlayRect, m_lockIcon); + else + painter->drawPixmap(statusOverlayRect, m_muteIcon); + } + + // Draw username + const QRect usernameRect( + avatarRect.right() + PADDING, + avatarRect.y(), + option.rect.width() - avatarRect.right() - PADDING - BUTTON_WIDTH - MARGIN, + option.rect.height() - MARGIN*2 + ); + QFont font = option.font; + font.setPixelSize(22); + font.setWeight(QFont::Light); + painter->setPen(option.palette.text().color()); + painter->setFont(font); + painter->drawText(usernameRect, index.data(canvas::UserListModel::NameRole).toString()); + + // Draw user flags + QString flags; + + if(index.data(canvas::UserListModel::IsModRole).toBool()) { + flags = tr("Moderator"); + + } else { + if(index.data(canvas::UserListModel::IsOpRole).toBool()) + flags = tr("Operator"); + else if(index.data(canvas::UserListModel::IsTrustedRole).toBool()) + flags = tr("Trusted"); + + if(index.data(canvas::UserListModel::IsBotRole).toBool()) { + if(!flags.isEmpty()) + flags += " | "; + flags += tr("Bot"); + + } else if(index.data(canvas::UserListModel::IsAuthRole).toBool()) { + if(!flags.isEmpty()) + flags += " | "; + flags += tr("Registered"); + } + } + + if(!flags.isEmpty()) { + font.setPixelSize(12); + font.setWeight(QFont::Normal); + painter->setFont(font); + painter->drawText(usernameRect, Qt::AlignBottom, flags); + } + + // Draw the context menu buttons + const QRect buttonRect( + option.rect.right() - BUTTON_WIDTH - MARGIN, + option.rect.top() + MARGIN, + BUTTON_WIDTH, + option.rect.height() - 2*MARGIN + ); + + painter->setPen(Qt::NoPen); + //if((option.state & QStyle::State_MouseOver)) + painter->setBrush(option.palette.color(QPalette::WindowText)); + //else + //painter->setBrush(option.palette.color(QPalette::AlternateBase)); + + const int buttonSize = buttonRect.height()/7; + for(int i=0;i<3;++i) { + painter->drawEllipse(QRect( + buttonRect.x() + (buttonRect.width()-buttonSize)/2, + buttonRect.y() + (1+i*2) * buttonSize, + buttonSize, + buttonSize + )); + } + + painter->restore(); +} + +bool UserItemDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) +{ + Q_UNUSED(model); + + if(event->type() == QEvent::MouseButtonPress && m_doc) { + const QMouseEvent *e = static_cast(event); + + if(e->button() == Qt::RightButton || (e->button() == Qt::LeftButton && e->x() > option.rect.right() - MARGIN - BUTTON_WIDTH)) { + showContextMenu(index, e->globalPos()); + return true; + } + } + else if(event->type() == QEvent::MouseButtonDblClick && m_doc) { + const int userId = index.data(canvas::UserListModel::IdRole).toInt(); + if(userId>0 && userId != m_doc->canvas()->localUserId()) { + emit requestPrivateChat(userId); + return true; + } + } + return false; +} + +void UserItemDelegate::showContextMenu(const QModelIndex &index, const QPoint &pos) +{ + m_menuId = index.data(canvas::UserListModel::IdRole).toInt(); + + m_menuTitle->setText(index.data(canvas::UserListModel::NameRole).toString()); + + const bool amOp = m_doc->canvas()->aclFilter()->isLocalUserOperator(); + const bool amDeputy = m_doc->canvas()->aclFilter()->isLocalUserTrusted() && m_doc->isSessionDeputies(); + const bool isSelf = m_menuId == m_doc->canvas()->localUserId(); + const bool isMod = index.data(canvas::UserListModel::IsModRole).toBool(); + + m_opAction->setChecked(index.data(canvas::UserListModel::IsOpRole).toBool()); + m_trustAction->setChecked(index.data(canvas::UserListModel::IsTrustedRole).toBool()); + m_lockAction->setChecked(index.data(canvas::UserListModel::IsLockedRole).toBool()); + m_muteAction->setChecked(index.data(canvas::UserListModel::IsMutedRole).toBool()); + + // Can't deop self or moderators + m_opAction->setEnabled(amOp && !isSelf && !isMod); + + m_trustAction->setEnabled(amOp); + m_lockAction->setEnabled(amOp); + m_muteAction->setEnabled(amOp); + m_undoAction->setEnabled(amOp); + m_redoAction->setEnabled(amOp); + + // Deputies can only kick non-trusted users + // No-one can kick themselves or moderators + const bool canKick = !isSelf && !isMod && + ( + amOp || + (amDeputy && !( + index.data(canvas::UserListModel::IsOpRole).toBool() || + index.data(canvas::UserListModel::IsTrustedRole).toBool() + ) + ) + ); + m_kickAction->setEnabled(canKick); + m_banAction->setEnabled(canKick); + + // Can't chat with self + m_chatAction->setEnabled(!isSelf); + + m_userMenu->popup(pos); +} + +void UserItemDelegate::toggleOpMode(bool op) +{ + emit opCommand(m_doc->canvas()->userlist()->getOpUserCommand(m_doc->canvas()->localUserId(), m_menuId, op)); +} + +void UserItemDelegate::toggleTrusted(bool trust) +{ + emit opCommand(m_doc->canvas()->userlist()->getTrustUserCommand(m_doc->canvas()->localUserId(), m_menuId, trust)); +} + +void UserItemDelegate::toggleLock(bool op) +{ + emit opCommand(m_doc->canvas()->userlist()->getLockUserCommand(m_doc->canvas()->localUserId(), m_menuId, op)); +} + +void UserItemDelegate::toggleMute(bool mute) +{ + emit opCommand(net::command::mute(m_menuId, mute)); +} + +void UserItemDelegate::kickUser() +{ + emit opCommand(net::command::kick(m_menuId, false)); +} + +void UserItemDelegate::banUser() +{ + emit opCommand(net::command::kick(m_menuId, true)); +} + +void UserItemDelegate::pmUser() +{ + emit requestPrivateChat(m_menuId); +} + +void UserItemDelegate::undoByUser() +{ + emit opCommand(protocol::MessagePtr(new protocol::Undo(m_doc->canvas()->localUserId(), m_menuId, false))); +} + +void UserItemDelegate::redoByUser() +{ + emit opCommand(protocol::MessagePtr(new protocol::Undo(m_doc->canvas()->localUserId(), m_menuId, true))); +} + +} diff --git a/src/desktop/widgets/useritemdelegate.h b/src/desktop/widgets/useritemdelegate.h new file mode 100644 index 000000000..3864d9f05 --- /dev/null +++ b/src/desktop/widgets/useritemdelegate.h @@ -0,0 +1,88 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ +#ifndef USERITEMDELEGATE_H +#define USERITEMDELEGATE_H + +#include + +class QMenu; + +class Document; + +namespace protocol { + class MessagePtr; +} + +namespace widgets { + +class UserItemDelegate : public QAbstractItemDelegate +{ + Q_OBJECT +public: + UserItemDelegate(QObject *parent=nullptr); + ~UserItemDelegate(); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) override; + + void setDocument(Document *doc) { m_doc = doc; } + +signals: + void opCommand(protocol::MessagePtr msg); + void requestPrivateChat(int userId); + +private slots: + void toggleOpMode(bool op); + void toggleTrusted(bool trust); + void toggleLock(bool lock); + void toggleMute(bool mute); + void kickUser(); + void banUser(); + void pmUser(); + void undoByUser(); + void redoByUser(); + +private: + void showContextMenu(const QModelIndex &index, const QPoint &pos); + + QMenu *m_userMenu; + + Document *m_doc; + + QPixmap m_lockIcon; + QPixmap m_muteIcon; + + QAction *m_menuTitle; + QAction *m_opAction; + QAction *m_trustAction; + QAction *m_lockAction; + QAction *m_muteAction; + QAction *m_kickAction; + QAction *m_banAction; + QAction *m_chatAction; + QAction *m_undoAction; + QAction *m_redoAction; + + int m_menuId; +}; + +} + +#endif diff --git a/src/desktop/widgets/userlistwidget.cpp b/src/desktop/widgets/userlistwidget.cpp deleted file mode 100644 index 7f1841fd6..000000000 --- a/src/desktop/widgets/userlistwidget.cpp +++ /dev/null @@ -1,261 +0,0 @@ -/* - Drawpile - a collaborative drawing program. - - Copyright (C) 2007-2018 Calle Laakkonen - - Drawpile 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 3 of the License, or - (at your option) any later version. - - Drawpile 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 Drawpile. If not, see . -*/ - -#include -#include -#include - -#include "widgets/userlistwidget.h" -#include "canvas/canvasmodel.h" -#include "canvas/userlist.h" -#include "canvas/aclfilter.h" -#include "utils/icon.h" -#include "net/commands.h" - -#include "../shared/net/control.h" -#include "../shared/net/meta2.h" -#include "../shared/net/undo.h" - -#include "widgets/groupedtoolbutton.h" -using widgets::GroupedToolButton; -#include "ui_userbox.h" - -namespace widgets { - -UserList::UserList(QWidget *parent) - :QWidget(parent) -{ - m_ui = new Ui_UserBox; - m_ui->setupUi(this); - - m_ui->userlist->setItemDelegate(new UserListDelegate(this)); - setOperatorMode(false); - - m_ui->userlist->setSelectionMode(QListView::SingleSelection); - - connect(m_ui->lockButton, &QAbstractButton::clicked, this, &UserList::lockSelected); - connect(m_ui->kickButton, &QAbstractButton::clicked, this, &UserList::kickSelected); - connect(m_ui->banButton, &QAbstractButton::clicked, this, &UserList::kickBanSelected); - connect(m_ui->undoButton, &QAbstractButton::clicked, this, &UserList::undoSelected); - connect(m_ui->redoButton, &QAbstractButton::clicked, this, &UserList::redoSelected); - connect(m_ui->opButton, &QAbstractButton::clicked, this, &UserList::opSelected); - connect(m_ui->muteButton, &QAbstractButton::clicked, this, &UserList::muteSelected); -} - -void UserList::setOperatorMode(bool op) -{ - m_ui->lockButton->setEnabled(op); - m_ui->kickButton->setEnabled(op); - m_ui->banButton->setEnabled(op); - m_ui->undoButton->setEnabled(op); - m_ui->redoButton->setEnabled(op); - m_ui->opButton->setEnabled(op); - m_ui->muteButton->setEnabled(op); -} - -void UserList::setCanvas(canvas::CanvasModel *canvas) -{ - m_canvas = canvas; - m_ui->userlist->setModel(m_canvas->userlist()); - - connect(m_canvas->userlist(), &canvas::UserListModel::dataChanged, this, &UserList::dataChanged); - connect(m_canvas->aclFilter(), &canvas::AclFilter::localOpChanged, this, &UserList::opPrivilegeChanged); - connect(m_ui->userlist->selectionModel(), &QItemSelectionModel::selectionChanged, this, &UserList::selectionChanged); -} - -QModelIndex UserList::currentSelection() -{ - QModelIndexList sel = m_ui->userlist->selectionModel()->selectedIndexes(); - if(sel.isEmpty()) - return QModelIndex(); - return sel.first(); -} - -void UserList::lockSelected() -{ - QModelIndex idx = currentSelection(); - if(idx.isValid()) { - const int id = idx.data().value().id; - bool lock = m_ui->lockButton->isChecked(); - - emit opCommand(m_canvas->userlist()->getLockUserCommand(m_canvas->localUserId(), id, lock)); - } -} - -void UserList::kickSelected() -{ - QModelIndex idx = currentSelection(); - if(idx.isValid()) { - emit opCommand(net::command::kick(idx.data().value().id, false)); - } -} - -void UserList::kickBanSelected() -{ - QModelIndex idx = currentSelection(); - if(idx.isValid()) { - emit opCommand(net::command::kick(idx.data().value().id, true)); - } -} - -void UserList::muteSelected() -{ - QModelIndex idx = currentSelection(); - if(idx.isValid()) { - const int id = idx.data().value().id; - const bool mute = m_ui->muteButton->isChecked(); - emit opCommand(net::command::mute(id, mute)); - } -} - -void UserList::undoSelected() -{ - QModelIndex idx = currentSelection(); - if(idx.isValid()) - emit opCommand(protocol::MessagePtr(new protocol::Undo(m_canvas->localUserId(), idx.data().value().id, false))); -} - -void UserList::redoSelected() -{ - QModelIndex idx = currentSelection(); - if(idx.isValid()) - emit opCommand(protocol::MessagePtr(new protocol::Undo(m_canvas->localUserId(), idx.data().value().id, true))); -} - -void UserList::opSelected() -{ - QModelIndex idx = currentSelection(); - if(idx.isValid()) { - const int id = idx.data().value().id; - bool op = m_ui->opButton->isChecked(); - - emit opCommand(m_canvas->userlist()->getOpUserCommand(m_canvas->localUserId(), id, op)); - } -} - -void UserList::selectionChanged(const QItemSelection &selected) -{ - bool on = selected.count() > 0; - - setOperatorMode(on && m_canvas->aclFilter()->isLocalUserOperator() && m_canvas->isOnline()); - - if(on) { - QModelIndex cs = currentSelection(); - dataChanged(cs,cs); - } -} - -void UserList::opPrivilegeChanged() -{ - bool on = currentSelection().isValid(); - setOperatorMode(on && m_canvas->aclFilter()->isLocalUserOperator() && m_canvas->isOnline()); -} - -void UserList::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) -{ - const int myRow = currentSelection().row(); - if(topLeft.row() <= myRow && myRow <= bottomRight.row()) { - const canvas::User &user = currentSelection().data().value(); - m_ui->lockButton->setChecked(user.isLocked); - m_ui->opButton->setChecked(user.isOperator); - m_ui->muteButton->setChecked(user.isMuted); - if(user.isLocal) { - m_ui->kickButton->setEnabled(false); - m_ui->banButton->setEnabled(false); - m_ui->opButton->setEnabled(false); - } - } -} - -UserListDelegate::UserListDelegate(QObject *parent) - : QItemDelegate(parent), - m_lockicon(icon::fromTheme("object-locked").pixmap(16, 16)), - m_opicon(icon::fromTheme("irc-operator").pixmap(16, 16)), - m_muteicon(icon::fromTheme("irc-unvoice").pixmap(16, 16)), - m_authicon(icon::fromTheme("im-user").pixmap(16, 16)) -{ -} - -void UserListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - QStyleOptionViewItem opt = setOptions(index, option); - painter->save(); - - const canvas::User user = index.data().value(); - - // Background - drawBackground(painter, opt, index); - - // (auth) + [OP/MOD] + Name - QRect textrect = opt.rect; - const QSize locksize = m_lockicon.size(); - { - QFontMetrics fm(opt.font); - if(user.isMod) { - const QString modmsg = QStringLiteral("MOD"); - opt.font.setBold(true); - drawDisplay(painter, opt, textrect, modmsg); - opt.font.setBold(false); - textrect.moveLeft(textrect.left() + qMax(m_opicon.width()*2, fm.width(modmsg)) + 2); - - } else { - if(user.isAuth) - painter->drawPixmap(QRect(textrect.topLeft(), m_authicon.size()), m_authicon); - - textrect.moveLeft(textrect.left() + m_authicon.width() + 2); - - if(user.isOperator) - painter->drawPixmap(QRect(textrect.topLeft(), m_opicon.size()), m_opicon); - - textrect.moveLeft(textrect.left() + m_opicon.width() + 2); - } - } - - if(user.isLocal) - opt.font.setStyle(QFont::StyleItalic); - - drawDisplay(painter, opt, textrect, user.name); - - // Lock indicator - if(user.isLocked) - painter->drawPixmap( - opt.rect.topRight()-QPoint(locksize.width(), -opt.rect.height()/2+locksize.height()/2), - m_lockicon - ); - - // Mute indicator - if(user.isMuted) - painter->drawPixmap( - opt.rect.topRight()-QPoint(locksize.width()*2, -opt.rect.height()/2+locksize.height()/2), - m_muteicon - ); - - painter->restore(); -} - -QSize UserListDelegate::sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index ) const -{ - QSize size = QItemDelegate::sizeHint(option, index); - const QSize iconsize = m_lockicon.size(); - if(size.height() < iconsize.height()) - size.setHeight(iconsize.height()); - return size; -} - -} diff --git a/src/desktop/widgets/userlistwidget.h b/src/desktop/widgets/userlistwidget.h deleted file mode 100644 index c12176b08..000000000 --- a/src/desktop/widgets/userlistwidget.h +++ /dev/null @@ -1,96 +0,0 @@ -/* - Drawpile - a collaborative drawing program. - - Copyright (C) 2007-2018 Calle Laakkonen - - Drawpile 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 3 of the License, or - (at your option) any later version. - - Drawpile 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 Drawpile. If not, see . -*/ -#ifndef USERLISTWIDGET_H -#define USERLISTWIDGET_H - -#include "../shared/net/message.h" - -#include -#include -#include - -class Ui_UserBox; - -namespace canvas { - class CanvasModel; -} - -namespace widgets { - -/** - * @brief User list window - * A dock widget that displays a list of users, with session administration - * controls. - */ -class UserList: public QWidget -{ -Q_OBJECT -public: - UserList(QWidget *parent=0); - - void setCanvas(canvas::CanvasModel *canvas); - -public slots: - void opPrivilegeChanged(); - -signals: - void opCommand(protocol::MessagePtr msg); - -private slots: - void lockSelected(); - void kickSelected(); - void kickBanSelected(); - void opSelected(); - void undoSelected(); - void redoSelected(); - void muteSelected(); - - void dataChanged(const QModelIndex &topLeft, const QModelIndex & bottomRight); - void selectionChanged(const QItemSelection &selected); - -private: - void setOperatorMode(bool op); - QModelIndex currentSelection(); - - Ui_UserBox *m_ui; - canvas::CanvasModel *m_canvas; -}; - -/** - * A delegate to display a session user with status icons - */ -class UserListDelegate : public QItemDelegate { -Q_OBJECT -public: - UserListDelegate(QObject *parent=0); - - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; - QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index ) const; - -private: - QPixmap m_lockicon; - QPixmap m_opicon; - QPixmap m_muteicon; - QPixmap m_authicon; -}; - - -} - -#endif diff --git a/src/desktop/widgets/viewstatus.cpp b/src/desktop/widgets/viewstatus.cpp index 85e8a4435..bb2346a76 100644 --- a/src/desktop/widgets/viewstatus.cpp +++ b/src/desktop/widgets/viewstatus.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2008-2015 Calle Laakkonen + Copyright (C) 2008-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,183 +18,115 @@ */ #include "viewstatus.h" -#include "utils/icon.h" -#include -#include +#include +#include +#include +#include #include -#include -#include namespace widgets { ViewStatus::ViewStatus(QWidget *parent) : QWidget(parent) { -#ifdef Q_OS_MAC - setStyleSheet(QStringLiteral( - "QToolButton { border: none }" - "QToolButton:checked, QToolButton:pressed { background: #c0c0c0 }" - )); -#endif setMinimumHeight(22); QHBoxLayout *layout = new QHBoxLayout(this); layout->setMargin(1); layout->setSpacing(0); - // View flipping - layout->addSpacing(10); - _viewFlip = new QToolButton(this); - _viewFlip->setAutoRaise(true); - - _viewMirror = new QToolButton(this); - _viewMirror->setAutoRaise(true); - - layout->addWidget(_viewFlip); - layout->addWidget(_viewMirror); - - // Rotation angle - layout->addSpacing(10); - _resetRotation = new QToolButton(this); - _resetRotation->setAutoRaise(true); - - auto *rotateLeft = new QToolButton(this); - rotateLeft->setAutoRaise(true); - rotateLeft->setIcon(icon::fromTheme("object-rotate-left")); - connect(rotateLeft, &QToolButton::clicked, this, &ViewStatus::rotateLeft); - - auto *rotateRight = new QToolButton(this); - rotateRight->setAutoRaise(true); - rotateRight->setIcon(icon::fromTheme("object-rotate-right")); - connect(rotateRight, &QToolButton::clicked, this, &ViewStatus::rotateRight); - - _angle = new QLabel(QString::fromUtf8("0°")); - _angle->setFixedWidth(_angle->fontMetrics().width("9999.9")); - _angle->setContextMenuPolicy(Qt::ActionsContextMenu); - - _angleSlider = new QSlider(Qt::Horizontal, this); - _angleSlider->setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum)); - _angleSlider->setMinimum(-360); - _angleSlider->setMaximum(360); - _angleSlider->setPageStep(45); - _angleSlider->setContextMenuPolicy(Qt::ActionsContextMenu); - connect(_angleSlider, &QSlider::valueChanged, [this](int val) { emit angleChanged(val); }); - - _angleSlider->setToolTip(tr("Drag the view while holding ctrl-space to rotate")); - - layout->addWidget(_resetRotation); - layout->addWidget(rotateLeft); - layout->addWidget(_angleSlider); - layout->addWidget(rotateRight); - layout->addWidget(_angle); - - addAngleShortcut(-180); - addAngleShortcut(-135); - addAngleShortcut(-90); - addAngleShortcut(-45); - addAngleShortcut(0); - addAngleShortcut(45); - addAngleShortcut(90); - addAngleShortcut(135); - addAngleShortcut(180); + // Canvas rotation + m_angleBox = new QComboBox(this); + m_angleBox->setFixedWidth(m_angleBox->fontMetrics().width("9999-O--")); + m_angleBox->setFrame(false); + m_angleBox->setEditable(true); + m_angleBox->setToolTip(tr("Canvas Rotation")); + + auto boxPalette = m_angleBox->palette(); + boxPalette.setColor(QPalette::Base, boxPalette.color(QPalette::Window)); + m_angleBox->setPalette(boxPalette); + + layout->addWidget(m_angleBox); + + m_angleBox->addItem(QStringLiteral("-90°")); + m_angleBox->addItem(QStringLiteral("-45°")); + m_angleBox->addItem(QStringLiteral("0°")); + m_angleBox->addItem(QStringLiteral("45°")); + m_angleBox->addItem(QStringLiteral("90°")); + m_angleBox->setEditText(QStringLiteral("0°")); + + m_angleBox->lineEdit()->setValidator( + new QRegularExpressionValidator( + QRegularExpression("-?[0-9]{0,3}°?"), + this + ) + ); + connect(m_angleBox, &QComboBox::editTextChanged, this, &ViewStatus::angleBoxChanged); // Zoom level - _zoomIn = new QToolButton(this); - _zoomIn->setAutoRaise(true); - _zoomOut = new QToolButton(this); - _zoomOut->setAutoRaise(true); - _zoomOriginal = new QToolButton(this); - _zoomOriginal->setAutoRaise(true); - - - _zoomSlider = new QSlider(Qt::Horizontal, this); - _zoomSlider->setMaximumWidth(120); - _zoomSlider->setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum)); - _zoomSlider->setMinimum(50); - _zoomSlider->setMaximum(1600); - _zoomSlider->setPageStep(50); - _zoomSlider->setValue(100); - _zoomSlider->setContextMenuPolicy(Qt::ActionsContextMenu); - connect(_zoomSlider, &QSlider::valueChanged, [this](int val) { emit zoomChanged(val); }); - - _zoom = new QLabel("100%", this); - _zoom->setFixedWidth(_zoom->fontMetrics().width("9999.9%")); - _zoom->setContextMenuPolicy(Qt::ActionsContextMenu); - - layout->addWidget(_zoomOriginal); - layout->addWidget(_zoomOut); - layout->addWidget(_zoomSlider); - layout->addWidget(_zoomIn); - layout->addWidget(_zoom); - - addZoomShortcut(50); - addZoomShortcut(100); - addZoomShortcut(200); - addZoomShortcut(400); + m_zoomBox = new QComboBox(this); + m_zoomBox->setFixedWidth(m_zoomBox->fontMetrics().width("9999.9%--")); + m_zoomBox->setFrame(false); + m_zoomBox->setEditable(true); + m_zoomBox->setToolTip(tr("Zoom")); + + m_zoomBox->setPalette(boxPalette); + + layout->addWidget(m_zoomBox); + + m_zoomBox->addItem(QStringLiteral("50%")); + m_zoomBox->addItem(QStringLiteral("100%")); + m_zoomBox->addItem(QStringLiteral("200%")); + m_zoomBox->addItem(QStringLiteral("400%")); + m_zoomBox->addItem(QStringLiteral("800%")); + m_zoomBox->addItem(QStringLiteral("1600%")); + m_zoomBox->setEditText(QStringLiteral("100%")); + + m_zoomBox->lineEdit()->setValidator( + new QRegularExpressionValidator( + QRegularExpression("[0-9]{0,4}%?"), + this + ) + ); + connect(m_zoomBox, &QComboBox::editTextChanged, this, &ViewStatus::zoomBoxChanged); } -void ViewStatus::setZoomActions(QAction *zoomIn, QAction *zoomOut, QAction *zoomOriginal) +void ViewStatus::setTransformation(qreal zoom, qreal angle) { - _zoomIn->setDefaultAction(zoomIn); - _zoomOut->setDefaultAction(zoomOut); - _zoomOriginal->setDefaultAction(zoomOriginal); -} + const int intZoom = qRound(zoom); + const int zoomCursorPos = m_zoomBox->lineEdit()->cursorPosition(); + m_zoomBox->setEditText(QString::number(intZoom) + QChar('%')); + m_zoomBox->lineEdit()->setCursorPosition(zoomCursorPos); -void ViewStatus::setRotationActions(QAction *resetRotation) -{ - _resetRotation->setDefaultAction(resetRotation); - // Currently there are no external actions for rotation buttons -} + const int intAngle = qRound(angle); + const int angleCursorPos = m_angleBox->lineEdit()->cursorPosition(); + m_angleBox->setEditText(QString::number(intAngle) + QChar(0x00b0)); + m_angleBox->lineEdit()->setCursorPosition(angleCursorPos); -void ViewStatus::setFlipActions(QAction *flip, QAction *mirror) -{ - _viewFlip->setDefaultAction(flip); - _viewMirror->setDefaultAction(mirror); -} -void ViewStatus::addZoomShortcut(int zoomLevel) -{ - QAction *a = new QAction(QString("%1%").arg(zoomLevel), this); - _zoom->addAction(a); - _zoomSlider->addAction(a); - connect(a, &QAction::triggered, [this, zoomLevel]() { - emit zoomChanged(zoomLevel); - }); } -void ViewStatus::addAngleShortcut(int angle) +void ViewStatus::zoomBoxChanged(const QString &text) { - QAction *a = new QAction(QString("%1°").arg(angle), this); - _angle->addAction(a); - _angleSlider->addAction(a); - connect(a, &QAction::triggered, [this, angle]() { - emit angleChanged(angle); - }); -} + const int suffix = text.indexOf('%'); + const QStringRef num = suffix>0 ? text.leftRef(suffix) : &text; -void ViewStatus::setTransformation(qreal zoom, qreal angle) -{ - _zoomSlider->setValue(zoom); - _angleSlider->setValue(angle); - _zoom->setText(QString::number(zoom, 'f', 0) + "%"); - _angle->setText(QString::number(angle, 'f', 1) + QChar(0xb0)); + bool ok; + const int number = num.toInt(&ok); + if(ok && number>= 1 && number < 10000) + emit zoomChanged(number); } -void ViewStatus::rotateLeft() +void ViewStatus::angleBoxChanged(const QString &text) { - int a = _angleSlider->value() - 10; - if(a < _angleSlider->minimum()) - a = _angleSlider->maximum() - 10; - _angleSlider->setValue(a); -} + const int suffix = text.indexOf(0x00b0); + const QStringRef num = suffix>0 ? text.leftRef(suffix) : &text; -void ViewStatus::rotateRight() -{ - int a = _angleSlider->value() + 10; - if(a > _angleSlider->maximum()) - a = _angleSlider->minimum() + 10; - _angleSlider->setValue(a); + bool ok; + const int number = num.toInt(&ok); + if(ok) + emit angleChanged(number); } } diff --git a/src/desktop/widgets/viewstatus.h b/src/desktop/widgets/viewstatus.h index 5a65c52ab..535b95c14 100644 --- a/src/desktop/widgets/viewstatus.h +++ b/src/desktop/widgets/viewstatus.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2008-2015 Calle Laakkonen + Copyright (C) 2008-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,21 +21,15 @@ #include -class QLabel; -class QSlider; -class QToolButton; +class QComboBox; namespace widgets { class ViewStatus : public QWidget { -Q_OBJECT + Q_OBJECT public: - ViewStatus(QWidget *parent=0); - - void setZoomActions(QAction *zoomIn, QAction *zoomOut, QAction *zoomOriginal); - void setRotationActions(QAction *resetRotation); - void setFlipActions(QAction *flip, QAction *mirror); + ViewStatus(QWidget *parent=nullptr); public slots: void setTransformation(qreal zoom, qreal angle); @@ -45,17 +39,12 @@ public slots: void angleChanged(qreal newAngle); private slots: - void rotateLeft(); - void rotateRight(); + void zoomBoxChanged(const QString &text); + void angleBoxChanged(const QString &text); private: - void addZoomShortcut(int zoomLevel); - void addAngleShortcut(int angle); - - QSlider *_zoomSlider, *_angleSlider; - QLabel *_zoom, *_angle; - QToolButton *_zoomIn, *_zoomOut, *_zoomOriginal, *_resetRotation; - QToolButton *_viewFlip, *_viewMirror; + QComboBox *m_zoomBox; + QComboBox *m_angleBox; }; } diff --git a/src/server/gui/serversummarypage.cpp b/src/server/gui/serversummarypage.cpp index fbfbebd9d..c18371bb0 100644 --- a/src/server/gui/serversummarypage.cpp +++ b/src/server/gui/serversummarypage.cpp @@ -59,6 +59,7 @@ struct ServerSummaryPage::Private { QCheckBox *allowGuestHosts; QDoubleSpinBox *sessionSizeLimit; + QDoubleSpinBox *autoresetTreshold; QDoubleSpinBox *idleTimeout; QSpinBox *maxSessions; QCheckBox *persistence; @@ -86,6 +87,7 @@ struct ServerSummaryPage::Private { allowGuests(new QCheckBox), allowGuestHosts(new QCheckBox), sessionSizeLimit(new QDoubleSpinBox), + autoresetTreshold(new QDoubleSpinBox), idleTimeout(new QDoubleSpinBox), maxSessions(new QSpinBox), persistence(new QCheckBox), @@ -106,6 +108,8 @@ struct ServerSummaryPage::Private { sessionSizeLimit->setSuffix(" MB"); sessionSizeLimit->setSpecialValueText(tr("unlimited")); + autoresetTreshold->setSuffix(" MB"); + autoresetTreshold->setSpecialValueText(tr("none")); idleTimeout->setSuffix(" min"); idleTimeout->setSingleStep(1); idleTimeout->setSpecialValueText(tr("unlimited")); @@ -216,6 +220,7 @@ ServerSummaryPage::ServerSummaryPage(Server *server, QWidget *parent) layout->addItem(new QSpacerItem(1,10), row++, 0); addWidgets(d, layout, row++, tr("Session size limit"), d->sessionSizeLimit, true); + addWidgets(d, layout, row++, tr("Default autoreset threshold"), d->autoresetTreshold, true); addWidgets(d, layout, row++, tr("Session idle timeout"), d->idleTimeout, true); addWidgets(d, layout, row++, tr("Maximum sessions"), d->maxSessions, true); addWidgets(d, layout, row++, QString(), d->persistence); @@ -291,6 +296,7 @@ void ServerSummaryPage::handleResponse(const QString &requestId, const JsonApiRe d->allowGuestHosts->setChecked(o[config::AllowGuestHosts.name].toBool()); d->sessionSizeLimit->setValue(o[config::SessionSizeLimit.name].toDouble() / (1024*1024)); + d->autoresetTreshold->setValue(o[config::AutoresetThreshold.name].toDouble() / (1024*1024)); d->idleTimeout->setValue(o[config::IdleTimeLimit.name].toDouble() / 60); d->maxSessions->setValue(o[config::SessionCountLimit.name].toInt()); d->logPurge->setValue(o[config::LogPurgeDays.name].toInt()); @@ -319,6 +325,7 @@ void ServerSummaryPage::saveSettings() {config::AllowGuests.name, d->allowGuests->isChecked()}, {config::AllowGuestHosts.name, d->allowGuestHosts->isChecked()}, {config::SessionSizeLimit.name, d->sessionSizeLimit->value() * 1024 * 1024}, + {config::AutoresetThreshold.name, d->autoresetTreshold->value() * 1024 * 1024}, {config::IdleTimeLimit.name, d->idleTimeout->value() * 60}, {config::SessionCountLimit.name, d->maxSessions->value()}, {config::LogPurgeDays.name, d->logPurge->value()}, diff --git a/src/server/multiserver.cpp b/src/server/multiserver.cpp index 28adfad65..b3f30c824 100644 --- a/src/server/multiserver.cpp +++ b/src/server/multiserver.cpp @@ -29,8 +29,6 @@ #include "../shared/server/serverconfig.h" #include "../shared/server/serverlog.h" -#include "../shared/util/announcementapi.h" - #include #include #include @@ -369,6 +367,7 @@ JsonApiResult MultiServer::serverJsonApi(JsonApiMethod method, const QStringList const ConfigKey settings[] = { config::ClientTimeout, config::SessionSizeLimit, + config::AutoresetThreshold, config::SessionCountLimit, config::EnablePersistence, config::ArchiveMode, diff --git a/src/server/templatefiles.cpp b/src/server/templatefiles.cpp index dccd98ca1..6ef243e40 100644 --- a/src/server/templatefiles.cpp +++ b/src/server/templatefiles.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2017 Calle Laakkonen + Copyright (C) 2017-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -139,6 +139,8 @@ bool TemplateFiles::init(SessionHistory *session) const flags |= SessionHistory::Persistent; if(reader.metadata().value("preserveChat").toBool()) flags |= SessionHistory::PreserveChat; + if(reader.metadata().value("deputies").toBool()) + flags |= SessionHistory::Deputies; session->setFlags(flags); // Set initial history @@ -147,10 +149,10 @@ bool TemplateFiles::init(SessionHistory *session) const recording::MessageRecord r = reader.readNext(); switch(r.status) { case recording::MessageRecord::OK: - session->addMessage(protocol::MessagePtr(r.message)); + session->addMessage(protocol::MessagePtr::fromNullable(r.message)); break; case recording::MessageRecord::INVALID: - qWarning("%s: Invalid message (type %d, len %d) in template!", qPrintable(session->idAlias()), r.error.type, r.error.len); + qWarning("%s: Invalid message (type %d, len %d) in template!", qPrintable(session->idAlias()), r.invalid_type, r.invalid_len); break; case recording::MessageRecord::END_OF_RECORDING: keepReading = false; diff --git a/src/server/tests/test.dptxt b/src/server/tests/test.dptxt index 7291fdb04..082d21d8c 100644 --- a/src/server/tests/test.dptxt +++ b/src/server/tests/test.dptxt @@ -1,4 +1,4 @@ -!version=dp:4.20.1 +!version=dp:4.21.2 !maxUserCount=1 !founder=tester !password=plain;qwerty123 diff --git a/src/shared/CMakeLists.txt b/src/shared/CMakeLists.txt index 1cd361107..8214bfa53 100644 --- a/src/shared/CMakeLists.txt +++ b/src/shared/CMakeLists.txt @@ -7,7 +7,7 @@ set ( net/message.cpp net/annotation.cpp net/layer.cpp - net/pen.cpp + net/brushes.cpp net/image.cpp net/annotation.cpp net/control.cpp diff --git a/src/shared/net/annotation.h b/src/shared/net/annotation.h index 94caa3b14..188447354 100644 --- a/src/shared/net/annotation.h +++ b/src/shared/net/annotation.h @@ -51,6 +51,9 @@ class AnnotationCreate : public Message { */ uint16_t id() const { return m_id; } + //! Alias for id() + uint16_t layer() const override { return m_id; } + int32_t x() const { return m_x; } int32_t y() const { return m_y; } uint16_t w() const { return m_w; } diff --git a/src/shared/net/brushes.cpp b/src/shared/net/brushes.cpp new file mode 100644 index 000000000..40752d44a --- /dev/null +++ b/src/shared/net/brushes.cpp @@ -0,0 +1,456 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#include "brushes.h" +#include "textmode.h" + +#include +#include + +namespace protocol { + +DrawDabsClassic *DrawDabsClassic::deserialize(uint8_t ctx, const uchar *data, uint len) +{ + if(len < 15) + return nullptr; + + const int dabCount = (len-15) / ClassicBrushDab::LENGTH; + if(uint(dabCount * ClassicBrushDab::LENGTH + 15) != len) + return nullptr; + + DrawDabsClassic *d = new DrawDabsClassic( + ctx, + qFromBigEndian(data+0), + qFromBigEndian(data+2), + qFromBigEndian(data+6), + qFromBigEndian(data+10), + *(data+14) + ); + d->m_dabs.reserve(dabCount); + + data += 15; + + for(int i=0;im_dabs << ClassicBrushDab { + int8_t(*(data+0)), + int8_t(*(data+1)), + qFromBigEndian(data+2), + *(data+4), + *(data+5) + }; + data += ClassicBrushDab::LENGTH; + } + + return d; +} + +int DrawDabsClassic::payloadLength() const +{ + return 2 + 4*3 + 1 + m_dabs.size() * ClassicBrushDab::LENGTH; +} + +int DrawDabsClassic::serializePayload(uchar *data) const +{ + Q_ASSERT(m_dabs.size() <= MAX_DABS); + + uchar *ptr = data; + qToBigEndian(m_layer, ptr); ptr += 2; + qToBigEndian(m_x, ptr); ptr += 4; + qToBigEndian(m_y, ptr); ptr += 4; + qToBigEndian(m_color, ptr); ptr += 4; + *(ptr++) = m_mode; + + for(const ClassicBrushDab &d : m_dabs) { + *(ptr++) = d.x; + *(ptr++) = d.y; + qToBigEndian(d.size, ptr); ptr += 2; + *(ptr++) = d.hardness; + *(ptr++) = d.opacity; + } + + return ptr-data; +} + +bool DrawDabsClassic::payloadEquals(const Message &m) const +{ + const auto &o = static_cast(m); + if(m_dabs.size() != o.m_dabs.size()) + return false; + + if( + m_x != o.m_x || + m_y != o.m_y || + m_color != o.m_color || + m_layer != o.m_layer || + m_mode != o.m_mode + ) + return false; + + for(int i=0;i(dabs); + + if(m_color != ddc.m_color || + m_layer != ddc.m_layer || + m_mode != ddc.m_mode) + return false; + + const int newLength = ddc.dabs().length() + m_dabs.length(); + if(newLength > MAX_DABS) + return false; + + int lastX = m_x; + int lastY = m_y; + for(const auto dab : m_dabs) { + lastX += dab.x; + lastY += dab.y; + } + + auto dab = ddc.dabs().first(); + + const int offsetX = ddc.originX() - lastX + dab.x; + const int offsetY = ddc.originY() - lastY + dab.y; + + if(qAbs(offsetX) > ClassicBrushDab::MAX_XY_DELTA || + qAbs(offsetY) > ClassicBrushDab::MAX_XY_DELTA) + return false; + + m_dabs.reserve(newLength); + + dab.x = offsetX; + dab.y = offsetY; + m_dabs << dab; + + for(int i=1;i(data+0), + qFromBigEndian(data+2), + qFromBigEndian(data+6), + qFromBigEndian(data+10), + *(data+14) + ); + d->m_dabs.reserve(dabCount); + + data += 15; + + for(int i=0;im_dabs << PixelBrushDab { + int8_t(*(data+0)), + int8_t(*(data+1)), + *(data+2), + *(data+3) + }; + data += PixelBrushDab::LENGTH; + } + + return d; +} + +int DrawDabsPixel::payloadLength() const +{ + return 2 + 4*3 + 1 + m_dabs.size() * PixelBrushDab::LENGTH; +} + +int DrawDabsPixel::serializePayload(uchar *data) const +{ + Q_ASSERT(m_dabs.size() <= MAX_DABS); + + uchar *ptr = data; + qToBigEndian(m_layer, ptr); ptr += 2; + qToBigEndian(m_x, ptr); ptr += 4; + qToBigEndian(m_y, ptr); ptr += 4; + qToBigEndian(m_color, ptr); ptr += 4; + *(ptr++) = m_mode; + + for(const PixelBrushDab &d : m_dabs) { + *(ptr++) = d.x; + *(ptr++) = d.y; + *(ptr++) = d.size; + *(ptr++) = d.opacity; + } + + return ptr-data; +} + +bool DrawDabsPixel::payloadEquals(const Message &m) const +{ + const auto &o = static_cast(m); + if(m_dabs.size() != o.m_dabs.size()) + return false; + + if( + m_x != o.m_x || + m_y != o.m_y || + m_color != o.m_color || + m_layer != o.m_layer || + m_mode != o.m_mode + ) + return false; + + for(int i=0;i(dabs); + + if(m_color != ddp.m_color || + m_layer != ddp.m_layer || + m_mode != ddp.m_mode) + return false; + + const int newLength = ddp.dabs().length() + m_dabs.length(); + if(newLength > MAX_DABS) + return false; + + int lastX = m_x; + int lastY = m_y; + for(const auto dab : m_dabs) { + lastX += dab.x; + lastY += dab.y; + } + + auto dab = ddp.dabs().first(); + + const int offsetX = ddp.originX() - lastX + dab.x; + const int offsetY = ddp.originY() - lastY + dab.y; + + if(qAbs(offsetX) > ClassicBrushDab::MAX_XY_DELTA || + qAbs(offsetY) > ClassicBrushDab::MAX_XY_DELTA) + return false; + + m_dabs.reserve(newLength); + + dab.x = offsetX; + dab.y = offsetY; + m_dabs << dab; + + for(int i=1;i. +*/ +#ifndef DP_NET_BRUSHES_H +#define DP_NET_BRUSHES_H + +#include "message.h" + +#include +#include + +class QRect; + +namespace protocol { + struct ClassicBrushDab { + int8_t x; // coordinates are relative to previous dab + int8_t y; // (or origin if this is the first dab.) + uint16_t size; // diameter multiplied by 256 + uint8_t hardness; + uint8_t opacity; + + static const int MAX_XY_DELTA = INT8_MAX; + static const int LENGTH = 6; + QString toString() const; + + bool operator!=(const ClassicBrushDab &o) const { + return x != o.x || y != o.y || size != o.size || hardness != o.hardness || opacity != o.opacity; + } + }; + + struct PixelBrushDab { + int8_t x; // coordinates are relative to the previous dab + int8_t y; // (or origin if this is the first dab) + uint8_t size; + uint8_t opacity; + + static const int MAX_XY_DELTA = INT8_MAX; + static const int LENGTH = 4; + QString toString() const; + + bool operator!=(const PixelBrushDab &o) const { + return x != o.x || y != o.y || size != o.size || opacity != o.opacity; + } + }; +} + +Q_DECLARE_TYPEINFO(protocol::ClassicBrushDab, Q_PRIMITIVE_TYPE); +Q_DECLARE_TYPEINFO(protocol::PixelBrushDab, Q_PRIMITIVE_TYPE); + +namespace protocol { + +typedef QVector ClassicBrushDabVector; +typedef QVector PixelBrushDabVector; + +enum class DabShape { + Round, + Square +}; + +/** + * @brief Abstract base class for DrawDabs* messages + */ +class DrawDabs : public Message { +public: + DrawDabs(MessageType type, uint8_t context) + : Message(type, context) + { } + + //! Should these dabs be composited in indirect mode? + virtual bool isIndirect() const = 0; + + //! Get the last coordinates of the last point in the dab vector + virtual QPoint lastPoint() const = 0; + + //! Get the bounding rectangle of the dab vector + virtual QRect bounds() const = 0; + + /** + * @brief Append the given dab message's dabs to this message's dab vector. + * + * If the extension is not possible, this function returns false and + * does not modify the current dab vector. + * + * Possible reasons why extension may fail: + * - given DrawDabs instance is of the wrong type + * - common properties are not the same + * - summed dab array length would be too long + * - distance between lastPoint() and dab.originXY is greater than MAX_XY_DELTA + */ + virtual bool extend(const DrawDabs &dab) = 0; +}; + +/** + * @brief Draw Classic Brush Dabs + * + */ +class DrawDabsClassic : public DrawDabs { +public: + static const int MAX_DABS = (0xffff - 15) / ClassicBrushDab::LENGTH; + + DrawDabsClassic( + uint8_t ctx, + uint16_t layer, + int32_t originX, int32_t originY, + uint32_t color, + uint8_t blend, + const ClassicBrushDabVector &dabs=ClassicBrushDabVector() + ) + : DrawDabs(MSG_DRAWDABS_CLASSIC, ctx), + m_dabs(dabs), + m_x(originX), m_y(originY), + m_color(color), + m_layer(layer), + m_mode(blend) + { + Q_ASSERT(dabs.size() <= MAX_DABS); + } + + static DrawDabsClassic *deserialize(uint8_t ctx, const uchar *data, uint len); + static DrawDabsClassic *fromText(uint8_t ctx, const Kwargs &kwargs, const QStringList &dabs); + + uint16_t layer() const override { return m_layer; } + int32_t originX() const { return m_x; } // Classic dab coordinates have subpixel precision. + int32_t originY() const { return m_y; } // They are converted to integers by multiplying by 4 + uint32_t color() const { return m_color; } + uint8_t mode() const { return m_mode; } + + // If the color's alpha channel is nonzero, that value is used + // as the opacity of the entire stroke. + bool isIndirect() const override { return (m_color & 0xff000000) > 0; } + + const ClassicBrushDabVector &dabs() const { return m_dabs; } + ClassicBrushDabVector &dabs() { return m_dabs; } + + QString toString() const override; + QString messageName() const override { return QStringLiteral("classicdabs"); } + + QPoint lastPoint() const override; + QRect bounds() const override; + bool extend(const DrawDabs &dab) override; + +protected: + int payloadLength() const override; + int serializePayload(uchar *data) const override; + bool payloadEquals(const Message &m) const override; + Kwargs kwargs() const override { return Kwargs(); } + +private: + ClassicBrushDabVector m_dabs; + int32_t m_x, m_y; + uint32_t m_color; + uint16_t m_layer; + uint8_t m_mode; +}; + + +/** + * @brief Draw Pixel Brush Dabs + * + */ +class DrawDabsPixel : public DrawDabs { +public: + static const int MAX_DABS = (0xffff - 15) / PixelBrushDab::LENGTH; + + DrawDabsPixel( + DabShape shape, + uint8_t ctx, + uint16_t layer, + int32_t originX, int32_t originY, + uint32_t color, + uint8_t blend, + const PixelBrushDabVector &dabs=PixelBrushDabVector() + ) + : DrawDabs(shape == DabShape::Square ? MSG_DRAWDABS_PIXEL_SQUARE : MSG_DRAWDABS_PIXEL, ctx), + m_dabs(dabs), + m_x(originX), m_y(originY), + m_color(color), + m_layer(layer), + m_mode(blend) + { + Q_ASSERT(dabs.size() <= MAX_DABS); + } + + static DrawDabsPixel *deserialize(DabShape shape, uint8_t ctx, const uchar *data, uint len); + static DrawDabsPixel *fromText(DabShape shape, uint8_t ctx, const Kwargs &kwargs, const QStringList &dabs); + + uint16_t layer() const override { return m_layer; } + int32_t originX() const { return m_x; } + int32_t originY() const { return m_y; } + uint32_t color() const { return m_color; } // If the alpha channel is set, the dabs are composited indirectly + uint8_t mode() const { return m_mode; } + bool isSquare() const { return type() == MSG_DRAWDABS_PIXEL_SQUARE; } + + // If the color's alpha channel is nonzero, that value is used + // as the opacity of the entire stroke. + bool isIndirect() const override { return (m_color & 0xff000000) > 0; } + + const PixelBrushDabVector &dabs() const { return m_dabs; } + PixelBrushDabVector &dabs() { return m_dabs; } + + QString toString() const override; + QString messageName() const override { return isSquare() ? QStringLiteral("squarepixeldabs") : QStringLiteral("pixeldabs"); } + + QPoint lastPoint() const override; + QRect bounds() const override; + bool extend(const DrawDabs &dab) override; + +protected: + int payloadLength() const override; + int serializePayload(uchar *data) const override; + bool payloadEquals(const Message &m) const override; + Kwargs kwargs() const override { return Kwargs(); } + +private: + PixelBrushDabVector m_dabs; + int32_t m_x, m_y; + uint32_t m_color; + uint16_t m_layer; + uint8_t m_mode; +}; + +/** + * @brief Pen up command + * + * The pen up command signals the end of a stroke. In indirect drawing mode, it causes + * indirect dabs (by this user) to be merged to their parent layers. + */ +class PenUp : public ZeroLengthMessage { +public: + PenUp(uint8_t ctx) : ZeroLengthMessage(MSG_PEN_UP, ctx) {} + + QString messageName() const override { return QStringLiteral("penup"); } +}; + +} + +#endif diff --git a/src/shared/net/control.cpp b/src/shared/net/control.cpp index 4a711db1d..1a4d84f8d 100644 --- a/src/shared/net/control.cpp +++ b/src/shared/net/control.cpp @@ -73,6 +73,8 @@ ServerReply ServerReply::fromJson(const QJsonDocument &doc) r.type = STATUS; else if(typestr == "reset") r.type = RESET; + else if(typestr == "autoreset") + r.type = RESETREQUEST; else if(typestr == "catchup") r.type = CATCHUP; else @@ -100,6 +102,7 @@ QJsonDocument ServerReply::toJson() const case STATUS: typestr=QStringLiteral("status"); break; case RESET: typestr=QStringLiteral("reset"); break; case CATCHUP: typestr=QStringLiteral("catchup"); break; + case RESETREQUEST: typestr=QStringLiteral("autoreset"); break; } o["type"] = typestr; diff --git a/src/shared/net/control.h b/src/shared/net/control.h index c4181898d..6fe6d32b3 100644 --- a/src/shared/net/control.h +++ b/src/shared/net/control.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2013-2017 Calle Laakkonen + Copyright (C) 2013-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -52,10 +52,11 @@ struct ServerReply { RESULT, // comand result LOG, // server log message SESSIONCONF, // session configuration update - SIZELIMITWARNING, // session history size nearing limit + SIZELIMITWARNING, // session history size nearing limit (deprecated) STATUS, // Periodic status update RESET, // session reset state - CATCHUP // number of messages queued for upload (use for progress bars) + CATCHUP, // number of messages queued for upload (use for progress bars) + RESETREQUEST // request client to perform a reset } type; QString message; QJsonObject reply; diff --git a/src/shared/net/image.cpp b/src/shared/net/image.cpp index 3490f8f9b..ba0a7c2b4 100644 --- a/src/shared/net/image.cpp +++ b/src/shared/net/image.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2013-2017 Calle Laakkonen + Copyright (C) 2013-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -119,6 +119,178 @@ PutImage *PutImage::fromText(uint8_t ctx, const Kwargs &kwargs) ); } +static QByteArray colorByteArray(quint32 c) +{ + QByteArray ba(4, 0); + qToBigEndian(c, ba.data()); + return ba; +} + +PutTile::PutTile(uint8_t ctx, uint16_t layer, uint8_t sublayer, uint16_t col, uint16_t row, uint16_t repeat, uint32_t color) + : PutTile(ctx, layer, sublayer, col, row, repeat, colorByteArray(color)) +{ +} + +PutTile *PutTile::deserialize(uint8_t ctx, const uchar *data, uint len) +{ + if(len < 13) + return nullptr; + + return new PutTile( + ctx, + qFromBigEndian(data+0), + *(data+2), + qFromBigEndian(data+3), + qFromBigEndian(data+5), + qFromBigEndian(data+7), + QByteArray((const char*)data+9, len-9) + ); +} + +int PutTile::payloadLength() const +{ + return 9 + m_image.length(); +} + +int PutTile::serializePayload(uchar *data) const +{ + uchar *ptr = data; + qToBigEndian(m_layer, ptr); ptr += 2; + *(ptr++) = m_sublayer; + qToBigEndian(m_col, ptr); ptr += 2; + qToBigEndian(m_row, ptr); ptr += 2; + qToBigEndian(m_repeat, ptr); ptr += 2; + + memcpy(ptr, m_image.constData(), m_image.length()); + ptr += m_image.length(); + + return ptr-data; +} + +uint32_t PutTile::color() const +{ + Q_ASSERT(m_image.length()==4); + return qFromBigEndian(m_image.constData()); +} + +bool PutTile::payloadEquals(const Message &m) const +{ + const PutTile &p = static_cast(m); + return + layer() == p.layer() && + sublayer() == p.sublayer() && + column() == p.column() && + row() == p.row() && + repeat() == p.repeat() && + image() == p.image(); +} + +Kwargs PutTile::kwargs() const +{ + Kwargs kw; + kw["layer"] = text::idString(m_layer); + if(m_sublayer>0) + kw["sublayer"] = QString::number(m_sublayer); + kw["row"] = QString::number(m_row); + kw["col"] = QString::number(m_col); + if(m_repeat>0) + kw["repeat"] = QString::number(m_repeat); + if(isSolidColor()) + kw["color"] = text::argbString(color()); + else + kw["img"] = splitToColumns(m_image.toBase64(), 70); + + return kw; +} + +PutTile *PutTile::fromText(uint8_t ctx, const Kwargs &kwargs) +{ + QByteArray img; + if(kwargs.contains("color")) { + img = colorByteArray(text::parseColor(kwargs["color"])); + + } else { + img = QByteArray::fromBase64(kwargs["img"].toUtf8()); + if(img.length()<=4) + return nullptr; + } + + return new PutTile( + ctx, + text::parseIdString16(kwargs["layer"]), + kwargs["sublayer"].toInt(), + kwargs["col"].toInt(), + kwargs["row"].toInt(), + kwargs["repeat"].toInt(), + img + ); +} + +CanvasBackground::CanvasBackground(uint8_t ctx, uint32_t color) + : CanvasBackground(ctx, colorByteArray(color)) +{ +} + +CanvasBackground *CanvasBackground::deserialize(uint8_t ctx, const uchar *data, uint len) +{ + if(len < 4) + return nullptr; + + return new CanvasBackground( + ctx, + QByteArray((const char*)data, len) + ); +} + +int CanvasBackground::payloadLength() const +{ + return m_image.length(); +} + +int CanvasBackground::serializePayload(uchar *data) const +{ + memcpy(data, m_image.constData(), m_image.length()); + return m_image.length(); +} + +uint32_t CanvasBackground::color() const +{ + Q_ASSERT(m_image.length()==4); + return qFromBigEndian(m_image.constData()); +} + +bool CanvasBackground::payloadEquals(const Message &m) const +{ + const CanvasBackground &p = static_cast(m); + return m_image == p.m_image; +} + +Kwargs CanvasBackground::kwargs() const +{ + Kwargs kw; + if(isSolidColor()) + kw["color"] = text::argbString(color()); + else + kw["img"] = splitToColumns(m_image.toBase64(), 70); + + return kw; +} + +CanvasBackground *CanvasBackground::fromText(uint8_t ctx, const Kwargs &kwargs) +{ + QByteArray img; + if(kwargs.contains("color")) { + img = colorByteArray(text::parseColor(kwargs["color"])); + + } else { + img = QByteArray::fromBase64(kwargs["img"].toUtf8()); + if(img.length()<=4) + return nullptr; + } + + return new CanvasBackground(ctx, img); +} + FillRect *FillRect::deserialize(uint8_t ctx, const uchar *data, uint len) { if(len != 23) diff --git a/src/shared/net/image.h b/src/shared/net/image.h index bc099b300..df28fac4c 100644 --- a/src/shared/net/image.h +++ b/src/shared/net/image.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2013-2017 Calle Laakkonen + Copyright (C) 2013-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -35,11 +35,7 @@ namespace protocol { * * All brush/layer blending modes are supported. * - * The image data is DEFLATEd 32bit non-premultiplied ARGB data. - * - * The contextId doesn't affect the way the bitmap is - * drawn, but it is needed to identify the user so PutImages - * can be undone/redone. + * The image data is DEFLATEd 32bit premultiplied ARGB data. * * Note that since the message length is fairly limited, a * large image may have to be divided into multiple PutImage @@ -59,7 +55,7 @@ class PutImage : public Message { static PutImage *deserialize(uint8_t ctx, const uchar *data, uint len); static PutImage *fromText(uint8_t ctx, const Kwargs &kwargs); - uint16_t layer() const { return m_layer; } + uint16_t layer() const override { return m_layer; } uint8_t blendmode() const { return m_mode; } uint32_t x() const { return m_x; } uint32_t y() const { return m_y; } @@ -85,6 +81,126 @@ class PutImage : public Message { QByteArray m_image; }; +/** + * @brief Set the content of a tile + * + * Unlike PutImage, this replaces an entire tile directly without any blending. + * This command is typically used during canvas initialization to set the initial content. + * + * PutTiles can be targeted at sublayers as well. This is used when generating a reset image + * with incomplete indirect strokes. Sending a PenUp command will merge the sublayer. + */ +class PutTile : public Message { +public: + /** + * @brief Construct a solid color PutTile + * @param ctx context ID + * @param layer target layer + * @param sublayer sublayer (0 means no sublayer) + * @param col tile column + * @param row tile row + * @param repeat put this many extra tiles + * @param color tile fill color ARGB (unpremultiplied) + */ + PutTile(uint8_t ctx, uint16_t layer, uint8_t sublayer, uint16_t col, uint16_t row, uint16_t repeat, uint32_t color); + + /** + * @brief Construct a PutTile + * @param ctx context ID + * @param layer target layer + * @param sublayer sublayer (0 means no sublayer) + * @param col tile column + * @param row tile row + * @param repeat put this many extra tiles + * @param image tile content. Uncompressed length must be 64x64x4 + */ + PutTile(uint8_t ctx, uint16_t layer, uint8_t sublayer, uint16_t col, uint16_t row, uint16_t repeat, const QByteArray &image) + : Message(MSG_PUTTILE, ctx), m_layer(layer), m_col(col), m_row(row), m_repeat(repeat), m_sublayer(sublayer), m_image(image) + { + // Note: an uncompressed tile is only 16KB, so this should never be + // anywhere near this long + Q_ASSERT(image.length() <= 0xffff - 8); + Q_ASSERT(image.length() >= 4); + } + + static PutTile *deserialize(uint8_t ctx, const uchar *data, uint len); + static PutTile *fromText(uint8_t ctx, const Kwargs &kwargs); + + uint16_t layer() const override { return m_layer; } + uint8_t sublayer() const { return m_sublayer; } + uint16_t column() const { return m_col; } + uint16_t row() const { return m_row; } + uint16_t repeat() const { return m_repeat; } + uint32_t color() const; + const QByteArray &image() const { return m_image; } + + bool isSolidColor() const { return m_image.length() == 4; } + + QString messageName() const override { return QStringLiteral("puttile"); } + +protected: + int payloadLength() const override; + int serializePayload(uchar *data) const override; + bool payloadEquals(const Message &m) const override; + Kwargs kwargs() const override; + +private: + uint16_t m_layer; + uint16_t m_col; + uint16_t m_row; + uint16_t m_repeat; + uint8_t m_sublayer; + QByteArray m_image; +}; + +/** + * @brief Set the canvas background + * + */ +class CanvasBackground : public Message { +public: + /** + * @brief Construct a solid color background + * @param ctx context ID + * @param color background color ARGB (unpremultiplied) + */ + CanvasBackground(uint8_t ctx, uint32_t color); + + /** + * @brief Construct a pattern background + * @param ctx context ID + * @param image tile content. Uncompressed length must be 64x64x4 + */ + CanvasBackground(uint8_t ctx, const QByteArray &image) + : Message(MSG_CANVAS_BACKGROUND, ctx), m_image(image) + { + // Note: an uncompressed tile is only 16KB, so this should never be + // anywhere near this long + Q_ASSERT(image.length() <= 0xffff - 8); + Q_ASSERT(image.length() >= 4); + } + + static CanvasBackground *deserialize(uint8_t ctx, const uchar *data, uint len); + static CanvasBackground *fromText(uint8_t ctx, const Kwargs &kwargs); + + uint32_t color() const; + const QByteArray &image() const { return m_image; } + + bool isSolidColor() const { return m_image.length() == 4; } + + QString messageName() const override { return QStringLiteral("background"); } + +protected: + int payloadLength() const override; + int serializePayload(uchar *data) const override; + bool payloadEquals(const Message &m) const override; + Kwargs kwargs() const override; + +private: + QByteArray m_image; +}; + + /** * @brief Fill a rectangle with solid color * @@ -100,7 +216,7 @@ class FillRect : public Message { static FillRect *deserialize(uint8_t ctx, const uchar *data, uint len); static FillRect *fromText(uint8_t ctx, const Kwargs &kwargs); - uint16_t layer() const { return m_layer; } + uint16_t layer() const override { return m_layer; } uint8_t blend() const { return m_blend; } uint32_t x() const { return m_x; } uint32_t y() const { return m_y; } @@ -177,7 +293,7 @@ class MoveRegion : public Message { static MoveRegion *deserialize(uint8_t ctx, const uchar *data, uint len); static MoveRegion *fromText(uint8_t ctx, const Kwargs &kwargs); - uint16_t layer() const { return m_layer; } + uint16_t layer() const override { return m_layer; } int32_t bx() const { return m_bx; } int32_t by() const { return m_by; } int32_t bw() const { return m_bw; } diff --git a/src/shared/net/layer.cpp b/src/shared/net/layer.cpp index 335cbf11a..ce113b011 100644 --- a/src/shared/net/layer.cpp +++ b/src/shared/net/layer.cpp @@ -144,19 +144,21 @@ LayerCreate *LayerCreate::fromText(uint8_t ctx, const Kwargs &kwargs) LayerAttributes *LayerAttributes::deserialize(uint8_t ctx, const uchar *data, uint len) { - if(len!=4) - return 0; + if(len!=6) + return nullptr; return new LayerAttributes( ctx, qFromBigEndian(data+0), *(data+2), - *(data+3) + *(data+3), + *(data+4), + *(data+5) ); } int LayerAttributes::payloadLength() const { - return 4; + return 6; } @@ -164,6 +166,8 @@ int LayerAttributes::serializePayload(uchar *data) const { uchar *ptr=data; qToBigEndian(m_id, ptr); ptr += 2; + *(ptr++) = m_sublayer; + *(ptr++) = m_flags; *(ptr++) = m_opacity; *(ptr++) = m_blend; return ptr-data; @@ -172,17 +176,29 @@ int LayerAttributes::serializePayload(uchar *data) const Kwargs LayerAttributes::kwargs() const { Kwargs kw; - kw["id"] = text::idString(m_id); + kw["layer"] = text::idString(m_id); + if(m_sublayer>0) + kw["sublayer"] = QString::number(m_sublayer); kw["opacity"] = text::decimal(m_opacity); kw["blend"] = QString::number(m_blend); + + QStringList flags; + if((m_flags&FLAG_CENSOR)) + flags << "censor"; + if(!flags.isEmpty()) + kw["flags"] = flags.join(','); + return kw; } LayerAttributes *LayerAttributes::fromText(uint8_t ctx, const Kwargs &kwargs) { + QStringList flags = kwargs["flags"].split(','); return new LayerAttributes( ctx, - text::parseIdString16(kwargs["id"]), + text::parseIdString16(kwargs["layer"]), + kwargs["sublayer"].toInt(), + flags.contains("censor") ? FLAG_CENSOR : 0, text::parseDecimal8(kwargs["opacity"]), kwargs["blend"].toInt() ); diff --git a/src/shared/net/layer.h b/src/shared/net/layer.h index 5d4ef739b..542c6958e 100644 --- a/src/shared/net/layer.h +++ b/src/shared/net/layer.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2013-2017 Calle Laakkonen + Copyright (C) 2013-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -52,8 +52,6 @@ class CanvasResize : public Message { int32_t bottom() const { return m_bottom; } int32_t left() const { return m_left; } - bool isOpCommand() const override { return true; } - QString messageName() const override { return QStringLiteral("resize"); } protected: @@ -104,7 +102,7 @@ class LayerCreate : public Message { static LayerCreate *deserialize(uint8_t ctx, const uchar *data, uint len); static LayerCreate *fromText(uint8_t ctx, const Kwargs &kwargs); - uint16_t id() const { return m_id; } + uint16_t layer() const override { return m_id; } uint16_t source() const { return m_source; } uint32_t fill() const { return m_fill; } uint8_t flags() const { return m_flags; } @@ -117,7 +115,7 @@ class LayerCreate : public Message { * created in single-user mode can use any ID. * This means layer IDs of the initial snapshot need not be validated. */ - bool isValidId() const { return (id()>>8) == contextId(); } + bool isValidId() const { return (m_id>>8) == contextId(); } QString messageName() const override { return QStringLiteral("newlayer"); } @@ -139,21 +137,30 @@ class LayerCreate : public Message { * * If the current layer or layer controls in general are locked, this command * requires session operator privileges. + * + * Specifying a sublayer requires session operator privileges. Currently, it is used + * only when sublayers are needed at canvas initialization. */ class LayerAttributes : public Message { public: - LayerAttributes(uint8_t ctx, uint16_t id, uint8_t opacity, uint8_t blend) + static const uint8_t FLAG_CENSOR = 0x01; // censored layer + + LayerAttributes(uint8_t ctx, uint16_t id, uint8_t sublayer, uint8_t flags, uint8_t opacity, uint8_t blend) : Message(MSG_LAYER_ATTR, ctx), m_id(id), - m_opacity(opacity), m_blend(blend) + m_sublayer(sublayer), m_flags(flags), m_opacity(opacity), m_blend(blend) {} static LayerAttributes *deserialize(uint8_t ctx, const uchar *data, uint len); static LayerAttributes *fromText(uint8_t ctx, const Kwargs &kwargs); - uint16_t id() const { return m_id; } + uint16_t layer() const override { return m_id; } + uint8_t sublayer() const { return m_sublayer; } + uint8_t flags() const { return m_flags; } uint8_t opacity() const { return m_opacity; } uint8_t blend() const { return m_blend; } + bool isCensored() const { return m_flags & FLAG_CENSOR; } + QString messageName() const override { return QStringLiteral("layerattr"); } protected: @@ -163,6 +170,8 @@ class LayerAttributes : public Message { private: uint16_t m_id; + uint8_t m_sublayer; + uint8_t m_flags; uint8_t m_opacity; uint8_t m_blend; }; @@ -188,7 +197,7 @@ class LayerVisibility : public Message { static LayerVisibility *deserialize(uint8_t ctx, const uchar *data, uint len); static LayerVisibility *fromText(uint8_t ctx, const Kwargs &kwargs); - uint16_t id() const { return m_id; } + uint16_t layer() const override { return m_id; } uint8_t visible() const { return m_visible; } QString messageName() const override { return QStringLiteral("layervisibility"); } @@ -221,7 +230,7 @@ class LayerRetitle : public Message { static LayerRetitle *deserialize(uint8_t ctx, const uchar *data, uint len); static LayerRetitle *fromText(uint8_t ctx, const Kwargs &kwargs); - uint16_t id() const { return m_id; } + uint16_t layer() const override { return m_id; } QString title() const { return QString::fromUtf8(m_title); } QString messageName() const override { return QStringLiteral("retitlelayer"); } @@ -310,7 +319,7 @@ class LayerDelete : public Message { static LayerDelete *deserialize(uint8_t ctx, const uchar *data, uint len); static LayerDelete *fromText(uint8_t ctx, const Kwargs &kwargs); - uint16_t id() const { return m_id; } + uint16_t layer() const override { return m_id; } uint8_t merge() const { return m_merge; } QString messageName() const override { return QStringLiteral("deletelayer"); } diff --git a/src/shared/net/message.cpp b/src/shared/net/message.cpp index 3166ff366..b3415bc75 100644 --- a/src/shared/net/message.cpp +++ b/src/shared/net/message.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2013-2018 Calle Laakkonen + Copyright (C) 2013-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -78,7 +78,7 @@ bool Message::payloadEquals(const Message &m) const return b1 == b2; } -Message *Message::deserialize(const uchar *data, int buflen, bool decodeOpaque) +NullableMessageRef Message::deserialize(const uchar *data, int buflen, bool decodeOpaque) { // All valid messages have the fixed length header if(buflen Kwargs; typedef QMapIterator KwargsIterator; class MessagePtr; +class NullableMessageRef; class Message { friend class MessagePtr; + friend class NullableMessageRef; public: //! Length of the fixed message header static const int HEADER_LEN = 4; - Message(MessageType type, uint8_t ctx): m_type(type), _undone(DONE), _refcount(0), m_contextid(ctx) {} + Message(MessageType type, uint8_t ctx): m_type(type), _undone(DONE), m_refcount(0), m_contextid(ctx) {} virtual ~Message() {} /** @@ -168,10 +178,16 @@ class Message { void setContextId(uint8_t userid) { m_contextid = userid; } /** - * @brief Does this command need operator privileges to issue? - * @return true if user must be session operator to send this + * @brief Get the ID of the layer this command affects + * + * For commands that do not affect any particular layer, 0 should + * be returned. + * + * Annotation editing commands can return the annotation ID here. + * + * @return layer (or equivalent) ID or 0 if not applicable */ - virtual bool isOpCommand() const { return false; } + virtual uint16_t layer() const { return 0; } /** * @brief Is this message type undoable? @@ -237,7 +253,7 @@ class Message { * @param decodeOpaque automatically decode opaque messages rather than returning OpaqueMessage * @return message or 0 if type is unknown */ - static Message *deserialize(const uchar *data, int buflen, bool decodeOpaque); + static NullableMessageRef deserialize(const uchar *data, int buflen, bool decodeOpaque); /** * @brief Check if this message has the same content as the other one @@ -299,7 +315,7 @@ class Message { private: const MessageType m_type; MessageUndoState _undone; - int _refcount; + int m_refcount; uint8_t m_contextid; }; @@ -346,47 +362,146 @@ class MessagePtr { * @param msg */ explicit MessagePtr(Message *msg) - : _ptr(msg) + : d(msg) { - Q_ASSERT(_ptr); - Q_ASSERT(_ptr->_refcount==0); - ++_ptr->_refcount; + Q_ASSERT(d); + Q_ASSERT(d->m_refcount==0); + ++d->m_refcount; } - MessagePtr(const MessagePtr &ptr) : _ptr(ptr._ptr) { ++_ptr->_refcount; } + MessagePtr(const MessagePtr &ptr) : d(ptr.d) { ++d->m_refcount; } + + static MessagePtr fromNullable(const NullableMessageRef &ref) { return MessagePtr(ref); } ~MessagePtr() { - Q_ASSERT(_ptr->_refcount>0); - if(--_ptr->_refcount == 0) - delete _ptr; + Q_ASSERT(d->m_refcount>0); + if(--d->m_refcount == 0) + delete d; } MessagePtr &operator=(const MessagePtr &msg) { - if(msg._ptr != _ptr) { - Q_ASSERT(_ptr->_refcount>0); - if(--_ptr->_refcount == 0) - delete _ptr; - _ptr = msg._ptr; - ++_ptr->_refcount; + if(msg.d != d) { + Q_ASSERT(d->m_refcount>0); + if(--d->m_refcount == 0) + delete d; + d = msg.d; + ++d->m_refcount; + } + return *this; + } + + Message &operator*() const { return *d; } + Message *operator->() const { return d; } + + template msgtype &cast() const { return static_cast(*d); } + + inline bool equals(const MessagePtr &m) const { return d->equals(*m); } + inline bool equals(const NullableMessageRef &m) const; + +private: + inline MessagePtr(const NullableMessageRef &ref); + + Message *d; +}; + +/** +* @brief A nullable reference counting pointer for Messages +* +* This object is the length of a normal pointer so it can be used +* efficiently with QList. +* +* @todo Maybe rename MessagePtr to MessageRef and this to MessagePtr? +*/ +class NullableMessageRef { +public: + NullableMessageRef() : d(nullptr) { } + NullableMessageRef(std::nullptr_t np) : d(np) { } + + /** + * @brief Take ownership of the given raw Message pointer. + * + * The message will be deleted when reference count falls to zero. + * @param msg + */ + explicit NullableMessageRef(Message *msg) + : d(msg) + { + if(d) { + Q_ASSERT(d->m_refcount==0); + ++d->m_refcount; + } + } + + NullableMessageRef(const MessagePtr &ptr) : d(&(*ptr)) { ++d->m_refcount; } + NullableMessageRef(const NullableMessageRef &ptr) : d(ptr.d) { if(d) ++d->m_refcount; } + + ~NullableMessageRef() + { + if(d) { + Q_ASSERT(d->m_refcount>0); + if(--d->m_refcount == 0) + delete d; + } + } + + NullableMessageRef &operator=(const NullableMessageRef &msg) + { + if(msg.d != d) { + if(d) { + Q_ASSERT(d->m_refcount>0); + if(--d->m_refcount == 0) + delete d; + } + d = msg.d; + if(d) + ++d->m_refcount; + } + return *this; + } + + NullableMessageRef &operator=(const MessagePtr &msg) + { + if(&(*msg) != d) { + if(d) { + Q_ASSERT(d->m_refcount>0); + if(--d->m_refcount == 0) + delete d; + } + d = &(*msg); + ++d->m_refcount; } return *this; } - Message &operator*() const { return *_ptr; } - Message *operator ->() const { return _ptr; } + inline bool isNull() const { return !d; } + + Message &operator*() const { Q_ASSERT(d); return *d; } + Message *operator->() const { Q_ASSERT(d); return d; } - template msgtype &cast() const { return static_cast(*_ptr); } + template msgtype &cast() const { Q_ASSERT(d); return static_cast(*d); } - bool equals(const MessagePtr &m) const { return _ptr->equals(*m); } + inline bool equals(const MessagePtr &m) const { return d && d->equals(*m); } + inline bool equals(const NullableMessageRef &m) const { return d && m.d && d->equals(*m); } private: - Message *_ptr; + Message *d; }; +MessagePtr::MessagePtr(const NullableMessageRef &ref) + : d(&(*ref)) +{ + if(!d) + qFatal("MessagePtr::fromNullable(nullptr) called!"); + ++d->m_refcount; +} + +bool MessagePtr::equals(const NullableMessageRef &m) const { return !m.isNull() && d->equals(*m); } + } Q_DECLARE_TYPEINFO(protocol::MessagePtr, Q_MOVABLE_TYPE); +Q_DECLARE_TYPEINFO(protocol::NullableMessageRef, Q_MOVABLE_TYPE); #endif diff --git a/src/shared/net/messagequeue.cpp b/src/shared/net/messagequeue.cpp index 6fd0ff686..f8bf8b6cc 100644 --- a/src/shared/net/messagequeue.cpp +++ b/src/shared/net/messagequeue.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2008-2017 Calle Laakkonen + Copyright (C) 2008-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -206,13 +206,11 @@ void MessageQueue::readData() { int len; while(m_recvbytes >= Message::HEADER_LEN && m_recvbytes >= (len=Message::sniffLength(m_recvbuffer))) { // Whole message received! - Message *message = Message::deserialize((const uchar*)m_recvbuffer, m_recvbytes, m_decodeOpaque); - if(!message) { + NullableMessageRef msg = Message::deserialize((const uchar*)m_recvbuffer, m_recvbytes, m_decodeOpaque); + if(msg.isNull()) { emit badData(len, (unsigned char)m_recvbuffer[2], (unsigned char)m_recvbuffer[3]); } else { - MessagePtr msg(message); - if(msg->type() == MSG_PING) { // Special handling for Ping messages bool isPong = msg.cast().isPong(); @@ -231,7 +229,7 @@ void MessageQueue::readData() { } } else { - m_inbox.enqueue(msg); + m_inbox.enqueue(MessagePtr::fromNullable(msg)); gotmessage = true; } } diff --git a/src/shared/net/meta.cpp b/src/shared/net/meta.cpp index a78e42be6..f05974216 100644 --- a/src/shared/net/meta.cpp +++ b/src/shared/net/meta.cpp @@ -33,13 +33,13 @@ UserJoin *UserJoin::deserialize(uint8_t ctx, const uchar *data, uint len) const uint8_t flags = data[0]; const uint nameLen = data[1]; - // Name must be at least one character long, but hash is optional + // Name must be at least one character long, but avatar is optional if(nameLen==0 || nameLen+2 > len) return nullptr; const QByteArray name = QByteArray((const char*)data+2, nameLen); - const QByteArray hash = QByteArray((const char*)data+2+nameLen, len-2-nameLen); - return new UserJoin(ctx, flags, name, hash); + const QByteArray avatar = QByteArray((const char*)data+2+nameLen, len-2-nameLen); + return new UserJoin(ctx, flags, name, avatar); } int UserJoin::serializePayload(uchar *data) const @@ -49,23 +49,23 @@ int UserJoin::serializePayload(uchar *data) const *(ptr++) = m_name.length(); memcpy(ptr, m_name.constData(), m_name.length()); ptr += m_name.length(); - memcpy(ptr, m_hash.constData(), m_hash.length()); - ptr += m_hash.length(); + memcpy(ptr, m_avatar.constData(), m_avatar.length()); + ptr += m_avatar.length(); return ptr - data; } int UserJoin::payloadLength() const { - return 1 + 1 + m_name.length() + m_hash.length(); + return 1 + 1 + m_name.length() + m_avatar.length(); } Kwargs UserJoin::kwargs() const { Kwargs kw; kw["name"] = name(); - if(!m_hash.isEmpty()) - kw["hash"] = QString::fromUtf8(m_hash); + if(!m_avatar.isEmpty()) + kw["avatar"] = QString::fromUtf8(m_avatar); QStringList flags; if(isModerator()) flags << "mod"; @@ -85,7 +85,7 @@ UserJoin *UserJoin::fromText(uint8_t ctx, const Kwargs &kwargs) (flags.contains("mod") ? FLAG_MOD : 0) | (flags.contains("auth") ? FLAG_AUTH : 0), kwargs["name"], - kwargs["hash"].toUtf8() + kwargs["avatar"].toUtf8() ); } @@ -128,6 +128,45 @@ SessionOwner *SessionOwner::fromText(uint8_t ctx, const Kwargs &kwargs) return new SessionOwner(ctx, text::parseIdListString8(kwargs["users"])); } + +TrustedUsers *TrustedUsers::deserialize(uint8_t ctx, const uchar *data, int len) +{ + if(len>255) + return nullptr; + + QList ids; + ids.reserve(len); + for(int i=0;i0 && name.length()<256); } - UserJoin(uint8_t ctx, uint8_t flags, const QString &name, const QByteArray &hash=QByteArray()) : UserJoin(ctx, flags, name.toUtf8(), hash) {} + UserJoin(uint8_t ctx, uint8_t flags, const QByteArray &name, const QByteArray &avatar) : Message(MSG_USER_JOIN, ctx), m_name(name), m_avatar(avatar), m_flags(flags) { Q_ASSERT(name.length()>0 && name.length()<256); } + UserJoin(uint8_t ctx, uint8_t flags, const QString &name, const QByteArray &avatar=QByteArray()) : UserJoin(ctx, flags, name.toUtf8(), avatar) {} static UserJoin *deserialize(uint8_t ctx, const uchar *data, uint len); static UserJoin *fromText(uint8_t ctx, const Kwargs &kwargs); QString name() const { return QString::fromUtf8(m_name); } - QByteArray identityHash() const { return m_hash; } + QByteArray avatar() const { return m_avatar; } uint8_t flags() const { return m_flags; } bool isModerator() const { return m_flags & FLAG_MOD; } bool isAuthenticated() const { return m_flags & FLAG_AUTH; } + bool isBot() const { return m_flags & FLAG_BOT; } QString messageName() const override { return QStringLiteral("join"); } @@ -65,7 +64,7 @@ class UserJoin : public Message { private: QByteArray m_name; - QByteArray m_hash; + QByteArray m_avatar; uint8_t m_flags; }; @@ -102,8 +101,6 @@ class SessionOwner : public Message { static SessionOwner *deserialize(uint8_t ctx, const uchar *data, int buflen); static SessionOwner *fromText(uint8_t ctx, const Kwargs &kwargs); - bool isOpCommand() const override { return true; } - QList ids() const { return m_ids; } void setIds(const QList ids) { m_ids = ids; } @@ -118,6 +115,40 @@ class SessionOwner : public Message { QList m_ids; }; +/** + * @brief List of trusted users + * + * This message sets the list of user who have been tagged as trusted, + * but who are not operators. The meaning of "trusted" is a mostly + * clientside concept, but the session can be configured to allow trusted + * users access to some operator commands. (Deputies) + * + * This command can be sent by operators or by the server (ctx=0). + * + * The server sanitizes the ID list so, when distributed to other users, + * it does not contain any duplicates or non-existing users. + */ +class TrustedUsers : public Message { +public: + TrustedUsers(uint8_t ctx, QList ids) : Message(MSG_TRUSTED_USERS, ctx), m_ids(ids) { } + + static TrustedUsers *deserialize(uint8_t ctx, const uchar *data, int buflen); + static TrustedUsers *fromText(uint8_t ctx, const Kwargs &kwargs); + + QList ids() const { return m_ids; } + void setIds(const QList ids) { m_ids = ids; } + + QString messageName() const override { return "trusted"; } + +protected: + int payloadLength() const override; + int serializePayload(uchar *data) const override; + Kwargs kwargs() const override; + +private: + QList m_ids; +}; + /** * @brief A chat message * @@ -203,6 +234,72 @@ class Chat : public Message { QByteArray m_msg; }; +/** + * @brief A private chat message + * + * Note. This message type was added in protocol 4.21.2 (v. 2.1.0). For backward compatiblity, + * the server will not send any private messages from itself; it will only relay them from + * other users. + * + * Private messages always bypass the session history. + */ +class PrivateChat : public Message { +public: + // Opaque flags: the server doesn't know anything about these + static const uint8_t FLAG_ACTION = 0x02; // this is an "action message" (like /me in IRC) + + PrivateChat(uint8_t ctx, uint8_t target, uint8_t oflags, const QByteArray &msg) : Message(MSG_PRIVATE_CHAT, ctx), m_target(target), m_oflags(oflags), m_msg(msg) {} + PrivateChat(uint8_t ctx, uint8_t target, uint8_t oflags, const QString &msg) : PrivateChat(ctx, target, oflags, msg.toUtf8()) {} + + //! Construct a regular chat message + static MessagePtr regular(uint8_t ctx, uint8_t target, const QString &message) { return MessagePtr(new PrivateChat(ctx, target, 0, message.toUtf8())); } + + //! Construct an action type message + static MessagePtr action(uint8_t ctx, uint8_t target, const QString &message) { return MessagePtr(new Chat(ctx, target, FLAG_ACTION, message.toUtf8())); } + + static PrivateChat *deserialize(uint8_t ctx, const uchar *data, uint len); + static PrivateChat *fromText(uint8_t ctx, const Kwargs &kwargs); + + //! Recipient ID + uint8_t target() const { return m_target; } + + uint8_t opaqueFlags() const { return m_oflags; } + + QString message() const { return QString::fromUtf8(m_msg); } + + //! Is this an action message? (client side only) + bool isAction() const { return m_oflags & FLAG_ACTION; } + + QString messageName() const override { return QStringLiteral("pm"); } + +protected: + int payloadLength() const override; + int serializePayload(uchar *data) const override; + Kwargs kwargs() const override; + +private: + uint8_t m_target; + uint8_t m_oflags; + QByteArray m_msg; +}; + +/** + * @brief Soft reset point marker + * + * This message marks the point in the session history where soft reset occurs. + * Soft resetting is not actually implemented yet; this is here for forward compatiblity. + * + * All users should truncate their own session history when receiving this message, + * since undos cannot cross the reset boundary. + * + * The current client implementation handles the history truncation part. This is + * enough to be compatible with future clients capable of initiating soft reset. + */ +class SoftResetPoint : public ZeroLengthMessage { +public: + explicit SoftResetPoint(uint8_t ctx) : ZeroLengthMessage(MSG_SOFTRESET, ctx) { } + QString messageName() const override { return QStringLiteral("softreset"); } +}; } diff --git a/src/shared/net/meta2.cpp b/src/shared/net/meta2.cpp index c40a58c19..625f7e67c 100644 --- a/src/shared/net/meta2.cpp +++ b/src/shared/net/meta2.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2013-2017 Calle Laakkonen + Copyright (C) 2013-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -172,19 +172,42 @@ int LayerACL::serializePayload(uchar *data) const { uchar *ptr = data; qToBigEndian(m_id, ptr); ptr += 2; - *(ptr++) = m_locked; + *(ptr++) = m_flags; for(uint8_t e : m_exclusive) *(ptr++) = e; return ptr-data; } +static const char *TIER_NAMES[4] = { + "op", "trusted", "auth", "guest" +}; + +static int tierFromName(const QString &name) +{ + for(int i=0;i<4;++i) + if(name == TIER_NAMES[i]) + return i; + return 0; +} + +static const char *tierName(int tier) +{ + return TIER_NAMES[qBound(0, tier, 3)]; +} + Kwargs LayerACL::kwargs() const { Kwargs kw; kw["id"] = text::idString(m_id); - kw["locked"] = m_locked ? "true" : "false"; - if(!m_exclusive.isEmpty()) - kw["exclusive"] = text::idListString(m_exclusive); + kw["locked"] = locked() ? "true" : "false"; + + + if(m_id > 0) { + kw["tier"] = tierName(tier()); + if(!m_exclusive.isEmpty()) + kw["exclusive"] = text::idListString(m_exclusive); + } + return kw; } @@ -194,58 +217,55 @@ LayerACL *LayerACL::fromText(uint8_t ctx, const Kwargs &kwargs) ctx, text::parseIdString16(kwargs["id"]), kwargs["locked"] == "true", + tierFromName(kwargs["tier"]), text::parseIdListString8(kwargs["exclusive"]) ); } -SessionACL *SessionACL::deserialize(uint8_t ctx, const uchar *data, uint len) +FeatureAccessLevels *FeatureAccessLevels::deserialize(uint8_t ctx, const uchar *data, uint len) { - if(len != 2) + if(len != FEATURES) return nullptr; - uint16_t flags = qFromBigEndian(data+0); - return new SessionACL(ctx, flags); + return new FeatureAccessLevels(ctx, data); } -int SessionACL::payloadLength() const +int FeatureAccessLevels::serializePayload(uchar *data) const { - return 2; + memcpy(data, m_featureTiers, FEATURES); + return FEATURES; } -int SessionACL::serializePayload(uchar *data) const +static const char *FEATURE_NAMES[FeatureAccessLevels::FEATURES] = { + "putimage", + "regionmove", + "resize", + "background", + "editlayers", + "ownlayers", + "createannotation", + "laser", + "undo" +}; + +Kwargs FeatureAccessLevels::kwargs() const { - uchar *ptr = data; - qToBigEndian(m_flags, ptr); ptr += 2; - return ptr-data; -} - -Kwargs SessionACL::kwargs() const -{ - QStringList locks; - if(isSessionLocked()) locks << "session"; - if(isLockedByDefault()) locks << "default"; - if(isLayerControlLocked()) locks << "layerctrl"; - if(isOwnLayers()) locks << "ownlayers"; - if(isImagesLocked()) locks << "images"; - if(isAnnotationCreationLocked()) locks << "annotations"; Kwargs kw; - kw["locks"] = locks.join(','); + for(int i=0;i 0) + kw[FEATURE_NAMES[i]] = tierName(m_featureTiers[i]); + } return kw; } -SessionACL *SessionACL::fromText(uint8_t ctx, const Kwargs &kwargs) +FeatureAccessLevels *FeatureAccessLevels::fromText(uint8_t ctx, const Kwargs &kwargs) { - QStringList locks = kwargs["locks"].split(','); + uint8_t features[FEATURES]; + for(int i=0;i ids() const { return m_ids; } QString messageName() const override { return QStringLiteral("useracl"); } @@ -123,20 +121,22 @@ class UserACL : public Message { * * When the OWNLAYERS mode is set, any user can use this to change the ACLs on layers they themselves * have created (identified by the ID prefix.) + * + * Using layer ID 0 sets or clears a general canvaswide lock. The tier and exclusive user list is not + * used in this case. */ class LayerACL : public Message { public: - LayerACL(uint8_t ctx, uint16_t id, uint8_t locked, const QList &exclusive) - : Message(MSG_LAYER_ACL, ctx), m_id(id), m_locked(locked), m_exclusive(exclusive) + LayerACL(uint8_t ctx, uint16_t id, bool locked, uint8_t tier, const QList &exclusive) + : LayerACL(ctx, id, (locked?0x80:0) | (tier&0x07), exclusive) {} static LayerACL *deserialize(uint8_t ctx, const uchar *data, uint len); static LayerACL *fromText(uint8_t ctx, const Kwargs &kwargs); - // Note: this is an operator only command, depending on the target layer and whether OWNLAYERS mode is set. - - uint16_t id() const { return m_id; } - uint8_t locked() const { return m_locked; } + uint16_t layer() const override { return m_id; } + bool locked() const { return m_flags & 0x80; } + uint8_t tier() const { return m_flags & 0x07;} const QList exclusive() const { return m_exclusive; } QString messageName() const override { return QStringLiteral("layeracl"); } @@ -147,50 +147,45 @@ class LayerACL : public Message { Kwargs kwargs() const override; private: + LayerACL(uint8_t ctx, uint16_t id, uint8_t flags, const QList &exclusive) + : Message(MSG_LAYER_ACL, ctx), m_id(id), m_flags(flags), m_exclusive(exclusive) + {} + uint16_t m_id; - uint8_t m_locked; + uint8_t m_flags; QList m_exclusive; }; /** - * @brief Change session wide access control settings + * @brief Change feature access levels * * This is an opaque meta command. */ -class SessionACL : public Message { +class FeatureAccessLevels : public Message { public: - static const uint16_t LOCK_SESSION = 0x01; // General session-wide lock (locks even operators) - static const uint16_t LOCK_DEFAULT = 0x02; // New users will be locked by default (lock applied when the JOIN message is received) - static const uint16_t LOCK_LAYERCTRL = 0x04; // Layer controls are limited to session operators - static const uint16_t LOCK_OWNLAYERS = 0x08; // Users can only delete/adjust their own layers. (May set layer ACLs too) - static const uint16_t LOCK_IMAGES = 0x10; // PutImage and FillRect commands (and features that use them) are limited to session operators - static const uint16_t LOCK_ANNOTATIONS = 0x20; // Only operators can create new annotations + static const int FEATURES = 9; // Number of configurable features - SessionACL(uint8_t ctx, uint16_t flags) : Message(MSG_SESSION_ACL, ctx), m_flags(flags) {} + FeatureAccessLevels(uint8_t ctx, const uint8_t *featureTiers) + : Message(MSG_FEATURE_LEVELS, ctx) + { + for(int i=0;i=0 && featureIdx=64); + Message *msg = nullptr; + switch(type) { - case MSG_INTERVAL: return Interval::deserialize(ctx, data, len); - case MSG_LASERTRAIL: return LaserTrail::deserialize(ctx, data, len); - case MSG_MOVEPOINTER: return MovePointer::deserialize(ctx, data, len); - case MSG_MARKER: return Marker::deserialize(ctx, data, len); - case MSG_USER_ACL: return UserACL::deserialize(ctx, data, len); - case MSG_LAYER_ACL: return LayerACL::deserialize(ctx, data, len); - case MSG_SESSION_ACL: return SessionACL::deserialize(ctx, data, len); - case MSG_LAYER_DEFAULT: return DefaultLayer::deserialize(ctx, data, len); + case MSG_INTERVAL: msg = Interval::deserialize(ctx, data, len); break; + case MSG_LASERTRAIL: msg = LaserTrail::deserialize(ctx, data, len); break; + case MSG_MOVEPOINTER: msg = MovePointer::deserialize(ctx, data, len); break; + case MSG_MARKER: msg = Marker::deserialize(ctx, data, len); break; + case MSG_USER_ACL: msg = UserACL::deserialize(ctx, data, len); break; + case MSG_LAYER_ACL: msg = LayerACL::deserialize(ctx, data, len); break; + case MSG_FEATURE_LEVELS: msg = FeatureAccessLevels::deserialize(ctx, data, len); break; + case MSG_LAYER_DEFAULT: msg = DefaultLayer::deserialize(ctx, data, len); break; case MSG_FILTERED: return Filtered::deserialize(ctx, data, len); - case MSG_CANVAS_RESIZE: return CanvasResize::deserialize(ctx, data, len); - case MSG_LAYER_CREATE: return LayerCreate::deserialize(ctx, data, len); - case MSG_LAYER_ATTR: return LayerAttributes::deserialize(ctx, data, len); - case MSG_LAYER_RETITLE: return LayerRetitle::deserialize(ctx, data, len); - case MSG_LAYER_ORDER: return LayerOrder::deserialize(ctx, data, len); - case MSG_LAYER_DELETE: return LayerDelete::deserialize(ctx, data, len); - case MSG_LAYER_VISIBILITY: return LayerVisibility::deserialize(ctx, data, len); - case MSG_PUTIMAGE: return PutImage::deserialize(ctx, data, len); - case MSG_TOOLCHANGE: return ToolChange::deserialize(ctx, data, len); - case MSG_PEN_MOVE: return PenMove::deserialize(ctx, data, len); - case MSG_PEN_UP: return PenUp::deserialize(ctx, data, len); - case MSG_ANNOTATION_CREATE: return AnnotationCreate::deserialize(ctx, data, len); - case MSG_ANNOTATION_RESHAPE: return AnnotationReshape::deserialize(ctx, data, len); - case MSG_ANNOTATION_EDIT: return AnnotationEdit::deserialize(ctx, data, len); - case MSG_ANNOTATION_DELETE: return AnnotationDelete::deserialize(ctx, data, len); - case MSG_UNDOPOINT: return UndoPoint::deserialize(ctx, data, len); - case MSG_UNDO: return Undo::deserialize(ctx, data, len); - case MSG_FILLRECT: return FillRect::deserialize(ctx, data, len); - case MSG_REGION_MOVE: return MoveRegion::deserialize(ctx, data, len); - default: - qWarning("Unhandled opaque message type: %d", type); - return nullptr; + case MSG_CANVAS_RESIZE: msg = CanvasResize::deserialize(ctx, data, len); break; + case MSG_LAYER_CREATE: msg = LayerCreate::deserialize(ctx, data, len); break; + case MSG_LAYER_ATTR: msg = LayerAttributes::deserialize(ctx, data, len); break; + case MSG_LAYER_RETITLE: msg = LayerRetitle::deserialize(ctx, data, len); break; + case MSG_LAYER_ORDER: msg = LayerOrder::deserialize(ctx, data, len); break; + case MSG_LAYER_DELETE: msg = LayerDelete::deserialize(ctx, data, len); break; + case MSG_LAYER_VISIBILITY: msg = LayerVisibility::deserialize(ctx, data, len); break; + case MSG_PUTIMAGE: msg = PutImage::deserialize(ctx, data, len); break; + case MSG_PEN_UP: msg = PenUp::deserialize(ctx, data, len); break; + case MSG_ANNOTATION_CREATE: msg = AnnotationCreate::deserialize(ctx, data, len); break; + case MSG_ANNOTATION_RESHAPE: msg = AnnotationReshape::deserialize(ctx, data, len); break; + case MSG_ANNOTATION_EDIT: msg = AnnotationEdit::deserialize(ctx, data, len); break; + case MSG_ANNOTATION_DELETE: msg = AnnotationDelete::deserialize(ctx, data, len); break; + case MSG_UNDOPOINT: msg = UndoPoint::deserialize(ctx, data, len); break; + case MSG_UNDO: msg = Undo::deserialize(ctx, data, len); break; + case MSG_FILLRECT: msg = FillRect::deserialize(ctx, data, len); break; + case MSG_REGION_MOVE: msg = MoveRegion::deserialize(ctx, data, len); break; + case MSG_PUTTILE: msg = PutTile::deserialize(ctx, data, len); break; + case MSG_CANVAS_BACKGROUND: msg = CanvasBackground::deserialize(ctx, data, len); break; + case MSG_DRAWDABS_CLASSIC: msg = DrawDabsClassic::deserialize(ctx, data, len); break; + case MSG_DRAWDABS_PIXEL: msg = DrawDabsPixel::deserialize(DabShape::Round, ctx, data, len); break; + case MSG_DRAWDABS_PIXEL_SQUARE: msg = DrawDabsPixel::deserialize(DabShape::Square, ctx, data, len); break; + default: qWarning("Unhandled opaque message type: %d", type); } + + return NullableMessageRef(msg); } -Message *OpaqueMessage::decode() const +NullableMessageRef OpaqueMessage::decode() const { return decode(type(), contextId(), m_payload, m_length); } diff --git a/src/shared/net/opaque.h b/src/shared/net/opaque.h index a38d5169d..4161f6285 100644 --- a/src/shared/net/opaque.h +++ b/src/shared/net/opaque.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2015-2017 Calle Laakkonen + Copyright (C) 2015-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -40,13 +40,13 @@ class OpaqueMessage : public Message OpaqueMessage(const OpaqueMessage &m) = delete; OpaqueMessage &operator=(const OpaqueMessage &m) = delete; - static Message *decode(MessageType type, uint8_t ctx, const uchar *data, uint len); + static NullableMessageRef decode(MessageType type, uint8_t ctx, const uchar *data, uint len); /** * @brief Decode this message * @return Message or nullptr if data is invalid */ - Message *decode() const; + NullableMessageRef decode() const; QString messageName() const override { return QStringLiteral("_opaque"); } diff --git a/src/shared/net/pen.cpp b/src/shared/net/pen.cpp deleted file mode 100644 index 972effbb2..000000000 --- a/src/shared/net/pen.cpp +++ /dev/null @@ -1,246 +0,0 @@ -/* - Drawpile - a collaborative drawing program. - - Copyright (C) 2013-2017 Calle Laakkonen - - Drawpile 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 3 of the License, or - (at your option) any later version. - - Drawpile 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 Drawpile. If not, see . -*/ - -#include "pen.h" -#include "textmode.h" - -#include - -namespace protocol { - -ToolChange *ToolChange::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len != 18) - return 0; - - return new ToolChange( - ctx, - qFromBigEndian(data+0), - *(data+2), - *(data+3), - *(data+4), - qFromBigEndian(data+5), - *(data+9), - *(data+10), - *(data+11), - *(data+12), - *(data+13), - *(data+14), - *(data+15), - *(data+16), - *(data+17) - ); -} - -int ToolChange::payloadLength() const -{ - return 18; -} - -int ToolChange::serializePayload(uchar *data) const -{ - uchar *ptr = data; - qToBigEndian(m_layer, ptr); ptr += 2; - *(ptr++) = m_blend; - *(ptr++) = m_mode; - *(ptr++) = m_spacing; - qToBigEndian(m_color, ptr); ptr += 4; - *(ptr++) = m_hard_h; - *(ptr++) = m_hard_l; - *(ptr++) = m_size_h; - *(ptr++) = m_size_l; - *(ptr++) = m_opacity_h; - *(ptr++) = m_opacity_l; - *(ptr++) = m_smudge_h; - *(ptr++) = m_smudge_l; - *(ptr++) = m_resmudge; - return ptr-data; -} - -Kwargs ToolChange::kwargs() const -{ - Kwargs kw; - QStringList mode; - if((m_mode & TOOL_MODE_SUBPIXEL)) mode << "soft"; - if((m_mode & TOOL_MODE_INCREMENTAL)) mode << "inc"; - kw["layer"] = text::idString(m_layer); - kw["blend"] = QString::number(m_blend); - kw["mode"] = mode.join(','); - kw["spacing"] = QString::number(m_spacing); - kw["color"] = text::rgbString(m_color); - if(m_hard_h != m_hard_l) { - kw["hardh"] = text::decimal(m_hard_h); - kw["hardl"] = text::decimal(m_hard_l); - } else { - kw["hard"] = text::decimal(m_hard_h); - } - if(m_size_h != m_size_l) { - kw["sizeh"] = QString::number(m_size_h); - kw["sizel"] = QString::number(m_size_l); - } else { - kw["size"] = QString::number(m_size_h); - } - if(m_opacity_h != m_opacity_l) { - kw["opacityh"] = text::decimal(m_opacity_h); - kw["opacityl"] = text::decimal(m_opacity_l); - } else { - kw["opacity"] = text::decimal(m_opacity_h); - } - if(m_smudge_h != m_smudge_l) { - kw["smudgeh"] = text::decimal(m_smudge_h); - kw["smudgel"] = text::decimal(m_smudge_l); - } else if(m_smudge_h>0) { - kw["smudge"] = text::decimal(m_smudge_h); - } - if(m_resmudge>0) - kw["resmudge"] = QString::number(m_resmudge); - return kw; -} - -ToolChange *ToolChange::fromText(uint8_t ctx, const Kwargs &kwargs) -{ - uint8_t hardh, hardl, sizeh, sizel, opacityh, opacityl, smudgeh, smudgel; - if(kwargs.contains("hard")) { - hardh = hardl = text::parseDecimal8(kwargs["hard"]); - } else { - hardh = text::parseDecimal8(kwargs["hardh"]); - hardl = text::parseDecimal8(kwargs["hardl"]); - } - - if(kwargs.contains("size")) { - sizeh = sizel = text::parseDecimal8(kwargs["size"]); - } else { - sizeh = kwargs.value("sizeh", "1").toInt(); - sizel = kwargs.value("sizel", "1").toInt(); - } - - if(kwargs.contains("opacity")) { - opacityh = opacityl = text::parseDecimal8(kwargs["opacity"]); - } else { - opacityh = text::parseDecimal8(kwargs.value("opacityh", "100")); - opacityl = text::parseDecimal8(kwargs.value("opacityl", "100")); - } - - if(kwargs.contains("smudge")) { - smudgeh = smudgel = text::parseDecimal8(kwargs["smudge"]); - } else { - smudgeh = text::parseDecimal8(kwargs["smudgeh"]); - smudgel = text::parseDecimal8(kwargs["smudgel"]); - } - - QStringList mode = kwargs["mode"].split(','); - - return new ToolChange( - ctx, - text::parseIdString16(kwargs["layer"]), - kwargs.value("blend", "1").toInt(), - (mode.contains("inc") ? TOOL_MODE_INCREMENTAL : 0) | - (mode.contains("soft") ? TOOL_MODE_SUBPIXEL : 0), - kwargs["spacing"].toInt(), - text::parseColor(kwargs["color"]), - hardh, hardl, - sizeh, sizel, - opacityh, opacityl, - smudgeh, smudgel, - kwargs["resmudge"].toInt() - ); -} - -PenMove *PenMove::deserialize(uint8_t ctx, const uchar *data, uint len) -{ - if(len<10 || len%10) - return nullptr; - PenPointVector pp; - - int points = len/10; - pp.reserve(points); - while(points--) { - pp.append(PenPoint( - qFromBigEndian(data), - qFromBigEndian(data+4), - qFromBigEndian(data+8) - )); - data += 10; - } - return new PenMove(ctx, pp); -} - -int PenMove::payloadLength() const -{ - return 10 * m_points.size(); -} - -int PenMove::serializePayload(uchar *data) const -{ - uchar *ptr = data; - for(const PenPoint &p : m_points) { - qToBigEndian(p.x, ptr); ptr += 4; - qToBigEndian(p.y, ptr); ptr += 4; - qToBigEndian(p.p, ptr); ptr += 2; - } - return ptr - data; -} - -bool PenMove::payloadEquals(const Message &m) const -{ - const PenMove &pm = static_cast(m); - - if(points().size() != pm.points().size()) - return false; - - for(int i=0;i. -*/ -#ifndef DP_NET_PEN_H -#define DP_NET_PEN_H - -#include "message.h" - -#include -#include - -namespace protocol { - struct PenPoint { - PenPoint() = default; - PenPoint(int32_t x_, int32_t y_, uint16_t p_) : x(x_), y(y_), p(p_) {} - int32_t x, y; - uint16_t p; - - bool operator!=(const PenPoint &o) const { return x != o.x || y != o.y || p != o.p; } - }; -} - -Q_DECLARE_TYPEINFO(protocol::PenPoint, Q_PRIMITIVE_TYPE); - -namespace protocol { - -static const uint8_t TOOL_MODE_SUBPIXEL = 0x01; -static const uint8_t TOOL_MODE_INCREMENTAL = 0x02; - -/** - * @brief Tool setting change command - * - * This command is sent by the client to set the tool properties. - * The initial state of the tool is undefined by the protocol, so the client - * must send a ToolChange before sending the first PenMove command. A tool - * change may only be sent when the client is in a PenUp state. - */ -class ToolChange : public Message { -public: - ToolChange( - uint8_t ctx, uint16_t layer, - uint8_t blend, uint8_t mode, uint8_t spacing, - uint32_t color, - uint8_t hard_h, uint8_t hard_l, - uint8_t size_h, uint8_t size_l, - uint8_t opacity_h, uint8_t opacity_l, - uint8_t smudge_h, uint8_t smudge_l, - uint8_t resmudge - ) - : Message(MSG_TOOLCHANGE, ctx), - m_layer(layer), m_blend(blend), m_mode(mode), - m_spacing(spacing), m_color(color), - m_hard_h(hard_h), m_hard_l(hard_l), m_size_h(size_h), m_size_l(size_l), - m_opacity_h(opacity_h), m_opacity_l(opacity_l), - m_smudge_h(smudge_h), m_smudge_l(smudge_l), m_resmudge(resmudge) - {} - - static ToolChange *deserialize(uint8_t ctx, const uchar *data, uint len); - static ToolChange *fromText(uint8_t ctx, const Kwargs &kwargs); - - uint16_t layer() const { return m_layer; } - uint8_t blend() const { return m_blend; } - uint8_t mode() const { return m_mode; } - uint8_t spacing() const { return m_spacing; } - uint32_t color() const { return m_color; } - uint8_t hard_h() const { return m_hard_h; } - uint8_t hard_l() const { return m_hard_l; } - uint8_t size_h() const { return m_size_h; } - uint8_t size_l() const { return m_size_l; } - uint8_t opacity_h() const { return m_opacity_h; } - uint8_t opacity_l() const { return m_opacity_l; } - uint8_t smudge_h() const { return m_smudge_h; } - uint8_t smudge_l() const { return m_smudge_l; } - uint8_t resmudge() const { return m_resmudge; } - - // The client resends ToolChange only if it has changed since - // the last time it was sent. If the ToolChange is undoable, - // the effective tool may be different than what the client thinks. - // To keep things simple, we just don't undo ToolChanges. - bool isUndoable() const override { return false; } - - QString messageName() const override { return QStringLiteral("brush"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - Kwargs kwargs() const override; - -private: - uint16_t m_layer; - uint8_t m_blend; - uint8_t m_mode; - uint8_t m_spacing; - uint32_t m_color; - uint8_t m_hard_h; - uint8_t m_hard_l; - uint8_t m_size_h; - uint8_t m_size_l; - uint8_t m_opacity_h; - uint8_t m_opacity_l; - uint8_t m_smudge_h; - uint8_t m_smudge_l; - uint8_t m_resmudge; -}; - -typedef QVector PenPointVector; - -/** - * @brief Pen move command - * - * The first pen move command starts a new stroke. - */ -class PenMove : public Message { -public: - //! The maximum number of points that will fit into a single PenMove message - static const int MAX_POINTS = 0xffff / 10; - - PenMove(uint8_t ctx, const PenPointVector &points) - : Message(MSG_PEN_MOVE, ctx), - m_points(points) - { - Q_ASSERT(!points.isEmpty()); - Q_ASSERT(points.size() <= MAX_POINTS); - } - - static PenMove *deserialize(uint8_t ctx, const uchar *data, uint len); - - const PenPointVector &points() const { return m_points; } - PenPointVector &points() { return m_points; } - - QString toString() const override; - QString messageName() const override { return QStringLiteral("penmove"); } - -protected: - int payloadLength() const override; - int serializePayload(uchar *data) const override; - bool payloadEquals(const Message &m) const override; - Kwargs kwargs() const override { return Kwargs(); } - -private: - PenPointVector m_points; -}; - -/** - * @brief Pen up command - * - * The pen up signals the end of the stroke. In indirect drawing mode, it causes - * the stroke to be committed to the current layer. - */ -class PenUp : public ZeroLengthMessage { -public: - PenUp(uint8_t ctx) : ZeroLengthMessage(MSG_PEN_UP, ctx) {} - - QString messageName() const override { return QStringLiteral("penup"); } -}; - -} - -#endif diff --git a/src/shared/net/recording.cpp b/src/shared/net/recording.cpp index f12abce8e..11282b1a6 100644 --- a/src/shared/net/recording.cpp +++ b/src/shared/net/recording.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2014-2018 Calle Laakkonen + Copyright (C) 2014-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -101,7 +101,7 @@ Filtered::~Filtered() delete []m_payload; } -Message *Filtered::deserialize(uint8_t ctx, const uchar *data, uint len) +NullableMessageRef Filtered::deserialize(uint8_t ctx, const uchar *data, uint len) { if(len<1 || len > 0xffff) return nullptr; @@ -109,10 +109,10 @@ Message *Filtered::deserialize(uint8_t ctx, const uchar *data, uint len) uchar *payload = new uchar[len]; memcpy(payload, data, len); - return new Filtered(ctx, payload, len); + return NullableMessageRef(new Filtered(ctx, payload, len)); } -Message *Filtered::decodeWrapped() const +NullableMessageRef Filtered::decodeWrapped() const { // Note: technically non-opaque messages could be wrapped as well, // but in practice they never are. Non-opaque messages are filtered diff --git a/src/shared/net/recording.h b/src/shared/net/recording.h index 31a54f54b..6157476e4 100644 --- a/src/shared/net/recording.h +++ b/src/shared/net/recording.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2014-2018 Calle Laakkonen + Copyright (C) 2014-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,11 +19,11 @@ #ifndef DP_NET_RECORDING_H #define DP_NET_RECORDING_H +#include "message.h" + #include #include -#include "message.h" - namespace protocol { /** @@ -94,7 +94,7 @@ class Filtered : public Message Filtered(uint8_t ctx, uchar *payload, int payloadLen); ~Filtered(); - static Message *deserialize(uint8_t ctx, const uchar *data, uint len); + static NullableMessageRef deserialize(uint8_t ctx, const uchar *data, uint len); // Note: this type has no fromText function since it is serialized as a comment /** @@ -106,7 +106,7 @@ class Filtered : public Message * * @return Message or nullptr if data is invalid */ - Message *decodeWrapped() const; + NullableMessageRef decodeWrapped() const; /** * @brief Get the type of the wrapped message diff --git a/src/shared/net/textmode.cpp b/src/shared/net/textmode.cpp index aa296fe72..98009564d 100644 --- a/src/shared/net/textmode.cpp +++ b/src/shared/net/textmode.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2017 Calle Laakkonen + Copyright (C) 2017-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -21,7 +21,7 @@ #include "meta.h" #include "meta2.h" -#include "pen.h" +#include "brushes.h" #include "annotation.h" #include "layer.h" #include "image.h" @@ -31,28 +31,6 @@ namespace protocol { namespace text { -static PenPoint parsePenPoint(const QStringList &tokens, bool *ok) -{ - if(tokens.length() < 2 || tokens.length() > 3) { - *ok = false; - return PenPoint(0, 0, 0); - } - - float x = tokens[0].toFloat(ok); - if(!ok) - return PenPoint(0, 0, 0); - float y = tokens[1].toFloat(ok); - if(!ok) - return PenPoint(0, 0, 0); - float p=1; - if(tokens.length()==3) { - p = tokens[2].toFloat(ok) / 100.0; - if(!ok) - return PenPoint(0, 0, 0); - } - return PenPoint(qRound(x*4), qRound(y*4), qRound(p*0xffff)); -} - Parser::Result Parser::parseLine(const QString &line) { switch(m_state) { @@ -91,7 +69,7 @@ Parser::Result Parser::parseLine(const QString &line) // Get message name m_cmd = tokens[1]; m_kwargs = Kwargs(); - m_points = PenPointVector(); + m_dabs = QStringList(); // Check if this is a multiline message bool multiline = false; @@ -100,33 +78,19 @@ Parser::Result Parser::parseLine(const QString &line) multiline = true; } - if(m_cmd == "penmove") { - // Extract Pen Point (special case for penmove message) - // If this is a multiline penpoint, the first point can be in the body part - if(!multiline || tokens.size()>2) { - bool ok; - m_points << parsePenPoint(tokens.mid(2), &ok); - if(!ok) { - m_error = "Invalid pen point: " + tokens.mid(2).join(' '); - return Result { Result::Error, nullptr }; - } - } - - } else { - // Extract named arguments - for(int i=2;ieof = false; } -static protocol::Message *readTextMessage(QIODevice *file, bool *eof) +static protocol::NullableMessageRef readTextMessage(QIODevice *file, bool *eof) { Parser parser; while(1) { @@ -452,13 +450,12 @@ bool Reader::readNextToBuffer(QByteArray &buffer) } } else { - protocol::Message *msg = readTextMessage(d->file, &d->eof); - if(!msg) + protocol::NullableMessageRef msg = readTextMessage(d->file, &d->eof); + if(msg.isNull()) return false; if(buffer.length() < msg->length()) buffer.resize(msg->length()); msg->serialize(buffer.data()); - delete msg; } ++d->current; @@ -469,41 +466,36 @@ bool Reader::readNextToBuffer(QByteArray &buffer) MessageRecord Reader::readNext() { Q_ASSERT(d->encoding != Encoding::Autodetect); - MessageRecord msg; if(d->encoding == Encoding::Binary) { if(!readNextToBuffer(d->msgbuf)) - return msg; + return MessageRecord::Eor(); - protocol::Message *message; + protocol::NullableMessageRef message; message = protocol::Message::deserialize((const uchar*)d->msgbuf.constData(), d->msgbuf.length(), !d->opaque); - if(message) { - msg.status = MessageRecord::OK; - msg.message = message; - } else { - msg.status = MessageRecord::INVALID; - msg.error.len = protocol::Message::sniffLength(d->msgbuf.constData()); - msg.error.type = protocol::MessageType(d->msgbuf.at(2)); - } + if(message.isNull()) + return MessageRecord::Invalid( + protocol::Message::sniffLength(d->msgbuf.constData()), + protocol::MessageType(d->msgbuf.at(2)) + ); + else + return MessageRecord::Ok(message); } else { d->currentPos = filePosition(); - protocol::Message *message = readTextMessage(d->file, &d->eof); + protocol::NullableMessageRef message = readTextMessage(d->file, &d->eof); if(!d->eof) { - if(message) { - msg.status = MessageRecord::OK; - msg.message = message; - ++d->current; - } else { - msg.status = MessageRecord::INVALID; - msg.error.len = 0; - msg.error.type = protocol::MSG_COMMAND; - } + if(message.isNull()) + return MessageRecord::Invalid(0, protocol::MSG_COMMAND); + + ++d->current; + return MessageRecord::Ok(message); } } - return msg; + return MessageRecord::Eor(); } } + diff --git a/src/shared/record/reader.h b/src/shared/record/reader.h index e11e77379..530361a20 100644 --- a/src/shared/record/reader.h +++ b/src/shared/record/reader.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2014-2016 Calle Laakkonen + Copyright (C) 2014-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -45,17 +45,18 @@ enum Compatibility { }; struct MessageRecord { - MessageRecord() : status(END_OF_RECORDING), message(0) {} + static MessageRecord Ok(protocol::NullableMessageRef msg) { return MessageRecord { OK, msg, 0, protocol::MSG_COMMAND }; } + static MessageRecord Invalid(int len, protocol::MessageType type) { return MessageRecord { INVALID, nullptr, len, type }; } + static MessageRecord Eor() { return MessageRecord { END_OF_RECORDING, nullptr, 0, protocol::MSG_COMMAND }; } enum { OK, INVALID, END_OF_RECORDING } status; - struct MessageRecordError { - int len; - protocol::MessageType type; - }; - union { - protocol::Message *message; - MessageRecordError error; - }; + + // The message (if status is OK) + protocol::NullableMessageRef message; + + // These are set if status is INVALID + int invalid_len; + protocol::MessageType invalid_type; }; /** diff --git a/src/shared/record/writer.cpp b/src/shared/record/writer.cpp index 020c86bab..18a0c25b5 100644 --- a/src/shared/record/writer.cpp +++ b/src/shared/record/writer.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2014-2018 Calle Laakkonen + Copyright (C) 2014-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -125,8 +125,9 @@ void Writer::writeFromBuffer(const QByteArray &buffer) const int len = protocol::Message::sniffLength(buffer.constData()); Q_ASSERT(len <= buffer.length()); m_file->write(buffer.constData(), len); + } else { - protocol::Message *msg = protocol::Message::deserialize(reinterpret_cast(buffer.constData()), buffer.length(), true); + protocol::NullableMessageRef msg = protocol::Message::deserialize(reinterpret_cast(buffer.constData()), buffer.length(), true); m_file->write(msg->toString().toUtf8()); m_file->write("\n", 1); } @@ -148,15 +149,16 @@ bool Writer::writeMessage(const protocol::Message &msg) // Special case: Filtered messages are // written as comments in the text format. const protocol::Filtered &fm = static_cast(msg); - std::unique_ptr wrapped { fm.decodeWrapped() }; - QString comment; - if(wrapped) { - comment = QStringLiteral("FILTERED: ") + wrapped->toString(); + auto wrapped = fm.decodeWrapped(); - } else { + QString comment; + if(wrapped.isNull()) { comment = QStringLiteral("FILTERED: undecodable message type #%1 of length %2") .arg(fm.wrappedType()) .arg(fm.wrappedPayloadLength()); + + } else { + comment = QStringLiteral("FILTERED: ") + wrapped->toString(); } return writeComment(comment); diff --git a/src/shared/server/client.cpp b/src/shared/server/client.cpp index c59b9396c..c608a2eae 100644 --- a/src/shared/server/client.cpp +++ b/src/shared/server/client.cpp @@ -48,16 +48,18 @@ struct Client::Private { int id; QString username; QString extAuthId; + QByteArray avatar; bool isOperator; bool isModerator; + bool isTrusted; bool isAuthenticated; bool isMuted; Private(QTcpSocket *socket, ServerLog *logger) : socket(socket), logger(logger), msgqueue(nullptr), historyPosition(-1), id(0), - isOperator(false), isModerator(false), isAuthenticated(false), isMuted(false) + isOperator(false), isModerator(false), isTrusted(false), isAuthenticated(false), isMuted(false) { Q_ASSERT(socket); Q_ASSERT(logger); @@ -86,7 +88,8 @@ protocol::MessagePtr Client::joinMessage() const return protocol::MessagePtr(new protocol::UserJoin( id(), (isAuthenticated() ? protocol::UserJoin::FLAG_AUTH : 0) | (isModerator() ? protocol::UserJoin::FLAG_MOD : 0), - username() + username(), + avatar() )); } @@ -176,6 +179,16 @@ const QString &Client::username() const return d->username; } +void Client::setAvatar(const QByteArray &avatar) +{ + d->avatar = avatar; +} + +const QByteArray &Client::avatar() const +{ + return d->avatar; +} + const QString &Client::extAuthId() const { return d->extAuthId; @@ -196,6 +209,11 @@ bool Client::isOperator() const return d->isOperator || d->isModerator; } +bool Client::isDeputy() const +{ + return !isOperator() && isTrusted() && d->session && d->session->isDeputies(); +} + void Client::setModerator(bool mod) { d->isModerator = mod; @@ -206,6 +224,16 @@ bool Client::isModerator() const return d->isModerator; } +bool Client::isTrusted() const +{ + return d->isTrusted; +} + +void Client::setTrusted(bool trusted) +{ + d->isTrusted = trusted; +} + void Client::setAuthenticated(bool auth) { d->isAuthenticated = auth; @@ -345,6 +373,7 @@ void Client::handleSessionMessage(MessagePtr msg) using namespace protocol; case MSG_USER_JOIN: case MSG_USER_LEAVE: + case MSG_SOFTRESET: log(Log().about(Log::Level::Warn, Log::Topic::RuleBreak).message("Received server-to-user only command " + msg->messageName())); return; case MSG_DISCONNECT: @@ -383,7 +412,30 @@ void Client::handleSessionMessage(MessagePtr msg) d->session->directToAll(msg); return; } + break; } + case protocol::MSG_PRIVATE_CHAT: { + const protocol::PrivateChat &chat = msg.cast(); + if(chat.target()>0) { + Client *c = d->session->getClientById(chat.target()); + if(c) { + this->sendDirectMessage(msg); + c->sendDirectMessage(msg); + } + } + return; + } + case protocol::MSG_TRUSTED_USERS: { + if(!isOperator()) { + log(Log().about(Log::Level::Warn, Log::Topic::RuleBreak).message("Tried to change trusted user list")); + return; + } + + QList ids = msg.cast().ids(); + ids = d->session->updateTrustedUsers(ids, username()); + msg.cast().setIds(ids); + break; + } default: break; } @@ -400,8 +452,8 @@ void Client::handleSessionMessage(MessagePtr msg) void Client::disconnectKick(const QString &kickedBy) { - emit loggedOff(this); log(Log().about(Log::Level::Info, Log::Topic::Kick).message("Kicked by " + kickedBy)); + emit loggedOff(this); d->msgqueue->sendDisconnect(protocol::Disconnect::KICK, kickedBy); } diff --git a/src/shared/server/client.h b/src/shared/server/client.h index 4f3d20b49..21f10e92f 100644 --- a/src/shared/server/client.h +++ b/src/shared/server/client.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2013-2018 Calle Laakkonen + Copyright (C) 2013-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -77,6 +77,15 @@ class Client : public QObject const QString &username() const; void setUsername(const QString &username); + /** + * @brief Get this user's avatar. + * + * The avatar should be a PNG image. + * @return + */ + const QByteArray &avatar() const; + void setAvatar(const QByteArray &avatar); + /** * @brief Get the ext-auth server's ID for this user * @@ -94,6 +103,13 @@ class Client : public QObject bool isOperator() const; void setOperator(bool op); + /** + * @brief Is this user a deputy (but not an operator) + * + * Deputies have more limited permissions than operators. + */ + bool isDeputy() const; + /** * @brief Is this user a moderator? * Moderators can access any session, always have OP status and cannot be kicked by other users. @@ -101,6 +117,15 @@ class Client : public QObject bool isModerator() const; void setModerator(bool mod); + /** + * @brief Is this a trusted user? + * + * The trust flag is granted by session operators. It's effects are purely clientside, + * but the server is aware of it so it can remember it for authenticated users. + */ + bool isTrusted() const; + void setTrusted(bool trusted); + /** * @brief Has this user been authenticated? */ diff --git a/src/shared/server/filedhistory.cpp b/src/shared/server/filedhistory.cpp index d42bebc45..c01f80c8d 100644 --- a/src/shared/server/filedhistory.cpp +++ b/src/shared/server/filedhistory.cpp @@ -221,6 +221,9 @@ bool FiledHistory::load() } else if(cmd == "MAXUSERS") { m_maxUsers = qBound(1, params.toInt(), 254); + } else if(cmd == "AUTORESET") { + m_autoResetThreshold = params.toUInt(); + } else if(cmd == "TITLE") { m_title = params; @@ -233,6 +236,8 @@ bool FiledHistory::load() flags |= PreserveChat; else if(f == "nsfm") flags |= Nsfm; + else if(f == "deputies") + flags |= Deputies; else qWarning() << id().toString() << "unknown flag:" << QString::fromUtf8(f); } @@ -277,6 +282,12 @@ bool FiledHistory::load() } else if(cmd == "DEOP") { m_ops.remove(QString::fromUtf8(params)); + } else if(cmd == "TRUST") { + m_trusted.insert(QString::fromUtf8(params)); + + } else if(cmd == "UNTRUST") { + m_trusted.remove(QString::fromUtf8(params)); + } else { qWarning() << id().toString() << "unknown journal entry:" << QString::fromUtf8(cmd); } @@ -477,6 +488,16 @@ void FiledHistory::setMaxUsers(int max) } } +void FiledHistory::setAutoResetThreshold(uint limit) +{ + const uint newLimit = qMin(uint(sizeLimit() * 0.7), limit); + if(newLimit != m_autoResetThreshold) { + m_autoResetThreshold = newLimit; + m_journal->write(QString("AUTORESET %1\n").arg(newLimit).toUtf8()); + m_journal->flush(); + } +} + void FiledHistory::setTitle(const QString &title) { if(title != m_title) { @@ -497,6 +518,8 @@ void FiledHistory::setFlags(Flags f) fstr << "preserveChat"; if(f.testFlag(Nsfm)) fstr << "nsfm"; + if(f.testFlag(Deputies)) + fstr << "deputies"; m_journal->write(QString("FLAGS %1\n").arg(fstr.join(' ')).toUtf8()); m_journal->flush(); } @@ -542,13 +565,13 @@ std::tuple, int> FiledHistory::getBatch(int after) c m_recording->close(); break; } - protocol::Message *msg = protocol::Message::deserialize((const uchar*)buffer.constData(), buffer.length(), false); - if(!msg) { + protocol::NullableMessageRef msg = protocol::Message::deserialize((const uchar*)buffer.constData(), buffer.length(), false); + if(msg.isNull()) { qWarning() << m_recording->fileName() << "Invalid message in block" << i; m_recording->close(); break; } - const_cast(b).messages << protocol::MessagePtr(msg); + const_cast(b).messages << protocol::MessagePtr::fromNullable(msg); } m_recording->seek(prevPos); @@ -669,4 +692,21 @@ void FiledHistory::setAuthenticatedOperator(const QString &username, bool op) } } +void FiledHistory::setAuthenticatedTrust(const QString &username, bool trusted) +{ + if(trusted) { + if(!m_trusted.contains(username)) { + m_trusted.insert(username); + m_journal->write(QString("TRUST %1\n").arg(username).toUtf8()); + m_journal->flush(); + } + } else { + if(m_trusted.contains(username)) { + m_trusted.remove(username); + m_journal->write(QString("UNTRUST %1\n").arg(username).toUtf8()); + m_journal->flush(); + } + } +} + } diff --git a/src/shared/server/filedhistory.h b/src/shared/server/filedhistory.h index b274d78b4..f7ca9b52c 100644 --- a/src/shared/server/filedhistory.h +++ b/src/shared/server/filedhistory.h @@ -80,6 +80,7 @@ class FiledHistory : public SessionHistory QByteArray opwordHash() const override { return m_opword; } int maxUsers() const override { return m_maxUsers; } QString title() const override { return m_title; } + uint autoResetThreshold() const override { return m_autoResetThreshold; } Flags flags() const override { return m_flags; } QDateTime startTime() const override; @@ -88,6 +89,7 @@ class FiledHistory : public SessionHistory void setMaxUsers(int max) override; void setTitle(const QString &title) override; void setFlags(Flags f) override; + void setAutoResetThreshold(uint limit) override; void joinUser(uint8_t id, const QString &name) override; void terminate() override; @@ -99,7 +101,9 @@ class FiledHistory : public SessionHistory QStringList announcements() const override { return m_announcements; } void setAuthenticatedOperator(const QString &username, bool op) override; + void setAuthenticatedTrust(const QString &username, bool trusted) override; bool isOperator(const QString &username) const override { return m_ops.contains(username); } + bool isTrusted(const QString &username) const override { return m_trusted.contains(username); } bool isAuthenticatedOperators() const override { return !m_ops.isEmpty(); } protected: @@ -139,9 +143,11 @@ class FiledHistory : public SessionHistory QByteArray m_password; QByteArray m_opword; int m_maxUsers; + uint m_autoResetThreshold; Flags m_flags; QStringList m_announcements; QSet m_ops; + QSet m_trusted; QVector m_blocks; bool m_archive; diff --git a/src/shared/server/inmemoryhistory.cpp b/src/shared/server/inmemoryhistory.cpp index 6336685df..02f4d415b 100644 --- a/src/shared/server/inmemoryhistory.cpp +++ b/src/shared/server/inmemoryhistory.cpp @@ -29,6 +29,7 @@ InMemoryHistory::InMemoryHistory(const QUuid &id, const QString &alias, const pr m_version(version), m_startTime(QDateTime::currentDateTime()), m_maxUsers(254), + m_autoReset(0), m_flags(0) { } diff --git a/src/shared/server/inmemoryhistory.h b/src/shared/server/inmemoryhistory.h index 94c429dc0..5e227b932 100644 --- a/src/shared/server/inmemoryhistory.h +++ b/src/shared/server/inmemoryhistory.h @@ -54,13 +54,17 @@ class InMemoryHistory : public SessionHistory { void setTitle(const QString &title) override { m_title = title; } Flags flags() const override { return m_flags; } void setFlags(Flags f) override { m_flags = f; } + void setAutoResetThreshold(uint limit) override { m_autoReset = qMin(uint(sizeLimit() * 0.7), limit); } + uint autoResetThreshold() const override { return m_autoReset; } void addAnnouncement(const QString &url) override { m_announcements.insert(url); } void removeAnnouncement(const QString &url) override { m_announcements.remove(url); } QStringList announcements() const override { return m_announcements.values(); } void setAuthenticatedOperator(const QString &username, bool op) override { if(op) m_ops.insert(username); else m_ops.remove(username); } + void setAuthenticatedTrust(const QString &username, bool trusted) override { if(trusted) m_trusted.insert(username); else m_trusted.remove(username); } bool isOperator(const QString &username) const override { return m_ops.contains(username); } + bool isTrusted(const QString &username) const override { return m_trusted.contains(username); } bool isAuthenticatedOperators() const override { return !m_ops.isEmpty(); } protected: @@ -72,6 +76,7 @@ class InMemoryHistory : public SessionHistory { private: QList m_history; QSet m_ops; + QSet m_trusted; QSet m_announcements; QString m_alias; QString m_founder; @@ -81,6 +86,7 @@ class InMemoryHistory : public SessionHistory { QByteArray m_password; QByteArray m_opword; int m_maxUsers; + uint m_autoReset; Flags m_flags; }; diff --git a/src/shared/server/loginhandler.cpp b/src/shared/server/loginhandler.cpp index 02c6d46e0..cd4921e10 100644 --- a/src/shared/server/loginhandler.cpp +++ b/src/shared/server/loginhandler.cpp @@ -231,6 +231,11 @@ void LoginHandler::handleIdentMessage(const protocol::ServerCommand &cmd) return; } + if(cmd.kwargs.contains("avatar")) { + // TODO validate + m_client->setAvatar(QByteArray::fromBase64(cmd.kwargs["avatar"].toString().toUtf8())); + } + switch(userAccount.status) { case RegisteredUser::NotFound: { // Account not found in internal user list. Allow guest login (if enabled) diff --git a/src/shared/server/loginhandler.h b/src/shared/server/loginhandler.h index 75228283b..4b6f0825c 100644 --- a/src/shared/server/loginhandler.h +++ b/src/shared/server/loginhandler.h @@ -23,6 +23,7 @@ #include #include +#include namespace protocol { struct ServerCommand; diff --git a/src/shared/server/opcommands.cpp b/src/shared/server/opcommands.cpp index fabad5ce8..05da5253b 100644 --- a/src/shared/server/opcommands.cpp +++ b/src/shared/server/opcommands.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2014-2017 Calle Laakkonen + Copyright (C) 2014-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -48,9 +48,10 @@ typedef void (*SrvCommandFn)(Client *, const QJsonArray &, const QJsonObject &); class SrvCommand { public: enum Mode { - NONOP, // usable by all - OP, // needs OP privileges - MOD // needs MOD privileges + NONOP, // usable by all + DEPUTY, // needs at least deputy privileges + OP, // needs operator privileges + MOD // needs moderator privileges }; SrvCommand(const QString &name, SrvCommandFn fn, Mode mode=OP) @@ -76,6 +77,13 @@ struct SrvCommandSet { const SrvCommandSet COMMANDS; +void readyToAutoReset(Client *client, const QJsonArray &args, const QJsonObject &kwargs) +{ + Q_UNUSED(args); + Q_UNUSED(kwargs); + client->session()->readyToAutoReset(client->id()); +} + void initBegin(Client *client, const QJsonArray &args, const QJsonObject &kwargs) { Q_UNUSED(args); @@ -155,6 +163,11 @@ void kickUser(Client *client, const QJsonArray &args, const QJsonObject &kwargs) if(target->isModerator()) throw CmdError("cannot kick moderators"); + if(client->isDeputy()) { + if(target->isOperator() || target->isTrusted()) + throw CmdError("cannot kick trusted users"); + } + if(kwargs["ban"].toBool()) { client->session()->addBan(target, client->username()); client->session()->messageAll(target->username() + " banned by " + client->username(), false); @@ -248,11 +261,12 @@ void reportAbuse(Client *client, const QJsonArray &args, const QJsonObject &kwar SrvCommandSet::SrvCommandSet() { commands + << SrvCommand("ready-to-autoreset", readyToAutoReset) << SrvCommand("init-begin", initBegin) << SrvCommand("init-complete", initComplete) << SrvCommand("init-cancel", initCancel) << SrvCommand("sessionconf", sessionConf) - << SrvCommand("kick-user", kickUser) + << SrvCommand("kick-user", kickUser, SrvCommand::DEPUTY) << SrvCommand("gain-op", opWord, SrvCommand::NONOP) << SrvCommand("reset-session", resetSession) @@ -282,6 +296,10 @@ void handleClientServerCommand(Client *client, const QString &command, const QJs client->sendDirectMessage(protocol::Command::error("Not a session owner")); return; } + else if(c.mode() == SrvCommand::DEPUTY && !client->isOperator() && !client->isDeputy()) { + client->sendDirectMessage(protocol::Command::error("Not a session owner or a deputy")); + return; + } try { c.call(client, args, kwargs); diff --git a/src/shared/server/serverconfig.h b/src/shared/server/serverconfig.h index 2d9000ff5..30a553d3c 100644 --- a/src/shared/server/serverconfig.h +++ b/src/shared/server/serverconfig.h @@ -54,7 +54,7 @@ class ConfigKey { namespace config { static const ConfigKey ClientTimeout(0, "clientTimeout", "60", ConfigKey::TIME), // Connection ping timeout for clients - SessionSizeLimit(1, "sessionSizeLimit", "15mb", ConfigKey::SIZE), // Session history size limit in bytes (int) + SessionSizeLimit(1, "sessionSizeLimit", "99mb", ConfigKey::SIZE), // Session history size limit in bytes SessionCountLimit(2, "sessionCountLimit", "25", ConfigKey::INT), // Maximum number of active sessions (int) EnablePersistence(3, "persistence", "false", ConfigKey::BOOL), // Enable session persistence (bool) AllowGuestHosts(4, "allowGuestHosts", "true", ConfigKey::BOOL), // Allow guests (or users without the HOST flag) to host sessions @@ -71,7 +71,8 @@ namespace config { ExtAuthFallback(15, "extauthfallback", "true", ConfigKey::BOOL), // Fall back to guest logins if ext auth server is unreachable ExtAuthMod(16, "extauthmod", "true", ConfigKey::BOOL), // Respect ext-auth user's "MOD" flag ReportToken(17, "reporttoken", "", ConfigKey::STRING), // Abuse report backend server authorization token - LogPurgeDays(18, "logpurgedays", "0", ConfigKey::INT) // Automatically purge log entries older than this many days (DB log only) + LogPurgeDays(18, "logpurgedays", "0", ConfigKey::INT), // Automatically purge log entries older than this many days (DB log only) + AutoresetThreshold(19, "autoResetThreshold", "15mb", ConfigKey::SIZE) // Default autoreset threshold in bytes ; } diff --git a/src/shared/server/serverlog.h b/src/shared/server/serverlog.h index b992fb5de..796b1cb72 100644 --- a/src/shared/server/serverlog.h +++ b/src/shared/server/serverlog.h @@ -52,6 +52,8 @@ class Log { Deop, // OP status was removed Mute, // User was muted Unmute, // User was unmuted + Trust, // User was tagged as trusted + Untrust, // User's trusted tag was removed BadData, // Received an invalid message from a client RuleBreak, // User tried to use a command they're not allowed to PubList, // Session announcement diff --git a/src/shared/server/session.cpp b/src/shared/server/session.cpp index 23cc4fb6c..2d55e7e59 100644 --- a/src/shared/server/session.cpp +++ b/src/shared/server/session.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2008-2018 Calle Laakkonen + Copyright (C) 2008-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -48,15 +48,13 @@ Session::Session(SessionHistory *history, ServerConfig *config, QObject *parent) m_recorder(nullptr), m_history(history), m_resetstreamsize(0), - m_publicListingClient(nullptr), - m_refreshTimer(nullptr), m_closed(false), m_authOnly(false), - m_historyLimitWarningSent(false) + m_autoResetRequestStatus(AutoResetState::NotSent) { m_history->setParent(this); m_history->setSizeLimit(config->getConfigSize(config::SessionSizeLimit)); - m_historyLimitWarning = m_history->sizeLimit() * 0.7; + m_history->setAutoResetThreshold(config->getConfigSize(config::AutoresetThreshold)); m_lastEventTime.start(); m_lastStatusUpdate.start(); @@ -69,6 +67,12 @@ Session::Session(SessionHistory *history, ServerConfig *config, QObject *parent) sendUpdatedSessionProperties(); } + // Session announcements + m_refreshTimer = new QTimer(this); + m_refreshTimer->setSingleShot(true); + m_refreshTimer->setTimerType(Qt::VeryCoarseTimer); + connect(m_refreshTimer, &QTimer::timeout, this, &Session::refreshAnnouncements); + for(const QString &announcement : m_history->announcements()) makeAnnouncement(QUrl(announcement), false); } @@ -101,11 +105,16 @@ void Session::switchState(State newstate) // Add list of currently logged in users to reset snapshot QList owners; + QList trusted; for(const Client *c : m_clients) { m_resetstream.prepend(c->joinMessage()); if(c->isOperator()) owners << c->id(); + if(c->isTrusted()) + trusted << c->id(); } + if(!trusted.isEmpty()) + m_resetstream.prepend(protocol::MessagePtr(new protocol::TrustedUsers(0, trusted))); m_resetstream.prepend(protocol::MessagePtr(new protocol::SessionOwner(0, owners))); // Send reset snapshot @@ -127,7 +136,7 @@ void Session::switchState(State newstate) catchup.reply["count"] = m_history->lastIndex() - m_history->firstIndex(); directToAll(MessagePtr(new protocol::Command(0, catchup))); - m_historyLimitWarningSent = false; + m_autoResetRequestStatus = AutoResetState::NotSent; sendUpdatedSessionProperties(); } @@ -207,16 +216,10 @@ void Session::joinUser(Client *user, bool host) if(user->isOperator() || m_history->isOperator(user->username())) changeOpStatus(user->id(), true, "the server"); - ensureOperatorExists(); + if(m_history->isTrusted(user->username())) + changeTrustedStatus(user->id(), true, "the server"); - // Let new users know about the size limit too - if(m_historyLimitWarning > 0 && m_history->sizeInBytes() > m_historyLimitWarning) { - protocol::ServerReply warning; - warning.type = protocol::ServerReply::SIZELIMITWARNING; - warning.reply["size"] = int(m_history->sizeInBytes()); - warning.reply["maxSize"] = int(m_historyLimitWarning); - user->sendDirectMessage(protocol::MessagePtr(new protocol::Command(0, warning))); - } + ensureOperatorExists(); // Make sure everyone is up to date sendUpdatedAnnouncementList(); @@ -369,6 +372,11 @@ void Session::setSessionConfig(const QJsonObject &conf, Client *changedBy) changes << "changed max. user count"; } + if(conf.contains("resetThreshold")) { + m_history->setAutoResetThreshold(conf["resetThreshold"].toInt()); + changes << "changed autoreset threshold"; + } + if(conf.contains("password")) { setPassword(conf["password"].toString()); changes << "changed password"; @@ -392,6 +400,11 @@ void Session::setSessionConfig(const QJsonObject &conf, Client *changedBy) changes << (conf["nsfm"].toBool() ? "tagged NSFM" : "removed NSFM tag"); } + if(conf.contains("deputies")) { + setFlag(flags, SessionHistory::Deputies, conf["deputies"].toBool()); + changes << (conf["deputies"].toBool() ? "enabled deputies" : "disabled deputies"); + } + m_history->setFlags(flags); if(!changes.isEmpty()) { @@ -490,6 +503,60 @@ void Session::changeOpStatus(int id, bool op, const QString &changedBy) kickResetter->disconnectError("De-opped while resetting"); } +QList Session::updateTrustedUsers(QList ids, const QString &changedBy) +{ + QList truelist; + for(Client *c : m_clients) { + const bool trusted = ids.contains(c->id()); + if(trusted != c->isTrusted()) { + c->setTrusted(trusted); + QString msg; + if(trusted) { + msg = "Trusted by " + changedBy; + c->log(Log().about(Log::Level::Info, Log::Topic::Trust).message(msg)); + } else { + msg = "Untrusted by " + changedBy; + c->log(Log().about(Log::Level::Info, Log::Topic::Untrust).message(msg)); + } + messageAll(c->username() + " " + msg, false); + if(c->isAuthenticated()) + m_history->setAuthenticatedTrust(c->username(), trusted); + + } + if(c->isTrusted()) + truelist << c->id(); + } + + return truelist; +} + +void Session::changeTrustedStatus(int id, bool trusted, const QString &changedBy) +{ + QList ids; + + for(Client *c : m_clients) { + if(c->id() == id && c->isTrusted() != trusted) { + c->setTrusted(trusted); + QString msg; + if(trusted) { + msg = "Trusted by " + changedBy; + c->log(Log().about(Log::Level::Info, Log::Topic::Trust).message(msg)); + } else { + msg = "Untrusted by " + changedBy; + c->log(Log().about(Log::Level::Info, Log::Topic::Untrust).message(msg)); + } + messageAll(c->username() + " " + msg, false); + if(c->isAuthenticated()) + m_history->setAuthenticatedTrust(c->username(), trusted); + } + + if(c->isTrusted()) + ids << c->id(); + } + + addToHistory(protocol::MessagePtr(new protocol::TrustedUsers(0, ids))); +} + void Session::sendUpdatedSessionProperties() { protocol::ServerReply props; @@ -500,8 +567,11 @@ void Session::sendUpdatedSessionProperties() conf["persistent"] = isPersistent(); conf["title"] = title(); conf["maxUserCount"] = m_history->maxUsers(); + conf["resetThreshold"] = int(m_history->autoResetThreshold()); + conf["resetThresholdBase"] = int(m_history->autoResetThresholdBase()); conf["preserveChat"] = m_history->flags().testFlag(SessionHistory::PreserveChat); conf["nsfm"] = m_history->flags().testFlag(SessionHistory::Nsfm); + conf["deputies"] = m_history->flags().testFlag(SessionHistory::Deputies); conf["hasPassword"] = hasPassword(); conf["hasOpword"] = hasOpword(); props.reply["config"] = conf; @@ -604,18 +674,42 @@ void Session::addToHistory(const protocol::MessagePtr &msg) m_recorder->recordMessage(msg); m_lastEventTime.start(); - // Send a warning if approaching size limit. - // The clients should update their internal size limits and reset the session when necessary. - if(m_historyLimitWarning>0 && !m_historyLimitWarningSent && m_history->sizeInBytes() > m_historyLimitWarning) { - log(Log().about(Log::Level::Warn, Log::Topic::Status).message("History limit warning treshold reached.")); + // Request auto-reset when threshold is crossed. + const uint autoResetThreshold = m_history->effectiveAutoResetThreshold(); + if(autoResetThreshold>0 && m_autoResetRequestStatus == AutoResetState::NotSent && m_history->sizeInBytes() > autoResetThreshold) { + log(Log().about(Log::Level::Info, Log::Topic::Status).message( + QString("Autoreset threshold (%1, effectively %2 MB) reached.") + .arg(m_history->autoResetThreshold()/(1024.0*1024.0), 0, 'g', 1) + .arg(autoResetThreshold/(1024.0*1024.0), 0, 'g', 1) + )); + // Legacy alert for Drawpile 2.0.x versions protocol::ServerReply warning; warning.type = protocol::ServerReply::SIZELIMITWARNING; warning.reply["size"] = int(m_history->sizeInBytes()); - warning.reply["maxSize"] = int(m_historyLimitWarning); + warning.reply["maxSize"] = int(autoResetThreshold); directToAll(protocol::MessagePtr(new protocol::Command(0, warning))); - m_historyLimitWarningSent = true; + + // New style for Drawpile 2.1.0 and newer + // Autoreset request: send an autoreset query to each logged in user. + // The user that responds first gets to perform the reset. + protocol::ServerReply resetRequest; + resetRequest.type = protocol::ServerReply::RESETREQUEST; + resetRequest.reply["maxSize"] = int(m_history->sizeLimit()); + resetRequest.reply["query"] = true; + protocol::MessagePtr reqMsg { new protocol::Command(0, resetRequest )}; + + // The request is sent only to clients that are fully caught up. + // Otherwise, a freshly joined client could end up rolling back + // the canvas to an earlier state. + const int uptodateThreshold = m_history->lastIndex() - 1; + for(Client *c : m_clients) { + if(c->isOperator() && c->historyPosition() >= uptodateThreshold) + c->sendDirectMessage(reqMsg); + } + + m_autoResetRequestStatus = AutoResetState::Queried; } // Regular history size status updates @@ -648,6 +742,39 @@ void Session::addToInitStream(protocol::MessagePtr msg) } } +void Session::readyToAutoReset(int ctxId) +{ + Client *c = getClientById(ctxId); + if(!c) { + // Shouldn't happen + log(Log().about(Log::Level::Error, Log::Topic::RuleBreak).message(QString("Non-existent user %1 sent ready-to-autoreset").arg(ctxId))); + return; + } + + if(!c->isOperator()) { + // Unlikely to happen normally, but possible if connection is + // really slow and user is deopped at just the right moment + log(Log().about(Log::Level::Warn, Log::Topic::RuleBreak).message(QString("User %1 is not an operator, but sent ready-to-autoreset").arg(ctxId))); + return; + } + + if(m_autoResetRequestStatus != AutoResetState::Queried) { + // Only the first response in handled + log(Log().about(Log::Level::Debug, Log::Topic::Status).message(QString("User %1 was late to respond to an autoreset request").arg(ctxId))); + return; + } + + log(Log().about(Log::Level::Info, Log::Topic::Status).message(QString("User %1 responded to autoreset request first").arg(ctxId))); + + protocol::ServerReply resetRequest; + resetRequest.type = protocol::ServerReply::RESETREQUEST; + resetRequest.reply["maxSize"] = int(m_history->sizeLimit()); + resetRequest.reply["query"] = false; + c->sendDirectMessage(protocol::MessagePtr { new protocol::Command(0, resetRequest )}); + + m_autoResetRequestStatus = AutoResetState::Requested; +} + void Session::handleInitBegin(int ctxId) { Client *c = getClientById(ctxId); @@ -875,27 +1002,6 @@ QStringList Session::userNames() const return lst; } -sessionlisting::AnnouncementApi *Session::publicListingClient() -{ - if(!m_publicListingClient) { - m_publicListingClient = new sessionlisting::AnnouncementApi(this); - connect(m_publicListingClient, &sessionlisting::AnnouncementApi::sessionAnnounced, this, &Session::sessionAnnounced); - connect(m_publicListingClient, &sessionlisting::AnnouncementApi::error, this, &Session::sessionAnnouncementError); - connect(m_publicListingClient, &sessionlisting::AnnouncementApi::messageReceived, this, [this](const QString &message) { - log(Log().about(Log::Level::Info, Log::Topic::PubList).message(message)); - this->messageAll(message, false); - }); - connect(m_publicListingClient, &sessionlisting::AnnouncementApi::logMessage, this, &Session::log); - - m_refreshTimer = new QTimer(this); - m_refreshTimer->setSingleShot(true); - m_refreshTimer->setTimerType(Qt::VeryCoarseTimer); - connect(m_refreshTimer, &QTimer::timeout, this, &Session::refreshAnnouncements); - } - - return m_publicListingClient; -} - void Session::makeAnnouncement(const QUrl &url, bool privateListing) { if(!url.isValid() || !m_config->isAllowedAnnouncementUrl(url)) { @@ -930,13 +1036,48 @@ void Session::makeAnnouncement(const QUrl &url, bool privateListing) (hasPassword() || privateUserList) ? QStringList() : userNames(), hasPassword(), isNsfm(), - privateListing ? sessionlisting::PrivateMode::Private : sessionlisting::PrivateMode::Public, + privateListing ? sessionlisting::PrivacyMode::Private : sessionlisting::PrivacyMode::Public, founder(), sessionStartTime() }; - log(Log().about(Log::Level::Debug, Log::Topic::PubList).message("Announcing session at " + url.toString())); - publicListingClient()->announceSession(url, s); + const QString apiUrl = url.toString(); + log(Log().about(Log::Level::Info, Log::Topic::PubList).message("Announcing session at at " + apiUrl)); + auto *response = sessionlisting::announceSession(url, s); + + connect(response, &sessionlisting::AnnouncementApiResponse::finished, this, [apiUrl, response, this](const QVariant &result, const QString &message, const QString &error) { + response->deleteLater(); + if(!error.isEmpty()) { + log(Log().about(Log::Level::Warn, Log::Topic::PubList).message(apiUrl + ": announcement failed: " + error)); + messageAll(error, false); + return; + } + + if(!message.isEmpty()) { + log(Log().about(Log::Level::Info, Log::Topic::PubList).message(message)); + messageAll(message, false); + } + + const sessionlisting::Announcement announcement = result.value(); + + // Make sure there are no double announcements + for(const sessionlisting::Announcement &a : m_publicListings) { + if(a.apiUrl == announcement.apiUrl) { + log(Log().about(Log::Level::Warn, Log::Topic::PubList).message("Double announcement at: " + announcement.apiUrl.toString())); + return; + } + } + + log(Log().about(Log::Level::Info, Log::Topic::PubList).message("Announced at: " + announcement.apiUrl.toString())); + if(!announcement.isPrivate) + m_history->addAnnouncement(announcement.apiUrl.toString()); + m_publicListings << announcement; + sendUpdatedAnnouncementList(); + + int timeout = announcement.refreshInterval * 60 * 1000; + if(!m_refreshTimer->isActive() || m_refreshTimer->remainingTime() > timeout) + m_refreshTimer->start(timeout); + }); } void Session::unlistAnnouncement(const QString &url, bool terminate, bool removeOnly) @@ -946,8 +1087,19 @@ void Session::unlistAnnouncement(const QString &url, bool terminate, bool remove while(i.hasNext()) { const sessionlisting::Announcement &a = i.next(); if(a.apiUrl == url || url == QStringLiteral("*")) { - if(!removeOnly) - publicListingClient()->unlistSession(a); + if(!removeOnly) { + log(Log().about(Log::Level::Info, Log::Topic::PubList).message(QStringLiteral("Unlisting announcement at ") + url)); + + auto *response = sessionlisting::unlistSession(a); + connect(response, &sessionlisting::AnnouncementApiResponse::finished, this, [response, this](const QVariant &result, const QString &message, const QString &error) { + Q_UNUSED(result); + Q_UNUSED(message); + response->deleteLater(); + if(!error.isEmpty()) { + log(Log().about(Log::Level::Warn, Log::Topic::PubList).message("Session unlisting failed")); + } + }); + } if(terminate) m_history->removeAnnouncement(a.apiUrl.toString()); @@ -967,7 +1119,7 @@ void Session::refreshAnnouncements() int timeout = 0; for(const sessionlisting::Announcement &a : m_publicListings) { - m_publicListingClient->refreshSession(a, { + auto *response = sessionlisting::refreshSession(a, { QString(), // cannot change 0, // cannot change QString(), // cannot change @@ -977,46 +1129,35 @@ void Session::refreshAnnouncements() hasPassword() || privateUserList ? QStringList() : userNames(), hasPassword(), isNsfm(), - a.isPrivate ? sessionlisting::PrivateMode::Private : sessionlisting::PrivateMode::Public, + a.isPrivate ? sessionlisting::PrivacyMode::Private : sessionlisting::PrivacyMode::Public, founder(), sessionStartTime() }); timeout = qMax(timeout, a.refreshInterval); - } - if(timeout > 0) { - m_refreshTimer->start(timeout * 60 * 1000); - } -} - -void Session::sessionAnnounced(const sessionlisting::Announcement &announcement) -{ - // Make sure there are no double announcements - for(const sessionlisting::Announcement &a : m_publicListings) { - if(a.apiUrl == announcement.apiUrl) { - log(Log().about(Log::Level::Warn, Log::Topic::PubList).message("Double announcement at: " + announcement.apiUrl.toString())); - return; - } - } + const QString apiUrl = a.apiUrl.toString(); - log(Log().about(Log::Level::Info, Log::Topic::PubList).message("Announced at: " + announcement.apiUrl.toString())); - if(!announcement.isPrivate) - m_history->addAnnouncement(announcement.apiUrl.toString()); - m_publicListings << announcement; - sendUpdatedAnnouncementList(); + connect(response, &sessionlisting::AnnouncementApiResponse::finished, [this, response, apiUrl](const QVariant &result, const QString &message, const QString &error) { + Q_UNUSED(result); - int timeout = announcement.refreshInterval * 60 * 1000; - if(!m_refreshTimer->isActive() || m_refreshTimer->remainingTime() > timeout) - m_refreshTimer->start(timeout); -} + if(!message.isEmpty()) { + log(Log().about(Log::Level::Info, Log::Topic::PubList).message(message)); + this->messageAll(message, false); + } -void Session::sessionAnnouncementError(const QString &apiUrl, const QString &error) -{ + response->deleteLater(); + if(!error.isEmpty()) { + // Remove listing on error + log(Log().about(Log::Level::Warn, Log::Topic::PubList).message(apiUrl + ": announcement error: " + error)); + unlistAnnouncement(apiUrl, true, true); + this->messageAll(error, false); + } + }); + } - // Remove listing on error - log(Log().about(Log::Level::Warn, Log::Topic::PubList).message(apiUrl + ": announcement error: " + error)); - unlistAnnouncement(apiUrl, true, true); - this->messageAll(error, false); + if(timeout > 0) { + m_refreshTimer->start(timeout * 60 * 1000); + } } void Session::historyCacheCleanup() @@ -1106,6 +1247,8 @@ QJsonObject Session::getDescription(bool full) const if(full) { // Full descriptions includes detailed info for server admins. o["maxSize"] = int(m_history->sizeLimit()); + o["resetThreshold"] = int(m_history->autoResetThreshold()); + o["deputies"] = m_history->flags().testFlag(SessionHistory::Deputies); QJsonArray users; for(const Client *user : m_clients) { diff --git a/src/shared/server/session.h b/src/shared/server/session.h index 3fbd78f6b..33be40830 100644 --- a/src/shared/server/session.h +++ b/src/shared/server/session.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2013-2018 Calle Laakkonen + Copyright (C) 2013-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -46,6 +46,7 @@ namespace server { class Client; class ServerConfig; +class Log; /** * The serverside session state. @@ -60,7 +61,7 @@ class Session : public QObject { Shutdown }; - Session(SessionHistory *history, ServerConfig *config, QObject *parent=0); + Session(SessionHistory *history, ServerConfig *config, QObject *parent=nullptr); const ServerConfig *config() const { return m_config; } @@ -110,6 +111,13 @@ class Session : public QObject { */ bool isNsfm() const { return m_history->flags().testFlag(SessionHistory::Nsfm); } + /** + * @brief Are trusted users deputized? + * + * If true, trusted users are granted limited access to kick/ban commands. + */ + bool isDeputies() const { return m_history->flags().testFlag(SessionHistory::Deputies); } + /** * @brief Set the name of the recording file to create * @@ -358,6 +366,7 @@ class Session : public QObject { //! Get the session state State state() const { return m_state; } + void readyToAutoReset(int ctxId); void handleInitBegin(int ctxId); void handleInitComplete(int ctxId); void handleInitCancel(int ctxId); @@ -367,11 +376,21 @@ class Session : public QObject { * * Generates log entries for each change * - * @param ids lisf of new session operators + * @param ids new list of session operators * @param changedBy name of the user who issued the change command * @return sanitized list of actual session operators */ - QList updateOwnership(QList ids, const QString &chanedBy); + QList updateOwnership(QList ids, const QString &changedBy); + + /** + * @brief Update the list of trusted users + * + * Generates log entries for each change + * @param ids new list of trusted users + * @param changedBy name of the user who issued the change command + * @return sanitized list of actual trusted users + */ + QList updateTrustedUsers(QList ids, const QString &changedBy); /** * @brief Grant or revoke OP status of a user @@ -381,6 +400,14 @@ class Session : public QObject { */ void changeOpStatus(int id, bool op, const QString &changedBy); + /** + * @brief Grant or revoke trusted status of a user + * @param id user ID + * @param trusted new status + * @param changedBy name of the user who issued the command + */ + void changeTrustedStatus(int id, bool trusted, const QString &changedBy); + //! Send refreshed ban list to all logged in users void sendUpdatedBanlist(); @@ -459,12 +486,8 @@ private slots: void removeUser(Client *user); void refreshAnnouncements(); - void sessionAnnounced(const sessionlisting::Announcement &announcement); - void sessionAnnouncementError(const QString &apiUrl, const QString &message); private: - sessionlisting::AnnouncementApi *publicListingClient(); - void cleanupCommandStream(); void restartRecording(); @@ -492,10 +515,8 @@ private slots: SessionHistory *m_history; QList m_resetstream; uint m_resetstreamsize; - uint m_historyLimitWarning; QList m_publicListings; - sessionlisting::AnnouncementApi *m_publicListingClient; QTimer *m_refreshTimer; QElapsedTimer m_lastEventTime; @@ -503,7 +524,7 @@ private slots: bool m_closed; bool m_authOnly; - bool m_historyLimitWarningSent; + enum class AutoResetState { NotSent, Queried, Requested} m_autoResetRequestStatus; }; } diff --git a/src/shared/server/sessionhistory.cpp b/src/shared/server/sessionhistory.cpp index dc4fcbd2b..0b461de47 100644 --- a/src/shared/server/sessionhistory.cpp +++ b/src/shared/server/sessionhistory.cpp @@ -21,7 +21,8 @@ namespace server { SessionHistory::SessionHistory(const QUuid &id, QObject *parent) - : QObject(parent), m_id(id), m_sizeInBytes(0), m_sizeLimit(0), m_firstIndex(0), m_lastIndex(-1) + : QObject(parent), m_id(id), m_sizeInBytes(0), m_sizeLimit(0), m_autoResetBaseSize(0), + m_firstIndex(0), m_lastIndex(-1) { } @@ -53,6 +54,7 @@ void SessionHistory::historyLoaded(uint size, int messageCount) Q_ASSERT(m_lastIndex==-1); m_sizeInBytes = size; m_lastIndex = messageCount - 1; + m_autoResetBaseSize = size; } bool SessionHistory::addMessage(const protocol::MessagePtr &msg) @@ -79,10 +81,21 @@ bool SessionHistory::reset(const QList &newHistory) m_sizeInBytes = newSize; m_firstIndex = m_lastIndex + 1; m_lastIndex += newHistory.size(); + m_autoResetBaseSize = newSize; historyReset(newHistory); emit newMessagesAvailable(); return true; } +uint SessionHistory::effectiveAutoResetThreshold() const +{ + const uint t = autoResetThreshold(); + // Zero means autoreset is not enabled + if(t>0) + return m_autoResetBaseSize + t; + else + return 0; +} + } diff --git a/src/shared/server/sessionhistory.h b/src/shared/server/sessionhistory.h index a3cbebd88..c7f125460 100644 --- a/src/shared/server/sessionhistory.h +++ b/src/shared/server/sessionhistory.h @@ -45,7 +45,8 @@ class SessionHistory : public QObject { enum Flag { Persistent = 0x01, PreserveChat = 0x02, - Nsfm = 0x04 + Nsfm = 0x04, + Deputies = 0x08 }; Q_DECLARE_FLAGS(Flags, Flag) @@ -107,6 +108,18 @@ class SessionHistory : public QObject { //! Remember a user who joined virtual void joinUser(uint8_t id, const QString &name); + //! Set the history size threshold for requesting autoreset + virtual void setAutoResetThreshold(uint limit) = 0; + + //! Get the history autoreset request threshold + virtual uint autoResetThreshold() const = 0; + + //! Get the final autoreset threshold that includes the reset image base size + uint effectiveAutoResetThreshold() const; + + //! Get the reset image base size + uint autoResetThresholdBase() const { return m_autoResetBaseSize; } + /** * @brief Add a new message to the history * @@ -154,10 +167,12 @@ class SessionHistory : public QObject { virtual void terminate() = 0; /** - * @brief Set the size limit for the history. + * @brief Set the hard size limit for the history. * * The size limit is checked when new messages are added to the session. * + * See also the autoreset threshold. + * * @param limit maximum size in bytes or 0 for no limit */ void setSizeLimit(uint limit) { m_sizeLimit = limit; } @@ -244,11 +259,24 @@ class SessionHistory : public QObject { */ virtual void setAuthenticatedOperator(const QString &username, bool op) = 0; + /** + * @brief Set an authenticated user's trust status + * + * This is used to remember an authenticated user's status so it + * can be automatically restored when they log in again. + */ + virtual void setAuthenticatedTrust(const QString &username, bool trusted) = 0; + /** * @brief Is the given name on the list of operators */ virtual bool isOperator(const QString &username) const = 0; + /** + * @brief Is the given name on the list of trusted users + */ + virtual bool isTrusted(const QString &username) const = 0; + /** * @brief Are there any names on the list of authenticated operators? */ @@ -278,6 +306,7 @@ class SessionHistory : public QObject { uint m_sizeInBytes; uint m_sizeLimit; + uint m_autoResetBaseSize; int m_firstIndex; int m_lastIndex; }; diff --git a/src/shared/server/sessionserver.cpp b/src/shared/server/sessionserver.cpp index 40df52c08..64ab794ac 100644 --- a/src/shared/server/sessionserver.cpp +++ b/src/shared/server/sessionserver.cpp @@ -27,8 +27,6 @@ #include "filedhistory.h" #include "templateloader.h" -#include "../util/announcementapi.h" - #include #include #include diff --git a/src/shared/tests/messages.cpp b/src/shared/tests/messages.cpp index 59496cfbe..98abea501 100644 --- a/src/shared/tests/messages.cpp +++ b/src/shared/tests/messages.cpp @@ -6,7 +6,7 @@ #include "../net/layer.h" #include "../net/image.h" #include "../net/undo.h" -#include "../net/pen.h" +#include "../net/brushes.h" #include "../net/textmode.h" #include @@ -32,6 +32,7 @@ private slots: QTest::newRow("userjoin(no hash)") << (Message*)new UserJoin(4, 0x03, QString("Test"), QByteArray()); QTest::newRow("userleave") << (Message*)new UserLeave(5); QTest::newRow("sessionowner") << (Message*)new SessionOwner(6, QList() << 1 << 2 << 5); + QTest::newRow("softreset") << (Message*)new SoftResetPoint(60); QTest::newRow("chat") << (Message*)new Chat(7, 0x01, 0x04, QByteArray("Test")); QTest::newRow("interval") << (Message*)new Interval(8, 0x1020); @@ -39,21 +40,22 @@ private slots: QTest::newRow("movepointer") << (Message*)new MovePointer(10, 0x11223344, 0x55667788); QTest::newRow("marker") << (Message*)new Marker(11, QString("Test")); QTest::newRow("useracl") << (Message*)new UserACL(12, QList() << 1 << 2 << 4); - QTest::newRow("layeracl") << (Message*)new LayerACL(13, 0x1122, 0x01, QList() << 1 << 2 << 4); - QTest::newRow("sessionacl") << (Message*)new SessionACL(14, 63); + QTest::newRow("layeracl") << (Message*)new LayerACL(13, 0x1122, 0x01, 0x02, QList() << 3 << 4 << 5); + QTest::newRow("featureaccess") << (Message*)new FeatureAccessLevels(14, (const uint8_t*)"\0\1\2\3\0\1\2\3\0"); QTest::newRow("defaultlayer") << (Message*)new DefaultLayer(14, 0x1401); QTest::newRow("undopoint") << (Message*)new UndoPoint(15); QTest::newRow("canvasresize") << (Message*)new CanvasResize(16, -0xfff, 0xaaa, -0xbbb, 0xccc); + QTest::newRow("background(color)") << (Message*)new CanvasBackground(17, 0x00ff0000); + QTest::newRow("background(img)") << (Message*)new CanvasBackground(17, QByteArray(64*64*4, '\xff')); QTest::newRow("layercreate") << (Message*)new LayerCreate(17, 0xaabb, 0xccdd, 0x11223344, 0x01, QString("Test layer")); - QTest::newRow("layerattributes") << (Message*)new LayerAttributes(18, 0xaabb, 0xcc, 0x10); + QTest::newRow("layerattributes") << (Message*)new LayerAttributes(18, 0xaabb, 0xcc, LayerAttributes::FLAG_CENSOR, 0x10, 0x22); QTest::newRow("layerretitle") << (Message*)new LayerRetitle(19, 0xaabb, QString("Test")); QTest::newRow("layerorder") << (Message*)new LayerOrder(20, QList() << 0x1122 << 0x3344 << 0x4455); QTest::newRow("layervisibility") << (Message*)new LayerVisibility(21, 0x1122, 1); QTest::newRow("putimage") << (Message*)new PutImage(22, 0x1122, 0x10, 100, 200, 300, 400, QByteArray("Test")); + QTest::newRow("puttile") << (Message*)new PutTile(22, 0x1122, 0x10, 1, 2, 3, 0xaabbccdd); QTest::newRow("fillrect") << (Message*)new FillRect(23, 0x1122, 0x10, 3, 200, 300, 400, 0x11223344); - QTest::newRow("toolchange") << (Message*)new ToolChange(24, 0x1122, 1, 2, 3, 0xffbbccdd, 10, 11, 20, 21, 30, 31, 40, 41, 60); - QTest::newRow("penmove") << (Message*)new PenMove(25, PenPointVector() << PenPoint {-10, 10, 0x00ff} << PenPoint { -100, 100, 0xff00 }); QTest::newRow("penup") << (Message*)new PenUp(26); QTest::newRow("annotationcreate") << (Message*)new AnnotationCreate(27, 0x1122, -100, -100, 200, 200); QTest::newRow("annotationreshape") << (Message*)new AnnotationReshape(28, 0x1122, -100, -100, 200, 200); @@ -61,6 +63,9 @@ private slots: QTest::newRow("annotationdelete") << (Message*)new AnnotationDelete(30, 0x1122); QTest::newRow("moveregion") << (Message*)new MoveRegion(30, 0x1122, 0, 1, 2, 3, 10, 11, 20, 21, 30, 31, 40, 41, QByteArray("test")); + QTest::newRow("classicdabs") << (Message*)new DrawDabsClassic(31, 0x1122, 100, -100, 0xff223344, 0x10, ClassicBrushDabVector() << ClassicBrushDab {1, 2, 3, 4, 5} << ClassicBrushDab {10, 20, 30, 40, 50}); + QTest::newRow("pixeldabs") << (Message*)new DrawDabsPixel(32, 0x1122, 100, -100, 0xff223344, 0x10, PixelBrushDabVector() << PixelBrushDab {1, 2, 3, 4} << PixelBrushDab {10, 20, 30, 40}); + QTest::newRow("undo") << (Message*)new Undo(254, 1, false); QTest::newRow("redo") << (Message*)new Undo(254, 1, true); } @@ -77,8 +82,8 @@ private slots: QByteArray buffer(msg->length(), 0); QCOMPARE(msg->serialize(buffer.data()), msg->length()); - Message *msg2 = Message::deserialize(reinterpret_cast(buffer.constData()), buffer.size(), true); - QVERIFY(msg2); + NullableMessageRef msg2 = Message::deserialize(reinterpret_cast(buffer.constData()), buffer.size(), true); + QVERIFY(!msg2.isNull()); QVERIFY(msg->equals(*msg2)); @@ -95,9 +100,8 @@ private slots: QFAIL(parser.errorString().toLocal8Bit().constData()); }; QCOMPARE(r.status, text::Parser::Result::Ok); - QVERIFY(r.msg); + QVERIFY(!r.msg.isNull()); QVERIFY(msg->equals(*r.msg)); - delete r.msg; } } @@ -112,14 +116,14 @@ private slots: QCOMPARE(written, filtered->length()); // As should deserializing - Message *deserialized = Message::deserialize(reinterpret_cast(serialized.data()), serialized.length(), true); - QVERIFY(deserialized); + NullableMessageRef deserialized = Message::deserialize(reinterpret_cast(serialized.data()), serialized.length(), true); + QVERIFY(!deserialized.isNull()); QCOMPARE(deserialized->type(), MSG_FILTERED); // The wrapped message should stay intact through the process // (assuming payload length is less than 65535) - Message *unwrapped = static_cast(deserialized)->decodeWrapped(); - QVERIFY(unwrapped); + NullableMessageRef unwrapped = deserialized.cast().decodeWrapped(); + QVERIFY(!unwrapped.isNull()); QVERIFY(unwrapped->equals(*original)); } diff --git a/src/shared/tests/recording.cpp b/src/shared/tests/recording.cpp index 3551e438e..380095797 100644 --- a/src/shared/tests/recording.cpp +++ b/src/shared/tests/recording.cpp @@ -17,17 +17,17 @@ using namespace protocol; // Hex encoded test recording. // Header contains one extra key: "test": "TESTING" -// Protocol version is "dp:4.20.1" +// Protocol version is "dp:4.21.2" // Body contains one message: UserJoin(1, 0, "hello", "world") -static const char *TEST_RECORDING = "44505245430000427b2274657374223a2254455354494e47222c2276657273696f6e223a2264703a342e32302e31222c2277726974657276657273696f6e223a22322e302e306232227d000c2001000568656c6c6f776f726c64"; +static const char *TEST_RECORDING = "44505245430000427b2274657374223a2254455354494e47222c2276657273696f6e223a2264703a342e32312e32222c2277726974657276657273696f6e223a22322e302e306232227d000c2001000568656c6c6f776f726c64"; // A test recording with a version number of dp:4.10.0, containing a single NewLayer message. static const char *TEST_RECORDING_OLD = "44505245430000317b2276657273696f6e223a2264703a342e31302e30222c2277726974657276657273696f6e223a22322e302e306232227d00098201000100000000000000"; static const char *TEST_TEXTMODE = - "!version=dp:4.20.1\n" + "!version=dp:4.21.2\n" "!test=TESTING\n" - "1 join name=hello hash=world\n"; + "1 join name=hello avatar=world\n"; class TestRecording: public QObject { @@ -104,11 +104,12 @@ private slots: QCOMPARE(reader.isCompressed(), false); Compatibility compat = reader.open(); + QCOMPARE(reader.formatVersion().asString(), QString("dp:4.21.2")); QCOMPARE(compat, COMPATIBLE); QCOMPARE(int(reader.encoding()), encoding); - QCOMPARE(reader.formatVersion().asString(), QString("dp:4.20.1")); + QCOMPARE(reader.formatVersion().asString(), QString("dp:4.21.2")); QCOMPARE(reader.metadata()["test"].toString(), QString("TESTING")); // No message read yet @@ -116,22 +117,21 @@ private slots: const qint64 firstPosition = reader.filePosition(); // There should be exactly one message in the test recording - MessageRecord mr = reader.readNext(); + const MessageRecord mr1 = reader.readNext(); - QCOMPARE(mr.status, MessageRecord::OK); + QCOMPARE(mr1.status, MessageRecord::OK); MessagePtr testMsg(new UserJoin(1, 0, QByteArray("hello"), QByteArray("world"))); - MessagePtr readMsg(mr.message); - QVERIFY(readMsg.equals(testMsg)); + QVERIFY(mr1.message.equals(testMsg)); // current* returns the index and position of the last read message QCOMPARE(reader.currentIndex(), 0); QCOMPARE(reader.currentPosition(), firstPosition); // Next message should be EOF - mr = reader.readNext(); - QCOMPARE(mr.status, MessageRecord::END_OF_RECORDING); + const MessageRecord mr2 = reader.readNext(); + QCOMPARE(mr2.status, MessageRecord::END_OF_RECORDING); QVERIFY(reader.isEof()); // Rewinding should take us back to the beginning @@ -139,10 +139,9 @@ private slots: QCOMPARE(reader.currentIndex(), -1); QCOMPARE(reader.filePosition(), firstPosition); - mr = reader.readNext(); - QCOMPARE(mr.status, MessageRecord::OK); - MessagePtr readMsg2(mr.message); - QVERIFY(readMsg.equals(readMsg2)); + const MessageRecord mr3 = reader.readNext(); + QCOMPARE(mr3.status, MessageRecord::OK); + QVERIFY(mr1.message.equals(mr3.message)); } // Autoclose is not enabled @@ -225,9 +224,8 @@ private slots: // The actual message should be of type OpaqueMessage MessageRecord mr = reader.readNext(); QCOMPARE(mr.status, MessageRecord::OK); - QVERIFY(mr.message); + QVERIFY(!mr.message.isNull()); QCOMPARE(mr.message->type(), protocol::MSG_LAYER_CREATE); - delete mr.message; } }; diff --git a/src/shared/util/announcementapi.cpp b/src/shared/util/announcementapi.cpp index 85776d878..d9e2594df 100644 --- a/src/shared/util/announcementapi.cpp +++ b/src/shared/util/announcementapi.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2015-2017 Calle Laakkonen + Copyright (C) 2015-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -19,7 +19,6 @@ #include "announcementapi.h" #include "networkaccess.h" -#include "../server/serverlog.h" #include "config.h" // for DRAWPILE_VERSION #include @@ -28,22 +27,12 @@ #include #include #include +#include namespace sessionlisting { -using server::Log; - -static const char *PROP_APIURL = "APIURL"; // The base API URL of the request -static const char *PROP_SESSION_ID = "SESSIONID"; // The ID of the session being announced or unlisted static const char *USER_AGENT = "DrawpileListingClient/" DRAWPILE_VERSION; -class ResponseError { -public: - ResponseError(const QString &e) : error(e) { } - - QString error; -}; - static QString slashcat(QString s, const QString &s2) { if(!s.endsWith('/')) @@ -52,26 +41,129 @@ static QString slashcat(QString s, const QString &s2) return s; } -AnnouncementApi::AnnouncementApi(QObject *parent) - : QObject(parent) +typedef QPair ApiReply; +static inline ApiReply ApiSuccess(const QJsonDocument &doc) { return ApiReply(doc, QString()); } +static inline QJsonDocument ApiSuccess(const ApiReply &r) { return r.first; } +static inline ApiReply ApiError(const QString &message) { return ApiReply(QJsonDocument(), message); } +static inline QString ApiError(const ApiReply &r) { return r.second; } +static inline bool IsApiError(const ApiReply &r) { return !ApiError(r).isEmpty(); } + +/** + * Read response body and handle standard errors + */ +static ApiReply readReply(QNetworkReply *reply) { + Q_ASSERT(reply); + + if(reply->error() != QNetworkReply::NoError) { + const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if(statusCode == 422 || statusCode == 409) { + // Server says that the problem is at our end + QJsonParseError error; + QByteArray body = reply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(body, &error); + if(error.error != QJsonParseError::NoError) + return ApiError(QStringLiteral("Http error 422 but response body was unparseable: %1").arg(error.errorString())); + + const QString msg = doc.object()["message"].toString(); + if(msg.isEmpty()) { + return ApiError(QStringLiteral("Http error 422 (no explanation given)")); + } else { + return ApiError(msg); + } + + } else { + // Other errors + return ApiError(QStringLiteral("Network error: ") + reply->errorString()); + } + } + + // No error, parse response body + QJsonParseError error; + const QByteArray body = reply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(body, &error); + if(error.error != QJsonParseError::NoError) { + return ApiError(QStringLiteral("Unparseable response: ") + error.errorString()); + } + + return ApiSuccess(doc); } -void AnnouncementApi::getApiInfo(const QUrl &apiUrl) +void AnnouncementApiResponse::setResult(const QVariant &result, const QString &message) { - emit logMessage(Log().about(Log::Level::Debug, Log::Topic::PubList).message("Getting API info from " + apiUrl.toString())); + m_result = result; + m_message = message; + emit finished(m_result, m_message, QString()); +} + +void AnnouncementApiResponse::setError(const QString &error) +{ + m_error = error; + emit finished(QVariant(), QString(), error); +} + +AnnouncementApiResponse *getApiInfo(const QUrl &apiUrl) +{ + AnnouncementApiResponse *res = new AnnouncementApiResponse(apiUrl); QNetworkRequest req(apiUrl); req.setHeader(QNetworkRequest::UserAgentHeader, USER_AGENT); QNetworkReply *reply = networkaccess::getInstance()->get(req); - reply->setProperty(PROP_APIURL, apiUrl); - connect(reply, &QNetworkReply::finished, this, [reply, this]() { handleResponse(reply, &AnnouncementApi::handleServerInfoResponse);} ); + reply->connect(reply, &QNetworkReply::finished, res, [reply, res]() { + auto r = readReply(reply); + if(IsApiError(r)) { + res->setError(ApiError(r)); + return; + } + const auto doc = ApiSuccess(r); + + if(!doc.isObject()) { + res->setError(QStringLiteral("Invalid response: object expected!")); + return; + } + + const QJsonObject obj = doc.object(); + + QString apiname = obj.value("api_name").toString(); + if(apiname != "drawpile-session-list") { + res->setError(QStringLiteral("This is not a Drawpile listing server!")); + return; + } + + ListServerInfo info { + obj.value("version").toString(), + obj.value("name").toString().trimmed(), + obj.value("description").toString().trimmed(), + obj.value("favicon").toString() + }; + + if(info.version.isEmpty()) { + res->setError(QStringLiteral("API version not specified!")); + return; + } + + if(!info.version.startsWith("1.")) { + res->setError(QStringLiteral("Unsupported API version!")); + return; + } + + if(info.name.isEmpty()) { + res->setError(QStringLiteral("Server name missing!")); + return; + } + + res->setResult(QVariant::fromValue(info)); + }); + reply->connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater); + + return res; } -void AnnouncementApi::getSessionList(const QUrl &apiUrl, const QString &protocol, const QString &title, bool nsfm) +AnnouncementApiResponse *getSessionList(const QUrl &apiUrl, const QString &protocol, const QString &title, bool nsfm) { - // Send request + AnnouncementApiResponse *res = new AnnouncementApiResponse(apiUrl); + QUrl url = apiUrl; url.setPath(slashcat(url.path(), "sessions/")); @@ -85,16 +177,61 @@ void AnnouncementApi::getSessionList(const QUrl &apiUrl, const QString &protocol url.setQuery(query); QNetworkRequest req(url); + req.setHeader(QNetworkRequest::UserAgentHeader, USER_AGENT); QNetworkReply *reply = networkaccess::getInstance()->get(req); - reply->setProperty(PROP_APIURL, apiUrl); - connect(reply, &QNetworkReply::finished, this, [reply, this]() { handleResponse(reply, &AnnouncementApi::handleListingResponse);} ); + reply->connect(reply, &QNetworkReply::finished, res, [reply, res]() { + auto r = readReply(reply); + if(IsApiError(r)) { + res->setError(ApiError(r)); + return; + } + const auto doc = ApiSuccess(r); + + if(!doc.isArray()) { + res->setError(QStringLiteral("Expected array of session descriptions!")); + qWarning() << "Received" << doc; + return; + } + + QList sessions; + + for(const QJsonValue &jsv : doc.array()) { + if(!jsv.isObject()) { + res->setError(QStringLiteral("Expected session description!")); + return; + } + + const QJsonObject obj = jsv.toObject(); + + QDateTime started = QDateTime::fromString(obj["started"].toString(), Qt::ISODate); + started.setTimeSpec(Qt::UTC); + + sessions << Session { + obj["host"].toString(), + obj["port"].toInt(), + obj["id"].toString(), + protocol::ProtocolVersion::fromString(obj["protocol"].toString()), + obj["title"].toString(), + obj["users"].toInt(), + obj["usernames"].toVariant().toStringList(), + obj["password"].toBool(), + obj["nsfm"].toBool(), + PrivacyMode::Public, // a listed session cannot be private by definition + obj["owner"].toString(), + started + }; + } + + res->setResult(QVariant::fromValue(sessions)); + }); + reply->connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater); + + return res; } -void AnnouncementApi::announceSession(const QUrl &apiUrl, const Session &session) +AnnouncementApiResponse *announceSession(const QUrl &apiUrl, const Session &session) { - emit logMessage(Log().about(Log::Level::Info, Log::Topic::PubList).message("Announcing " + session.id + " at " + apiUrl.toString())); - // Construct the announcement QJsonObject o; if(!session.host.isEmpty()) @@ -109,9 +246,11 @@ void AnnouncementApi::announceSession(const QUrl &apiUrl, const Session &session o["password"] = session.password; o["owner"] = session.owner; o["nsfm"] = session.nsfm; - if(session.isPrivate == PrivateMode::Private) + if(session.isPrivate == PrivacyMode::Private) o["private"] = true; + const QString sessionId = session.id; + // Send request QUrl url = apiUrl; url.setPath(slashcat(url.path(), "sessions/")); @@ -119,16 +258,37 @@ void AnnouncementApi::announceSession(const QUrl &apiUrl, const Session &session req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); req.setHeader(QNetworkRequest::UserAgentHeader, USER_AGENT); + AnnouncementApiResponse *res = new AnnouncementApiResponse(apiUrl); + QNetworkReply *reply = networkaccess::getInstance()->post(req, QJsonDocument(o).toJson()); - reply->setProperty(PROP_APIURL, apiUrl); - reply->setProperty(PROP_SESSION_ID, session.id); - connect(reply, &QNetworkReply::finished, this, [reply, this]() { handleResponse(reply, &AnnouncementApi::handleAnnounceResponse);} ); + reply->connect(reply, &QNetworkReply::finished, res, [reply, res, apiUrl, sessionId]() { + auto r = readReply(reply); + if(IsApiError(r)) { + res->setError(ApiError(r)); + return; + } + const auto doc = ApiSuccess(r); + const QJsonObject obj = doc.object(); + + const Announcement a { + apiUrl, + sessionId, + obj["key"].toString(), + obj["roomcode"].toString(), + obj["id"].toInt(), + qMax(2, obj["expires"].toInt(6)) - 1, + obj["private"].toBool() + }; + + res->setResult(QVariant::fromValue(a), doc.object()["message"].toString()); + }); + reply->connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater); + + return res; } -void AnnouncementApi::refreshSession(const Announcement &a, const Session &session) +AnnouncementApiResponse *refreshSession(const Announcement &a, const Session &session) { - emit logMessage(Log().about(Log::Level::Debug, Log::Topic::PubList).message(QString("Refreshing listing %1 at %2").arg(a.listingId).arg(a.apiUrl.toString()))); - // Construct the announcement QJsonObject o; @@ -138,8 +298,8 @@ void AnnouncementApi::refreshSession(const Announcement &a, const Session &sessi o["password"] = session.password; o["owner"] = session.owner; o["nsfm"] = session.nsfm; - if(session.isPrivate != PrivateMode::Undefined) - o["private"] = session.isPrivate == PrivateMode::Private; + if(session.isPrivate != PrivacyMode::Undefined) + o["private"] = session.isPrivate == PrivacyMode::Private; // Send request QUrl url = a.apiUrl; @@ -150,15 +310,28 @@ void AnnouncementApi::refreshSession(const Announcement &a, const Session &sessi req.setHeader(QNetworkRequest::UserAgentHeader, USER_AGENT); req.setRawHeader("X-Update-Key", a.updateKey.toUtf8()); + AnnouncementApiResponse *res = new AnnouncementApiResponse(a.apiUrl); + QNetworkReply *reply = networkaccess::getInstance()->put(req, QJsonDocument(o).toJson()); - reply->setProperty(PROP_APIURL, a.apiUrl); - connect(reply, &QNetworkReply::finished, this, [reply, this]() { handleResponse(reply, &AnnouncementApi::handleRefreshResponse);} ); + reply->connect(reply, &QNetworkReply::finished, res, [reply, res, a]() { + auto r = readReply(reply); + if(IsApiError(r)) { + res->setError(ApiError(r)); + return; + } + const auto doc = ApiSuccess(r); + const QJsonObject obj = doc.object(); + + res->setResult(a.id, doc.object()["message"].toString()); + }); + + reply->connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater); + + return res; } -void AnnouncementApi::unlistSession(const Announcement &a) +AnnouncementApiResponse *unlistSession(const Announcement &a) { - emit logMessage(Log().about(Log::Level::Info, Log::Topic::PubList).message(QString("Unlisting announcement %1 at %2").arg(a.listingId).arg(a.apiUrl.toString()))); - QUrl url = a.apiUrl; url.setPath(slashcat(url.path(), QStringLiteral("sessions/%1").arg(a.listingId))); @@ -166,13 +339,18 @@ void AnnouncementApi::unlistSession(const Announcement &a) req.setHeader(QNetworkRequest::UserAgentHeader, USER_AGENT); req.setRawHeader("X-Update-Key", a.updateKey.toUtf8()); + AnnouncementApiResponse *res = new AnnouncementApiResponse(a.apiUrl); + QNetworkReply *reply = networkaccess::getInstance()->deleteResource(req); - reply->setProperty(PROP_APIURL, a.apiUrl); - reply->setProperty(PROP_SESSION_ID, a.id); - connect(reply, &QNetworkReply::finished, this, [reply, this]() { handleResponse(reply, &AnnouncementApi::handleUnlistResponse);} ); + reply->connect(reply, &QNetworkReply::finished, res, [a, res]() { + res->setResult(a.id); + }); + reply->connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater); + + return res; } -void AnnouncementApi::queryRoomcode(const QUrl &apiUrl, const QString &roomcode) +AnnouncementApiResponse *queryRoomcode(const QUrl &apiUrl, const QString &roomcode) { QUrl url = apiUrl; url.setPath(slashcat(url.path(), QStringLiteral("join/") + roomcode)); @@ -180,205 +358,39 @@ void AnnouncementApi::queryRoomcode(const QUrl &apiUrl, const QString &roomcode) QNetworkRequest req(url); req.setHeader(QNetworkRequest::UserAgentHeader, USER_AGENT); - QNetworkReply *reply = networkaccess::getInstance()->get(req); - reply->setProperty(PROP_APIURL, apiUrl); - connect(reply, &QNetworkReply::finished, this, [reply, this]() { handleResponse(reply, &AnnouncementApi::handleRoomcodeResponse);} ); -} + AnnouncementApiResponse *res = new AnnouncementApiResponse(apiUrl); -void AnnouncementApi::handleResponse(QNetworkReply *reply, AnnouncementApi::HandlerFunc handlerFunc) -{ - Q_ASSERT(reply); - Q_ASSERT(handlerFunc); - - try { - if(reply->error() != QNetworkReply::NoError) { - const int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if(statusCode == 422 || statusCode == 409) { - // Server says that the problem is at our end - QJsonParseError error; - QByteArray body = reply->readAll(); - QJsonDocument doc = QJsonDocument::fromJson(body, &error); - if(error.error != QJsonParseError::NoError) - throw ResponseError(QStringLiteral("Http error 422 but response body was unparseable: %1").arg(error.errorString())); - - const QString msg = doc.object()["message"].toString(); - if(msg.isEmpty()) { - throw ResponseError(QStringLiteral("Http error 422 (no explanation given)")); - } else { - throw ResponseError(msg); - } + QNetworkReply *reply = networkaccess::getInstance()->get(req); - } else { - // Other errors - throw ResponseError(QStringLiteral("Network error: ") + reply->errorString()); - } + reply->connect(reply, &QNetworkReply::finished, res, [reply, res]() { + auto r = readReply(reply); + if(IsApiError(r)) { + res->setError(ApiError(r)); + return; } - - // Server did not return an error: handle the response - ((this)->*(handlerFunc))(reply); - - } catch(const ResponseError &e) { - emit logMessage(Log().about(Log::Level::Error, Log::Topic::PubList).message("Announcement API error: " + e.error)); - emit error(reply->property(PROP_APIURL).toString(), "Session announcement: " + e.error); - } - - reply->deleteLater(); -} - -void AnnouncementApi::handleAnnounceResponse(QNetworkReply *reply) -{ - QJsonParseError error; - QByteArray body = reply->readAll(); - QJsonDocument doc = QJsonDocument::fromJson(body, &error); - if(error.error != QJsonParseError::NoError) - throw ResponseError(QStringLiteral("Error parsing announcement response: %1").arg(error.errorString())); - - Announcement a; - a.apiUrl = reply->property(PROP_APIURL).toUrl(); - a.id = reply->property(PROP_SESSION_ID).toString(); - const QJsonObject obj = doc.object(); - a.roomcode = obj["roomcode"].toString(); - a.updateKey = obj["key"].toString(); - a.listingId = obj["id"].toInt(); - a.refreshInterval = qMax(2, obj["expires"].toInt(6)) - 1; - a.isPrivate = obj["private"].toBool(); - - qDebug("Announcement server %s refresh interval is %d minutes.", qPrintable(a.apiUrl.toString()), a.refreshInterval); - - emit logMessage(Log().about(Log::Level::Debug, Log::Topic::PubList).message(QString("Announced session %2. Got listing ID %1").arg(a.listingId).arg(a.id))); - - emit sessionAnnounced(a); - - QString welcome = doc.object()["message"].toString(); - if(!welcome.isEmpty()) - emit messageReceived(doc.object()["message"].toString()); -} - -void AnnouncementApi::handleUnlistResponse(QNetworkReply *reply) -{ - emit unlisted( - reply->property(PROP_APIURL).toString(), - reply->property(PROP_SESSION_ID).toString() - ); -} - -void AnnouncementApi::handleRefreshResponse(QNetworkReply *reply) -{ - QJsonParseError error; - QByteArray body = reply->readAll(); - QJsonDocument doc = QJsonDocument::fromJson(body, &error); - if(error.error != QJsonParseError::NoError) - throw ResponseError(QStringLiteral("Error parsing refresh response: %1").arg(error.errorString())); - - QString msg = doc.object()["message"].toString(); - if(!msg.isEmpty()) - emit messageReceived(msg); -} - -void AnnouncementApi::handleListingResponse(QNetworkReply *reply) -{ - QJsonParseError error; - QByteArray body = reply->readAll(); - QJsonDocument doc = QJsonDocument::fromJson(body, &error); - if(error.error != QJsonParseError::NoError) - throw ResponseError(QStringLiteral("Error parsing announcement response: %1").arg(error.errorString())); - - QList sessions; - - if(!doc.isArray()) - throw ResponseError(QStringLiteral("Expected array of session descriptions!")); - - for(const QJsonValue &jsv : doc.array()) { - if(!jsv.isObject()) - throw ResponseError(QStringLiteral("Expected session description!")); - const QJsonObject obj = jsv.toObject(); - - QDateTime started = QDateTime::fromString(obj["started"].toString(), Qt::ISODate); - started.setTimeSpec(Qt::UTC); - - sessions << Session { + const auto doc = ApiSuccess(r); + const QJsonObject obj = doc.object(); + const Session session { obj["host"].toString(), obj["port"].toInt(), obj["id"].toString(), - protocol::ProtocolVersion::fromString(obj["protocol"].toString()), - obj["title"].toString(), - obj["users"].toInt(), - obj["usernames"].toVariant().toStringList(), - obj["password"].toBool(), - obj["nsfm"].toBool(), - PrivateMode::Public, // a listed session cannot be private by definition - obj["owner"].toString(), - started + protocol::ProtocolVersion::current(), + QString(), + 0, + QStringList(), + false, + false, + PrivacyMode::Undefined, + QString(), + QDateTime() }; - } + res->setResult(QVariant::fromValue(session)); + }); - emit sessionListReceived(sessions); -} + reply->connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater); -void AnnouncementApi::handleRoomcodeResponse(QNetworkReply *reply) -{ - QJsonParseError error; - QByteArray body = reply->readAll(); - QJsonDocument doc = QJsonDocument::fromJson(body, &error); - if(error.error != QJsonParseError::NoError) - throw ResponseError(QStringLiteral("Error parsing roomcode response: %1").arg(error.errorString())); - - if(!doc.isObject()) - throw ResponseError(QStringLiteral("Expected session info object!")); - - const QJsonObject obj = doc.object(); - const Session session { - obj["host"].toString(), - obj["port"].toInt(), - obj["id"].toString(), - protocol::ProtocolVersion::current(), - QString(), - 0, - QStringList(), - false, - false, - PrivateMode::Undefined, - QString(), - QDateTime() - }; - - emit sessionFound(session); + return res; } -void AnnouncementApi::handleServerInfoResponse(QNetworkReply *reply) -{ - QJsonParseError error; - QByteArray body = reply->readAll(); - QJsonDocument doc = QJsonDocument::fromJson(body, &error); - if(error.error != QJsonParseError::NoError) - throw ResponseError(QStringLiteral("Error parsing API response: %1").arg(error.errorString())); - - if(!doc.isObject()) - throw ResponseError(QStringLiteral("Expected object!")); - - QJsonObject obj = doc.object(); - - QString apiname = obj.value("api_name").toString(); - if(apiname != "drawpile-session-list") - throw ResponseError(QStringLiteral("This is not a Drawpile listing server!")); - - ListServerInfo info { - obj.value("version").toString(), - obj.value("name").toString().trimmed(), - obj.value("description").toString().trimmed(), - obj.value("favicon").toString() - }; - - if(info.version.isEmpty()) - throw ResponseError(QStringLiteral("API version not specified!")); - - if(!info.version.startsWith("1.")) - throw ResponseError(QStringLiteral("Unsupported API version!")); - - if(info.name.isEmpty()) - throw ResponseError(QStringLiteral("Server name missing!")); - - emit serverInfo(info); } -} diff --git a/src/shared/util/announcementapi.h b/src/shared/util/announcementapi.h index 0d4e2320f..5bc9f897e 100644 --- a/src/shared/util/announcementapi.h +++ b/src/shared/util/announcementapi.h @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2015-2017 Calle Laakkonen + Copyright (C) 2015-2018 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -17,25 +17,17 @@ along with Drawpile. If not, see . */ -#ifndef ANNOUNCEMENTAPI_H -#define ANNOUNCEMENTAPI_H +#ifndef ANNOUNCEMENTAPI_V2_H +#define ANNOUNCEMENTAPI_V2_H #include "../net/protover.h" #include #include #include +#include #include -#include - -class QNetworkAccessManager; -class QNetworkReply; - -namespace server { - class Log; -} - namespace sessionlisting { struct ListServerInfo { @@ -45,8 +37,8 @@ struct ListServerInfo { QString faviconUrl; }; -enum class PrivateMode { - Undefined, // undefined, defaults to public +enum class PrivacyMode { + Undefined, // not specified, defaults to public Public, Private }; @@ -61,14 +53,12 @@ struct Session { QStringList usernames; bool password; bool nsfm; - PrivateMode isPrivate; + PrivacyMode isPrivate; QString owner; QDateTime started; }; struct Announcement { - Announcement() : listingId(0) { } - QUrl apiUrl; QString id; QString updateKey; @@ -78,96 +68,88 @@ struct Announcement { bool isPrivate; }; -/** - * @brief Public session listing API client - */ -class AnnouncementApi : public QObject +} + +Q_DECLARE_METATYPE(sessionlisting::ListServerInfo) +Q_DECLARE_METATYPE(sessionlisting::Session) +Q_DECLARE_METATYPE(sessionlisting::Announcement) + +namespace sessionlisting { + +class AnnouncementApiResponse : public QObject { Q_OBJECT public: - explicit AnnouncementApi(QObject *parent=nullptr); - - /** - * @brief Query information about the API - */ - void getApiInfo(const QUrl &apiUrl); - - /** - * @brief Send a request for a session list - * - * The signal sessionListReceived is emitted when the query finishes successfully. - * - * @param protocol if empty, limit query to sessions with this protocol version - * @param title if empty, limit query to sessions whose title contains this string - * @param nsfm if set to false, sessions tagged as "Not Suitable For Minors" will not be fetched - */ - void getSessionList(const QUrl &apiUrl, const QString &protocol=QString(), const QString &title=QString(), bool nsfm=false); - - /** - * @brief Send session announcement - * @param apiUrl - * @param session - */ - void announceSession(const QUrl &apiUrl, const Session &session); - - /** - * @brief Refresh the session previously announced with announceSession - */ - void refreshSession(const Announcement &a, const Session &session); - - /** - * @brief Unlist the session previously announced with announceSession - */ - void unlistSession(const Announcement &a); - - /** - * @brief Query session info for a room code - * - * If the room code is found, sessionFound signal is emitted. - * Otherwise, the error signal is emitted. - * - * @param apiUrl - * @param roomcode - */ - void queryRoomcode(const QUrl &apiUrl, const QString &roomcode); + AnnouncementApiResponse(const QUrl &url, QObject *parent=nullptr) + : QObject(parent), m_apiUrl(url) + { } -signals: - //! Server info reply received - void serverInfo(const ListServerInfo &info); + void setResult(const QVariant &result, const QString &message=QString()); + void setError(const QString &error); + + QUrl apiUrl() const { return m_apiUrl; } + QVariant result() const { return m_result; } + QString message() const { return m_message; } + QString errorMessage() const { return m_error; } - //! Session list received - void sessionListReceived(const QList &sessions); +signals: + void finished(const QVariant &result, const QString &message, const QString &error); - //! A message was received in response to a succesfull announcement - void messageReceived(const QString &message); +private: + QUrl m_apiUrl; + QVariant m_result; + QString m_message; + QString m_error; +}; - //! Session was announced succesfully - void sessionAnnounced(const Announcement &session); +/** + * @brief Fetch information about a listing server + * + * Returns ListServerInfo + */ +AnnouncementApiResponse *getApiInfo(const QUrl &apiUrl); - //! Session was unlisted succesfully - void unlisted(const QString &apiUrl, const QString &sessionId); +/** + * @brief Fetch the list of public sessions from a listing server + * + * @param url API url + * @param protocol if specified, return only sessions using the given protocol + * @param title if specified, return only sessions whose title contains this substring + * @param nsfm if true, fetch sessions tagged as NSFM + * + * Returns QList + */ +AnnouncementApiResponse *getSessionList(const QUrl &apiUrl, const QString &protocol=QString(), const QString &title=QString(), bool nsfm=false); - //! Session for a roomcode was found (only host, port and id fields are filled) - void sessionFound(const Session &session); +/** + * @brief Announce a session at the given listing server + * + * Returns Announcement + */ +AnnouncementApiResponse *announceSession(const QUrl &apiUrl, const Session &session); - //! An error occurred - void error(const QString &apiUrl, const QString &errorString); +/** + * @brief Refresh a session announcement + * + * Returns the session ID and may set the result message. + */ +AnnouncementApiResponse *refreshSession(const Announcement &a, const Session &session); - //! A log message - void logMessage(const server::Log &message); +/** + * @brief Unlist a session announcement + * + * Returns the session ID + */ +AnnouncementApiResponse *unlistSession(const Announcement &a); -private: - typedef void (AnnouncementApi::*HandlerFunc)(QNetworkReply*); - void handleResponse(QNetworkReply *reply, HandlerFunc); - - void handleAnnounceResponse(QNetworkReply *reply); - void handleUnlistResponse(QNetworkReply *reply); - void handleRefreshResponse(QNetworkReply *reply); - void handleListingResponse(QNetworkReply *reply); - void handleServerInfoResponse(QNetworkReply *reply); - void handleRoomcodeResponse(QNetworkReply *reply); -}; +/** + * @brief Query this server for a room code + * + * Returns a Session + */ +AnnouncementApiResponse *queryRoomcode(const QUrl &apiUrl, const QString &roomcode); } -#endif // ANNOUNCEMENTAPI_H +#endif + diff --git a/src/shared/util/whatismyip.cpp b/src/shared/util/whatismyip.cpp index 0abe11fba..79523af2b 100644 --- a/src/shared/util/whatismyip.cpp +++ b/src/shared/util/whatismyip.cpp @@ -81,7 +81,7 @@ void WhatIsMyIp::discoverMyIp() return; m_querying = true; - QNetworkReply *reply = networkaccess::get(QUrl("http://ipecho.net/plain"), QString()); + QNetworkReply *reply = networkaccess::get(QUrl("https://ipecho.net/plain"), QString()); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if(reply->error() != QNetworkReply::NoError) { diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index de7ab1a90..5eef34ada 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -1,6 +1,7 @@ set ( DPRECTOOL_SOURCES dprectool.cpp + stats.cpp ../client/canvas/aclfilter.cpp ) diff --git a/src/tools/dprectool.cpp b/src/tools/dprectool.cpp index 9195c1e1f..2605ecf6f 100644 --- a/src/tools/dprectool.cpp +++ b/src/tools/dprectool.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2014-2018 Calle Laakkonen + Copyright (C) 2014-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,6 +18,7 @@ */ #include "config.h" +#include "stats.h" #include "../shared/record/reader.h" #include "../shared/record/writer.h" @@ -102,7 +103,7 @@ bool convertRecording(const QString &inputfilename, const QString &outputfilenam ); return false; } - if(!writer->writeHeader()) { + if(!writer->writeHeader(reader.metadata())) { fprintf(stderr, "Error while writing header: %s\n", qPrintable(writer->errorString()) ); @@ -119,13 +120,11 @@ bool convertRecording(const QString &inputfilename, const QString &outputfilenam MessageRecord mr = reader.readNext(); switch(mr.status) { case MessageRecord::OK: { - protocol::MessagePtr msg(mr.message); - - if(doAclFiltering && !aclFilter.filterMessage(*msg)) { - writer->writeMessage(*msg->asFiltered()); + if(doAclFiltering && !aclFilter.filterMessage(*mr.message)) { + writer->writeMessage(*mr.message->asFiltered()); } else { - if(!writer->writeMessage(*msg)) { + if(!writer->writeMessage(*mr.message)) { fprintf(stderr, "Error while writing message: %s\n", qPrintable(writer->errorString()) ); @@ -137,8 +136,8 @@ bool convertRecording(const QString &inputfilename, const QString &outputfilenam case MessageRecord::INVALID: writer->writeComment(QStringLiteral("WARNING: Unrecognized message type %1 of length %2 at offset 0x%3") - .arg(int(mr.error.type)) - .arg(mr.error.len) + .arg(int(mr.invalid_type)) + .arg(mr.invalid_len) .arg(reader.currentPosition()) ); break; @@ -219,6 +218,10 @@ int main(int argc, char *argv[]) { QCommandLineOption aclOption(QStringList() << "A" << "acl", "Perform ACL filtering"); parser.addOption(aclOption); + // --msg-freq + QCommandLineOption msgFreqOption(QStringList() << "msg-freq", "Print message frequency table"); + parser.addOption(msgFreqOption); + // input file name parser.addPositionalArgument("input", "recording file", ""); @@ -241,6 +244,10 @@ int main(int argc, char *argv[]) { return !printRecordingVersion(inputfiles.at(0)); } + if(parser.isSet(msgFreqOption)) { + return printMessageFrequency(inputfiles.at(0)) ? 0 : 1; + } + if(!convertRecording( inputfiles.at(0), parser.value(outOption), diff --git a/src/tools/renderer.cpp b/src/tools/renderer.cpp index 11b82c937..634c21994 100644 --- a/src/tools/renderer.cpp +++ b/src/tools/renderer.cpp @@ -1,7 +1,7 @@ /* Drawpile - a collaborative drawing program. - Copyright (C) 2018 Calle Laakkonen + Copyright (C) 2018-2019 Calle Laakkonen Drawpile is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -63,7 +63,7 @@ QImage resizeImage(const QImage &img, const QSize &maxSize, bool fixedSize) bool saveImage(const DrawpileCmdSettings &settings, const paintcore::LayerStack &layers, ExportState &state) { - if(layers.size().isEmpty()) { + if(layers.size().isEmpty() || layers.layerCount()==0) { // The layer stack has no size until the first resize command. // Trying to export before it is not a fatal error. if(settings.verbose) @@ -167,21 +167,19 @@ bool renderDrawpileRecording(const DrawpileCmdSettings &settings) record = reader.readNext(); if(record.status == recording::MessageRecord::OK) { - protocol::MessagePtr msg(record.message); - - if(settings.acl && !aclfilter.filterMessage(*msg)) { + if(settings.acl && !aclfilter.filterMessage(*record.message)) { if(settings.verbose) fprintf(stderr, "[A] Filtered message %s from %d (idx %d @ %llx)", - qPrintable(msg->messageName()), - msg->contextId(), + qPrintable(record.message->messageName()), + record.message->contextId(), reader.currentIndex(), offset ); } - if(msg->isCommand()) { + if(record.message->isCommand()) { renderTime.start(); - statetracker.receiveCommand(msg); + statetracker.receiveCommand(protocol::MessagePtr::fromNullable(record.message)); totalRenderTime += renderTime.nsecsElapsed(); } @@ -192,7 +190,7 @@ bool renderDrawpileRecording(const DrawpileCmdSettings &settings) ++exportCounter; break; case ExportEvery::Sequence: - if(msg->type() == protocol::MSG_UNDOPOINT) + if(record.message->type() == protocol::MSG_UNDOPOINT) ++exportCounter; break; } @@ -208,7 +206,7 @@ bool renderDrawpileRecording(const DrawpileCmdSettings &settings) } else if(record.status == recording::MessageRecord::INVALID) { fprintf(stderr, "[E] Invalid message type %d at index %d, offset 0x%llx", - record.error.type, + record.invalid_type, reader.currentIndex(), offset ); diff --git a/src/tools/stats.cpp b/src/tools/stats.cpp new file mode 100644 index 000000000..5e587d8e6 --- /dev/null +++ b/src/tools/stats.cpp @@ -0,0 +1,111 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018-2019 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ + +#include "stats.h" +#include "../shared/record/reader.h" + +#include + +using namespace recording; + +struct MessageCount { + QString name; + unsigned int count; + unsigned int totalLength; + + MessageCount() : count(0), totalLength(0) {} +}; + +bool printMessageFrequency(const QString &filename) +{ + // Open recording + Reader reader(filename); + Compatibility compat = reader.open(); + + switch(compat) { + case NOT_DPREC: + fprintf(stderr, "Input file is not a Drawpile recording!\n"); + return false; + case CANNOT_READ: + fprintf(stderr, "Unable to read input file: %s\n", qPrintable(reader.errorString())); + return false; + + case INCOMPATIBLE: // As long as the message envelope format is the same, we don't care abotu the message content + case COMPATIBLE: + case MINOR_INCOMPATIBILITY: + case UNKNOWN_COMPATIBILITY: + // OK to proceed + break; + } + + // Count message types + MessageCount counts[256]; + unsigned int invalidCount = 0; + unsigned int totalCount = 0; + unsigned int totalLength = 0; + + bool notEof = true; + do { + MessageRecord mr = reader.readNext(); + switch(mr.status) { + case MessageRecord::OK: { + MessageCount &mc = counts[int(mr.message->type())]; + if(mc.name.isNull()) + mc.name = mr.message->messageName(); + mc.count++; + totalCount++; + mc.totalLength += mr.message->length(); + totalLength += mr.message->length(); + break; + } + case MessageRecord::INVALID: { + MessageCount &mc = counts[mr.invalid_type]; + invalidCount++; + mc.count++; + totalCount++; + mc.totalLength += mr.invalid_len; + totalLength += mr.invalid_len; + break; + } + case MessageRecord::END_OF_RECORDING: + notEof = false; + break; + } + } while(notEof); + + // Print frequency table + + for(int i=0;i<256;++i) { + if(counts[i].count==0) + continue; + + printf("%.2x %-17s %-9d %-9d (%.2f%%)\n", + i, + counts[i].name.isEmpty() ? "(unknown)" : qPrintable(counts[i].name), + counts[i].count, + counts[i].totalLength, + counts[i].totalLength/double(totalLength)*100 + ); + } + printf("Total count: %d\n", totalCount); + printf("Invalid messages: %d\n", invalidCount); + printf("Total length: %d (%.2f MB)\n", totalLength, totalLength/(1024.0*1024.0)); + + return true; +} diff --git a/src/tools/stats.h b/src/tools/stats.h new file mode 100644 index 000000000..a29e34501 --- /dev/null +++ b/src/tools/stats.h @@ -0,0 +1,30 @@ +/* + Drawpile - a collaborative drawing program. + + Copyright (C) 2018 Calle Laakkonen + + Drawpile 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 3 of the License, or + (at your option) any later version. + + Drawpile 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 Drawpile. If not, see . +*/ +#ifndef DPRECTOOL_STATS_H +#define DPRECTOOL_STATS_H + +class QString; + +/** + * @brief Print a message frequency table + * @param filename recording file + */ +bool printMessageFrequency(const QString &filename); + +#endif diff --git a/tests/test_puttile.dptxt b/tests/test_puttile.dptxt new file mode 100644 index 000000000..9fafd14fe --- /dev/null +++ b/tests/test_puttile.dptxt @@ -0,0 +1,68 @@ +!version=dp:4.21.2 + +1 resize bottom=320 right=320 +1 newlayer id=1 fill=#ffffffff { + title=PutTile test + } + +# Tile with bitmap content +1 puttile layer=1 col=0 row=0 { + img=AABAAHja7ZtPaFxFGMC/pmlB2/ivkJg0QS9KqQbaujRgCXiQ1lSFRsjJioYmkYoRVPZgob + img=Wx2YOHPYg1pRV70RxEtmSNtKdcFVq21sPeW9JUsOS4F2Xz+X37ZvfNm/1m3rz9k27FhB8z + img=b+b7vvm+ebPvzZs3D6Dtf0PEBJEhckSBWCNKRFlRUmUFJZNROkPwcP6NElmiSGCTFJWt0Q + img=6PuYdI22KmE4l0QpFOLNIJRjrRuKYoqLKMkhly90VatdVJcWfUGI74O7oNMLsTsLgLEHuJ + img=PoWU79XoC3RYl20I/VBSbT7ofpgl1nXferoA0xRDcQ/Fsa81sC22ybaNflhXPmz23zCxEo + img=l7K43f5wFLh8nnowJjCqn8aAxKpnQkaIPbMvphRfm0GX/T6ppda392L+D6cfJxijhhpFPa + img=cRxTMeUq5ba4TaMPysq3dv5l9TaHaUyucNynFWe0vERcvY+elue22QejH7Jtin1Rb2f6EG + img=D5G/JjgbigsSDk9dSUj2PBklewD+yL0QeLLY49p9vPTlLbeWJJkdeO80ZZ3pCN01ly2HbA + img=Phl9kGvHeV88R+1dF7hhKU9CkzYWv2j5OIj83nOXqJ1V4q7BqiXvklkV8kkR9NjHFl0Ppi + img=Pn/Xuyv0GUtVRnQ8MsL1vkfWzF6ZcNuY3AV6MPphu4v9fucZ9nAf9EP+551t1r0lYc7LNx + img=b0wyP6jNbcanaX5O9m5qFIS0gLKcVG6rL1hsJkG3z74bcyTfOW1F55lhwDyNqV9QZtlIq3 + img=mp3MWyZ+rSlfi5HMSg9UHcXLlHn89/sgJ4CWUuor0uKRebtHnR4RfHYDwvuJ6ZMlXZg7OA + img=ZzHKXMyxxJxH/ZwmN5fQno8PI7ORPsg4zn3lGXZbD+C764DvY8iMkNdTvV5ixpGfcZTPJG + img=xTKuNYOCbt2VkaA+lqH+3JAL6JAW8YaSM0o9sqOCZtDKSF+CvrNl3UT/tLgC9hlBTWlyWR + img=ScWUpWL0Uh51KUue4Zi6eiLrSOZ6XaVuRxqwH0MGNKRjs8xFv6OsX5Dpd8jp7bt81es4Nm + img=0MjIrz3CLxX/0vWufFwZrl6PaYHhxo8mz7nFnXiPAdRdIIUWUcY/Q3MFTrk+yg5deWcvwS + img=U8Iv2edq4XslSHnKpGL0VZ5jDMdA9d2EGvuvdNAVu01wjGH8E7U5z9AO4U46E3M3n4mpc9 + img=3dTZm4GURcO5LPFrscazgXCtZ2JvY6ZmZJZmlzMelZT11be5I/Zx1yQkwTL+hrRIVKPnOs + img=hTP6DodjDeKvvotEyJ2KebKLe+paNvB56lv2qFtO+NTnUc+xBvGv1d5bFX4QnsBvejxpFz + img=YZnxUEm++qrLCoPwsE6zxrv2NjqzzNrNI0u+pzrzEfONZwXSiIf3UNoYwIG4oyhsdSuVRn + img=6m049F26Nlw2N2J81bm7pscfjP9rNC7uopvVmLJVA0lu1WLHpz2X3SRwrOH4D65/X+YQrm + img=PnciNhnUueYw2vf8H97+0MwhIG5BVLBnmBJQt5S95Wbzt2tePjo2TzeEa//wXznwMTCBcw + img=YAHlvC8LDegttKHOFg/HGs5/gvnvY0MIZ6juNIZpUhrV20ybbI9jDee/4fPPW0WEExgwpb + img=Adm2VJ0jgZqY0p4dimJ/leTTnG6PNP+Py7J4twlGTGMEirjKFcLqHLjjnqXfpJ9JLY45Rj + img=jD7/husfO0YR9uHmsT9heaNyOhxj/RpguP71FI2PXpLrU/Ri/bFZZ+LS7RV0Jdk+D12XrN + img=TGrqJt/au2/rkF0rgVMEK3kbaD7gT13TG6Nn85Nsv6Z+03sAV6cDeUcIjkBwk99WEwpm4w + img=oY5Lz2XH9H2AYuLYLOvfkfcfg5DBEdJhDipGLOlBQaaat8mMGHZccqbOiMUHl8xIpR8yce + img=8/au+/uqmfXod1PEZ644SeSowL+bgym/6xBO2Me9gYr2wdXK/EFPP+K/L+cxhm8STpfkBU + img=0zhOeuTj7Pm2lUSfY/F4/1n3/nsSVpC3O80T5xTzxrFZNi/I+JadE+yasvMOOUmHY0jw/j + img=uy/2E3DOO39MD8HdmpclnL+5S7uNxgnU3e1GHfOYYE+x/q9r+8BtN4jWxJXLWUu+qvNmAn + img=aRtV2PcG9r/U7X/6FLJ4i+wxf2jcEtJbjjpJ1oak84dwbNNln5vY/1S3/+0CLOJfZLfV3L + img=fkfXWqx/e1PPva5P43ef8j/EiTyL8F/jHSVqDb9LfPPrZ4P3R0/yt8Re3cJu4obgvcMWQk + img=2TsWbjtkbW0FsG9t2gcd3f8MH1F7v3nya5PHfnbZpzbtf5b3v8OrWIafqO1ci7likNPSnH + img=F8peID+9Lm/e/y9w/Qjys0t0L4mjgvpOeFcrNOqpf06+tW4EPyYWCzvn+wf/8CKVyHj8mn + img=U4rPtHzr4ba4zQfw/Yv9+yfoxgwcwBK8Qz6+R0yqVGLSIuMqnyTbxyttcFsP8Psn9/dvsB + img=XT8CwW4WXy+TBxRKHnXWXV8rCuCIcqNtl2h3z/5vf9IzxK1+Wnyf/nKI5h4kUjtcM6rMs2 + img=Ovj7xwTfv3bhBGyn8fsIzVF2YgGewDV4skIBHqeynkody7DsQ/T96//fPz/E37//C2ERkB + img=U= + } + +# Tile with solid color and a repeat +1 puttile layer=1 col=1 row=0 repeat=1 color=#ff0000ff + +# Tile on a sublayer (green, merged) +1 puttile layer=1 sublayer=1 col=0 row=1 repeat=4 color=#ff00ff00 +1 penup + +# Tile on a sublayer (red, not merged) +1 puttile layer=1 sublayer=1 col=0 row=2 repeat=4 color=#ffff0000 + +# Tile on sublayer with attributes (yellow, merged) +1 layerattr layer=1 sublayer=2 opacity=50 blend=1 +1 puttile layer=1 sublayer=2 col=0 row=3 repeat=4 color=#ffffff00 +2 penup + +# Tile on sublayer with attributes (magenta, not merged) +1 layerattr layer=1 sublayer=3 opacity=50 blend=1 +1 puttile layer=1 sublayer=3 col=0 row=4 repeat=4 color=#ffff00ff sublayer=3 +