diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 51979e86..5fd6f88c 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -35,6 +35,11 @@ jobs: cache: 'pnpm' cache-dependency-path: ui/pnpm-lock.yaml + - name: Install Cairo dependencies + run: | + sudo apt-get update + sudo apt-get install -y libcairo2-dev pkg-config + - name: Build run: make build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 981e4161..008ffc1a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,12 @@ jobs: version: 9 run_install: false - - name: Build + - name: Install Cairo dependencies + run: | + sudo apt-get update + sudo apt-get install -y libcairo2-dev pkg-config + + - name: Build run: make all - name: Release diff --git a/Dockerfile b/Dockerfile index 2ae0b5ae..45c0db39 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,14 +14,25 @@ RUN pnpm install && pnpm build FROM golang:bookworm AS gobuilder ARG VERSION WORKDIR /src + +# Install Cairo development dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + libcairo2-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + COPY . . COPY --from=uibuilder /src/dist ./ui/dist -#RUN apk add git -RUN go generate ./... && CGO_ENABLED=0 go build -ldflags "-s -w -X main.version=${VERSION}" -o rmfakecloud-docker ./cmd/rmfakecloud/ +RUN go generate ./... && CGO_ENABLED=1 go build -tags cairo -ldflags "-s -w -X main.version=${VERSION}" -o rmfakecloud-docker ./cmd/rmfakecloud/ -FROM scratch +FROM debian:bookworm-slim EXPOSE 3000 -ADD ./docker/rootfs.tar / -COPY --from=gobuilder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ -COPY --from=gobuilder /src/rmfakecloud-docker / -ENTRYPOINT ["/rmfakecloud-docker"] + +# Install Cairo runtime libraries +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + libcairo2 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=gobuilder /src/rmfakecloud-docker /rmfakecloud +ENTRYPOINT ["/rmfakecloud"] diff --git a/Dockerfile.make b/Dockerfile.make index 405d162f..8c109014 100644 --- a/Dockerfile.make +++ b/Dockerfile.make @@ -1,5 +1,12 @@ -FROM scratch +FROM debian:bookworm-slim EXPOSE 3000 + +# Install Cairo runtime libraries +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libcairo2 \ + && rm -rf /var/lib/apt/lists/* + #ENV RMAPI_HWR_HMAC #ENV RM_SMTP_SERVER="" #ENV RM_SMTP_USERNAME="" diff --git a/Makefile b/Makefile index 5bacdfab..29d996de 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ LDFLAGS := "-s -w -X main.version=$(VERSION)" OUT_DIR := dist CMD := ./cmd/rmfakecloud BINARY := rmfakecloud -BUILD = go build -ldflags $(LDFLAGS) -o $(@) $(CMD) +BUILD = CGO_ENABLED=1 go build -tags cairo -ldflags $(LDFLAGS) -o $(@) $(CMD) ASSETS = ui/dist GOFILES := $(shell find . -iname '*.go' ! -iname "*_test.go") GOFILES += $(ASSETS) @@ -35,13 +35,13 @@ $(OUT_DIR)/$(BINARY)-arm64:$(GOFILES) GOARCH=arm64 $(BUILD) $(OUT_DIR)/$(BINARY)-docker:$(GOFILES) - CGO_ENABLED=0 $(BUILD) + $(BUILD) container: $(OUT_DIR)/$(BINARY)-docker docker build -t rmfakecloud -f Dockerfile.make . run: $(ASSETS) - go run $(CMD) $(ARG) + go run -tags cairo $(CMD) $(ARG) $(ASSETS): $(UIFILES) ui/pnpm-lock.yaml #@cp ui/node_modules/pdfjs-dist/build/pdf.worker.js ui/public/ @@ -70,5 +70,5 @@ testui: #CI=true $(PNPM) test testgo: - go test ./... + go test -tags cairo ./... diff --git a/docs/install/source.md b/docs/install/source.md index 3c3d22ef..79016274 100644 --- a/docs/install/source.md +++ b/docs/install/source.md @@ -10,6 +10,11 @@ To be able to compile from source, you'll need the following dependencies: * [pnpm](https://pnpm.io/) * [go](https://go.dev/) version 1.16 at least * make +* **Cairo graphics library** (required for PDF rendering with annotations) + - On Debian/Ubuntu: `sudo apt-get install libcairo2-dev pkg-config` + - On Fedora/RHEL: `sudo dnf install cairo-devel pkgconfig` + - On macOS: `brew install cairo pkg-config` + - On Arch Linux: `sudo pacman -S cairo pkgconf` Build ----- @@ -20,6 +25,8 @@ cd rmfakecloud make all ``` +**Note:** The build process requires Cairo to be installed, as it's used for rendering reMarkable annotations to PDF. The build uses the `-tags cairo` flag to enable Cairo support. If you encounter build errors related to Cairo, ensure the Cairo development libraries are properly installed on your system. + Installing ========== diff --git a/go.mod b/go.mod index 20ec771e..e6df3166 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ddvk/rmfakecloud -go 1.23.3 +go 1.24.0 toolchain go1.24.1 @@ -11,27 +11,24 @@ require ( github.com/golang-jwt/jwt/v4 v4.5.2 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.1 - github.com/juruen/rmapi v0.0.25 github.com/mochi-mqtt/server/v2 v2.7.9 + github.com/pdfcpu/pdfcpu v0.11.1 github.com/poundifdef/go-remarkable2pdf v0.2.0 github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 github.com/sirupsen/logrus v1.9.3 - github.com/soheilhy/cmux v0.1.5 github.com/stretchr/testify v1.9.0 github.com/studio-b12/gowebdav v0.9.0 - github.com/unidoc/unipdf/v3 v3.56.0 - golang.org/x/crypto v0.36.0 + github.com/ungerik/go-cairo v0.0.0-20240304075741-47de8851d267 + golang.org/x/crypto v0.43.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/adrg/strutil v0.3.1 // indirect - github.com/adrg/sysfont v0.1.2 // indirect - github.com/adrg/xdg v0.4.0 // indirect github.com/bytedance/sonic v1.11.3 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -40,32 +37,30 @@ require ( github.com/go-playground/validator/v10 v10.19.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/gorilla/i18n v0.0.0-20150820051429-8b358169da46 // indirect + github.com/hhrutter/lzw v1.0.0 // indirect + github.com/hhrutter/pkcs7 v0.2.0 // indirect + github.com/hhrutter/tiff v1.0.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/jung-kurt/gofpdf v1.16.2 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/pelletier/go-toml/v2 v2.2.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/xid v1.4.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect - github.com/unidoc/freetype v0.2.3 // indirect - github.com/unidoc/pkcs7 v0.2.0 // indirect - github.com/unidoc/timestamp v0.0.0-20200412005513-91597fd3793a // indirect - github.com/unidoc/unichart v0.3.0 // indirect - github.com/unidoc/unitype v0.4.0 // indirect golang.org/x/arch v0.7.0 // indirect - golang.org/x/image v0.18.0 // indirect - golang.org/x/net v0.38.0 // indirect + golang.org/x/image v0.32.0 // indirect + golang.org/x/net v0.45.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index d307a7fc..3ac3b4b1 100644 --- a/go.sum +++ b/go.sum @@ -33,14 +33,6 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/adrg/strutil v0.2.2/go.mod h1:EF2fjOFlGTepljfI+FzgTG13oXthR7ZAil9/aginnNQ= -github.com/adrg/strutil v0.3.1 h1:OLvSS7CSJO8lBii4YmBt8jiK9QOtB9CzCzwl4Ic/Fz4= -github.com/adrg/strutil v0.3.1/go.mod h1:8h90y18QLrs11IBffcGX3NW/GFBXCMcNg4M7H6MspPA= -github.com/adrg/sysfont v0.1.2 h1:MSU3KREM4RhsQ+7QgH7wPEPTgAgBIz0Hw6Nd4u7QgjE= -github.com/adrg/sysfont v0.1.2/go.mod h1:6d3l7/BSjX9VaeXWJt9fcrftFaD/t7l11xgSywCPZGk= -github.com/adrg/xdg v0.3.0/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ= -github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= -github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= @@ -58,6 +50,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/danjacques/gofslock v0.0.0-20240212154529-d899e02bfe22 h1:m+Fkk9QEMuV6Z1ithqqYogOHV7Pl6rMKe34NBTJTS/c= github.com/danjacques/gofslock v0.0.0-20240212154529-d899e02bfe22/go.mod h1:jXqs4TJbb7Xtl0FwUgBaOXty8edb/61H37U4D9E5EQE= @@ -146,12 +140,16 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gorilla/i18n v0.0.0-20150820051429-8b358169da46 h1:N+R2A3fGIr5GucoRMu2xpqyQWQlfY31orbofBCdjMz8= -github.com/gorilla/i18n v0.0.0-20150820051429-8b358169da46/go.mod h1:2Yoiy15Cf7Q3NFwfaJquh7Mk1uGI09ytcD7CUhn8j7s= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0= +github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo= +github.com/hhrutter/pkcs7 v0.2.0 h1:i4HN2XMbGQpZRnKBLsUwO3dSckzgX142TNqY/KfXg+I= +github.com/hhrutter/pkcs7 v0.2.0/go.mod h1:aEzKz0+ZAlz7YaEMY47jDHL14hVWD6iXt0AgqgAvWgE= +github.com/hhrutter/tiff v1.0.2 h1:7H3FQQpKu/i5WaSChoD1nnJbGx4MxU5TlNqqpxw55z8= +github.com/hhrutter/tiff v1.0.2/go.mod h1:pcOeuK5loFUE7Y/WnzGw20YxUdnqjY1P0Jlcieb/cCw= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= @@ -162,15 +160,11 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= -github.com/juruen/rmapi v0.0.25 h1:9i9LhzWBtSKRuhgzLWK5X013kNzb+X5rd+0WM5eHbC0= -github.com/juruen/rmapi v0.0.25/go.mod h1:w3sRs3dEsPenlZJec2x1iy9EGeKzIAAMrPeZ+iBDEnA= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -182,6 +176,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mochi-mqtt/server/v2 v2.7.9 h1:y0g4vrSLAag7T07l2oCzOa/+nKVLoazKEWAArwqBNYI= github.com/mochi-mqtt/server/v2 v2.7.9/go.mod h1:lZD3j35AVNqJL5cezlnSkuG05c0FCHSsfAKSPBOSbqc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -189,12 +185,14 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= -github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/pdfcpu/pdfcpu v0.11.1 h1:htHBSkGH5jMKWC6e0sihBFbcKZ8vG1M67c8/dJxhjas= +github.com/pdfcpu/pdfcpu v0.11.1/go.mod h1:pP3aGga7pRvwFWAm9WwFvo+V68DfANi9kxSQYioNYcw= github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/poundifdef/go-remarkable2pdf v0.2.0 h1:WDRh/ZBkpEOLPLj3lVfoHrQpyVkOQv2A3XOpViRZ0mE= @@ -210,11 +208,8 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo= github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY= -github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -222,7 +217,6 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -236,19 +230,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/unidoc/freetype v0.2.3 h1:uPqW+AY0vXN6K2tvtg8dMAtHTEvvHTN52b72XpZU+3I= -github.com/unidoc/freetype v0.2.3/go.mod h1:mJ/Q7JnqEoWtajJVrV6S1InbRv0K/fJerPB5SQs32KI= -github.com/unidoc/pkcs7 v0.0.0-20200411230602-d883fd70d1df/go.mod h1:UEzOZUEpJfDpywVJMUT8QiugqEZC29pDq7kdIZhWCr8= -github.com/unidoc/pkcs7 v0.2.0 h1:0Y0RJR5Zu7OuD+/l7bODXARn6b8Ev2G4A8lI4rzy9kg= -github.com/unidoc/pkcs7 v0.2.0/go.mod h1:UEzOZUEpJfDpywVJMUT8QiugqEZC29pDq7kdIZhWCr8= -github.com/unidoc/timestamp v0.0.0-20200412005513-91597fd3793a h1:RLtvUhe4DsUDl66m7MJ8OqBjq8jpWBXPK6/RKtqeTkc= -github.com/unidoc/timestamp v0.0.0-20200412005513-91597fd3793a/go.mod h1:j+qMWZVpZFTvDey3zxUkSgPJZEX33tDgU/QIA0IzCUw= -github.com/unidoc/unichart v0.3.0 h1:VX1j5yzhjrR3f2flC03Yat6/WF3h7Z+DLEvJLoTGhoc= -github.com/unidoc/unichart v0.3.0/go.mod h1:8JnLNKSOl8yQt1jXewNgYFHhFm5M6/ZiaydncFDpakA= -github.com/unidoc/unipdf/v3 v3.56.0 h1:15Lt+AZvELP03PH23ypV0y5reKZxCRKSq46SZg+vx6A= -github.com/unidoc/unipdf/v3 v3.56.0/go.mod h1:iBr/OsbLnJ49WhJlpfpYS3VmXrkTG05O7rKe9crppmc= -github.com/unidoc/unitype v0.4.0 h1:/TMZ3wgwfWWX64mU5x2O9no9UmoBqYCB089LYYqHyQQ= -github.com/unidoc/unitype v0.4.0/go.mod h1:HV5zuUeqMKA4QgYQq3KDlJY/P96XF90BQB+6czK6LVA= +github.com/ungerik/go-cairo v0.0.0-20240304075741-47de8851d267 h1:KA55kgg61iraQP4wSKIFRHwHIgDqim2Tvh8EXn7Udxw= +github.com/ungerik/go-cairo v0.0.0-20240304075741-47de8851d267/go.mod h1:yLTJg56omDJ+JVxZ5whpCrZgQdaSs+OBdFa+X6ViJcI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -268,8 +251,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -283,9 +266,8 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= -golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= +golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -333,11 +315,10 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -360,7 +341,6 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -385,16 +365,14 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -402,11 +380,10 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -456,8 +433,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -538,11 +513,12 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/archive/reader.go b/internal/archive/reader.go new file mode 100644 index 00000000..b90dd5df --- /dev/null +++ b/internal/archive/reader.go @@ -0,0 +1,347 @@ +package archive + +import ( + "archive/zip" + "bufio" + "encoding/json" + "errors" + "io" + "path" + "path/filepath" + "strconv" + "strings" + + "github.com/ddvk/rmfakecloud/internal/encoding/rm" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" +) + +// Read fills a Zip parsing a Remarkable archive file. +func (z *Zip) Read(r io.ReaderAt, size int64) error { + zr, err := zip.NewReader(r, size) + if err != nil { + return err + } + + // reading content first because it contains the number of pages + if err := z.readContent(zr); err != nil { + return err + } + + if err := z.readPayload(zr); err != nil { + return err + } + + //uploading and then downloading a file results in 0 pages + if z.Content.PageCount <= 0 { + log.Warn("PageCount is 0") + return nil + } + + if err := z.readMetadata(zr); err != nil { + return err + } + + if err := z.readPagedata(zr); err != nil { + return err + } + + if err := z.readData(zr); err != nil { + return err + } + + if err := z.readThumbnails(zr); err != nil { + return err + } + + return nil +} + +// readContent reads the .content file contained in an archive and the UUID +func (z *Zip) readContent(zr *zip.Reader) error { + files, err := zipExtFinder(zr, ".content") + if err != nil { + return err + } + + if len(files) != 1 { + return errors.New("archive does not contain a unique content file") + } + + contentFile := files[0] + file, err := contentFile.Open() + if err != nil { + return err + } + defer file.Close() + + bytes, err := io.ReadAll(file) + if err != nil { + return err + } + + if err = json.Unmarshal(bytes, &z.Content); err != nil { + return err + } + p := contentFile.FileInfo().Name() + id := docPathToName(p) + z.UUID = id + + redirectedCount := len(z.Content.RedirectionMap) + pagesCount := len(z.Content.Pages) + if redirectedCount > 0 { + z.pageMap = make(map[string]int) + z.Pages = make([]Page, redirectedCount) + for index, docPage := range z.Content.RedirectionMap { + if index >= pagesCount { + log.Warn("redirection > pages") + break + } + pageUUID := z.Content.Pages[index] + z.pageMap[pageUUID] = index + z.Pages[index].DocPage = docPage + } + + } else if pagesCount > 0 { + z.pageMap = make(map[string]int) + z.Pages = make([]Page, pagesCount) + for index, pageUUID := range z.Content.Pages { + z.pageMap[pageUUID] = index + z.Pages[index].DocPage = index + } + } else { + // instantiate the slice of pages + z.Pages = make([]Page, z.Content.PageCount) + } + return nil +} + +// readPagedata reads the .pagedata file contained in an archive +// and iterate to gather which template was used for each page. +func (z *Zip) readPagedata(zr *zip.Reader) error { + files, err := zipExtFinder(zr, ".pagedata") + if err != nil { + return err + } + + if len(files) != 1 { + return errors.New("archive does not contain a unique pagedata file") + } + + file, err := files[0].Open() + if err != nil { + return err + } + defer file.Close() + + // iterate pagedata file lines + sc := bufio.NewScanner(file) + var i int = 0 + for sc.Scan() { + line := sc.Text() + z.Pages[i].Pagedata = line + i++ + } + + if err := sc.Err(); err != nil { + return err + } + + return nil +} + +// readPayload tries to extract the payload from an archive if it exists. +func (z *Zip) readPayload(zr *zip.Reader) error { + ext := z.Content.FileType + files, err := zipExtFinder(zr, "."+ext) + if err != nil { + return err + } + + // return if not found + if len(files) != 1 { + return nil + } + + file, err := files[0].Open() + if err != nil { + return err + } + defer file.Close() + + z.Payload, err = io.ReadAll(file) + if err != nil { + return err + } + + return nil +} + +// readData extracts existing .rm files from an archive. +func (z *Zip) readData(zr *zip.Reader) error { + files, err := zipExtFinder(zr, ".rm") + if err != nil { + return err + } + + for _, file := range files { + name, _ := splitExt(file.FileInfo().Name()) + + idx, err := z.pageIndex(name) + if err != nil { + return err + } + + if len(z.Pages) <= idx { + return errors.New("page not found") + } + + r, err := file.Open() + if err != nil { + return err + } + + bytes, err := io.ReadAll(r) + if err != nil { + return err + } + + z.Pages[idx].Data = rm.New() + err = z.Pages[idx].Data.UnmarshalBinary(bytes) + if err != nil { + return err + } + } + + return nil +} + +// readThumbnails extracts existing thumbnails from an archive. +func (z *Zip) readThumbnails(zr *zip.Reader) error { + files, err := zipExtFinder(zr, ".jpg") + if err != nil { + return err + } + + for _, file := range files { + name, _ := splitExt(file.FileInfo().Name()) + + idx, err := strconv.Atoi(name) + if err != nil { + return errors.New("error in .jpg filename") + } + + if len(z.Pages) <= idx { + return errors.New("page not found") + } + + r, err := file.Open() + if err != nil { + return err + } + + z.Pages[idx].Thumbnail, err = io.ReadAll(r) + if err != nil { + return err + } + } + + return nil +} + +func (z *Zip) pageIndex(namePart string) (idx int, err error) { + idx, err = strconv.Atoi(namePart) + if err == nil { + return idx, nil + } + _, err = uuid.Parse(namePart) + if err != nil { + return -1, errors.New("neither int nor uuid page") + } + + if z.pageMap == nil { + return -1, errors.New("no uuid pagemap") + } + var ok bool + idx, ok = z.pageMap[namePart] + if !ok { + log.Warn("Page not found in map: ", namePart) + } + + return +} + +// readMetadata extracts existing .json metadata files from an archive. +func (z *Zip) readMetadata(zr *zip.Reader) error { + files, err := zipExtFinder(zr, ".json") + if err != nil { + return err + } + + for _, file := range files { + name, _ := splitExt(file.FileInfo().Name()) + + // name is 0-metadata.json or uuid-metadata + namePart := strings.TrimSuffix(name, "-metadata") + idx, err := z.pageIndex(namePart) + if err != nil { + return err + } + + if len(z.Pages) <= idx { + return errors.New("page not found") + } + + r, err := file.Open() + if err != nil { + return err + } + + bytes, err := io.ReadAll(r) + if err != nil { + return err + } + + err = json.Unmarshal(bytes, &z.Pages[idx].Metadata) + if err != nil { + return err + } + } + + return nil +} + +// splitExt splits the extension from a filename +func splitExt(name string) (string, string) { + ext := filepath.Ext(name) + return name[0 : len(name)-len(ext)], ext +} + +// zipExtFinder searches for a file matching the substr pattern +// in a zip file. +func zipExtFinder(zr *zip.Reader, ext string) ([]*zip.File, error) { + var files []*zip.File + + for _, file := range zr.File { + parentFolderName := path.Dir(file.FileHeader.Name) + if strings.HasSuffix(parentFolderName, ".highlights") { + continue + } + filename := file.FileInfo().Name() + if _, e := splitExt(filename); e == ext { + files = append(files, file) + } + } + + return files, nil +} + +// docPathToName extracts document name from path (simple version) +func docPathToName(p string) string { + name := filepath.Base(p) + ext := filepath.Ext(name) + if ext != "" { + name = name[0 : len(name)-len(ext)] + } + return name +} diff --git a/internal/archive/types.go b/internal/archive/types.go new file mode 100644 index 00000000..d8a079f4 --- /dev/null +++ b/internal/archive/types.go @@ -0,0 +1,160 @@ +// Package archive contains types for parsing reMarkable archive files +package archive + +import ( + "github.com/ddvk/rmfakecloud/internal/encoding/rm" +) + +// Set the default pagedata template to Blank +const defaultPagadata string = "Blank" + +// Zip represents an entire Remarkable archive file. +type Zip struct { + Content Content + Pages []Page + Payload []byte + UUID string + pageMap map[string]int +} + +// NewZip creates a File with sane defaults. +func NewZip() *Zip { + content := Content{ + DummyDocument: false, + ExtraMetadata: ExtraMetadata{ + LastBrushColor: "Black", + LastBrushThicknessScale: "2", + LastColor: "Black", + LastEraserThicknessScale: "2", + LastEraserTool: "Eraser", + LastPen: "Ballpoint", + LastPenColor: "Black", + LastPenThicknessScale: "2", + LastPencil: "SharpPencil", + LastPencilColor: "Black", + LastPencilThicknessScale: "2", + LastTool: "SharpPencil", + ThicknessScale: "2", + LastFinelinerv2Size: "1", + }, + FileType: "", + FontName: "", + LastOpenedPage: 0, + LineHeight: -1, + Margins: 100, + Orientation: "portrait", + PageCount: 0, + Pages: []string{}, + TextScale: 1, + Transform: Transform{ + M11: 1, + M12: 0, + M13: 0, + M21: 0, + M22: 1, + M23: 0, + M31: 0, + M32: 0, + M33: 1, + }, + } + + return &Zip{ + Content: content, + } +} + +// A Page represents a note page. +type Page struct { + // Data is the rm binary encoded file representing the drawn content + Data *rm.Rm + // Metadata is a json file containing information about layers + Metadata Metadata + // Thumbnail is a small image of the overall page + Thumbnail []byte + // Pagedata contains the name of the selected background template + Pagedata string + // page number of the underlying document + DocPage int +} + +// Metadata represents the structure of a .metadata json file associated to a page. +type Metadata struct { + Layers []Layer `json:"layers"` +} + +// Layers is a struct contained into a Metadata struct. +type Layer struct { + Name string `json:"name"` +} + +// Content represents the structure of a .content json file. +type Content struct { + DummyDocument bool `json:"dummyDocument"` + ExtraMetadata ExtraMetadata `json:"extraMetadata"` + + // FileType is "pdf", "epub" or empty for a simple note + FileType string `json:"fileType"` + FontName string `json:"fontName"` + LastOpenedPage int `json:"lastOpenedPage"` + LineHeight int `json:"lineHeight"` + Margins int `json:"margins"` + // Orientation can take "portrait" or "landscape". + Orientation string `json:"orientation"` + PageCount int `json:"pageCount"` + // Pages is a list of page IDs + Pages []string `json:"pages"` + Tags []string `json:"pageTags"` + RedirectionMap []int `json:"redirectionPageMap"` + TextScale int `json:"textScale"` + + Transform Transform `json:"transform"` +} + +// ExtraMetadata is a struct contained into a Content struct. +type ExtraMetadata struct { + LastBrushColor string `json:"LastBrushColor"` + LastBrushThicknessScale string `json:"LastBrushThicknessScale"` + LastColor string `json:"LastColor"` + LastEraserThicknessScale string `json:"LastEraserThicknessScale"` + LastEraserTool string `json:"LastEraserTool"` + LastPen string `json:"LastPen"` + LastPenColor string `json:"LastPenColor"` + LastPenThicknessScale string `json:"LastPenThicknessScale"` + LastPencil string `json:"LastPencil"` + LastPencilColor string `json:"LastPencilColor"` + LastPencilThicknessScale string `json:"LastPencilThicknessScale"` + LastTool string `json:"LastTool"` + ThicknessScale string `json:"ThicknessScale"` + LastFinelinerv2Size string `json:"LastFinelinerv2Size"` +} + +// Transform is a struct contained into a Content struct. +type Transform struct { + M11 float32 `json:"m11"` + M12 float32 `json:"m12"` + M13 float32 `json:"m13"` + M21 float32 `json:"m21"` + M22 float32 `json:"m22"` + M23 float32 `json:"m23"` + M31 float32 `json:"m31"` + M32 float32 `json:"m32"` + M33 float32 `json:"m33"` +} + +// MetadataFile content +type MetadataFile struct { + DocName string `json:"visibleName"` + CollectionType string `json:"type"` + Parent string `json:"parent"` + //LastModified in milliseconds + LastModified string `json:"lastModified"` + LastOpened string `json:"lastOpened"` + LastOpenedPage int `json:"lastOpenedPage"` + Version int `json:"version"` + Pinned bool `json:"pinned"` + Synced bool `json:"synced"` + Modified bool `json:"modified"` + Deleted bool `json:"deleted"` + MetadataModified bool `json:"metadatamodified"` +} diff --git a/internal/encoding/rm/marshal.go b/internal/encoding/rm/marshal.go new file mode 100644 index 00000000..c678eb5d --- /dev/null +++ b/internal/encoding/rm/marshal.go @@ -0,0 +1,8 @@ +package rm + +// MarshalBinary implements encoding.MarshalBinary for +// transforming a Rm page into bytes +// TODO +func (rm *Rm) MarshalBinary() (data []byte, err error) { + return nil, nil +} diff --git a/internal/encoding/rm/rm.go b/internal/encoding/rm/rm.go new file mode 100644 index 00000000..f30cd3e2 --- /dev/null +++ b/internal/encoding/rm/rm.go @@ -0,0 +1,180 @@ +// Package rm provides primitives for encoding and decoding +// the .rm format which is a proprietary format created by +// Remarkable to store the data of a drawing made with the device. +// +// Axel Huebl has made a great job of understanding this binary format and +// has written an excellent blog post that helped a lot for writting this package. +// https://plasma.ninja/blog/devices/remarkable/binary/format/2017/12/26/reMarkable-lines-file-format.html +// As well, he has its own implementation of this decoder in C++ at this repository. +// https://github.com/ax3l/lines-are-beautiful +// +// To mention that the format has since evolve to a new version labeled as v3 in the +// header. This implementation is targeting this new version. +// +// As Ben Johnson says, "In the Go standard library, we use the term encoding +// and marshaling for two separate but related ideas. An encoder in Go is an object +// that applies structure to a stream of bytes while marshaling refers +// to applying structure to bounded, in-memory bytes." +// https://medium.com/go-walkthrough/go-walkthrough-encoding-package-bc5e912232d +// +// We will follow this convention and refer to marshaling for this encoder/decoder +// because we want to transform a .rm binary into a bounded in-memory representation +// of a .rm file. +// +// To try to be as idiomatic as possible, this package implements the two following interfaces +// of the default encoding package (https://golang.org/pkg/encoding/). +// - BinaryMarshaler +// - BinaryUnmarshaler +// +// The scope of this package is defined as just the encoding/decoding of the .rm format. +// It will only deal with bytes and not files (one must take care of unzipping the archive +// taken from the device, extracting and providing the content of .rm file as bytes). +// +// This package won't be used for retrieving metadata or attached PDF, ePub files. +package rm + +import ( + "fmt" + "strings" +) + +// Version defines the version number of a remarkable note. +type Version int + +const ( + V3 Version = iota + V5 +) + +// Header starting a .rm binary file. This can help recognizing a .rm file. +const ( + HeaderV3 = "reMarkable .lines file, version=3 " + HeaderV5 = "reMarkable .lines file, version=5 " + HeaderLen = 43 +) + +// Width and Height of the device in pixels. +const ( + Width int = 1404 + Height int = 1872 +) + +// BrushColor defines the 3 colors of the brush. +type BrushColor uint32 + +// Mapping of the three colors. +const ( + Black BrushColor = 0 + Grey BrushColor = 1 + White BrushColor = 2 +) + +// BrushType respresents the type of brush. +// +// The different types of brush are explained here: +// https://blog.remarkable.com/how-to-find-your-perfect-writing-instrument-for-notetaking-on-remarkable-f53c8faeab77 +type BrushType uint32 + +// Mappings for brush types. +const ( + BallPoint BrushType = 2 + Marker BrushType = 3 + Fineliner BrushType = 4 + SharpPencil BrushType = 7 + TiltPencil BrushType = 1 + Brush BrushType = 0 + Highlighter BrushType = 5 + Eraser BrushType = 6 + EraseArea BrushType = 8 + + // v5 brings new brush type IDs + BallPointV5 BrushType = 15 + MarkerV5 BrushType = 16 + FinelinerV5 BrushType = 17 + SharpPencilV5 BrushType = 13 + TiltPencilV5 BrushType = 14 + BrushV5 BrushType = 12 + HighlighterV5 BrushType = 18 +) + +// BrushSize represents the base brush sizes. +type BrushSize float32 + +// 3 different brush sizes are noticed. +const ( + Small BrushSize = 1.875 + Medium BrushSize = 2.0 + Large BrushSize = 2.125 +) + +// A Rm represents an entire .rm file +// and is composed of layers. +type Rm struct { + Version Version + Layers []Layer +} + +// A Layer contains lines. +type Layer struct { + Lines []Line +} + +// A Line is composed of points. +type Line struct { + BrushType BrushType + BrushColor BrushColor + Padding uint32 + Unknown float32 + BrushSize BrushSize + Points []Point +} + +// A Point has coordinates. +type Point struct { + X float32 + Y float32 + Speed float32 + Direction float32 + Width float32 + Pressure float32 +} + +// New helps creating an empty Rm page. +// By mashaling an empty Rm page and exporting it +// to the device, we should generate an empty page +// as if it were created using the device itself. +// TODO +func New() *Rm { + return &Rm{} +} + +// String implements the fmt.Stringer interface +// The aim is to create a textual representation of a page as in the following image. +// https://plasma.ninja/blog/assets/reMarkable/2017_12_21_reMarkableAll.png +// TODO +func (rm Rm) String() string { + var o strings.Builder + + fmt.Fprintf(&o, "no of layers: %d\n", len(rm.Layers)) + for i, layer := range rm.Layers { + fmt.Fprintf(&o, "layer %d\n", i) + fmt.Fprintf(&o, " nb of lines: %d\n", len(layer.Lines)) + for j, line := range layer.Lines { + fmt.Fprintf(&o, " line %d\n", j) + fmt.Fprintf(&o, " brush type: %d\n", line.BrushType) + fmt.Fprintf(&o, " brush color: %d\n", line.BrushColor) + fmt.Fprintf(&o, " padding: %d\n", line.Padding) + fmt.Fprintf(&o, " brush size: %f\n", line.BrushSize) + fmt.Fprintf(&o, " nb of points: %d\n", len(line.Points)) + for k, point := range line.Points { + fmt.Fprintf(&o, " point %d\n", k) + fmt.Fprintf(&o, " coords: %f, %f\n", point.X, point.Y) + fmt.Fprintf(&o, " speed: %f\n", point.Speed) + fmt.Fprintf(&o, " direction: %f\n", point.Direction) + fmt.Fprintf(&o, " width: %f\n", point.Width) + fmt.Fprintf(&o, " pressure: %f\n", point.Pressure) + } + } + } + return o.String() +} diff --git a/internal/encoding/rm/unmarshal.go b/internal/encoding/rm/unmarshal.go new file mode 100644 index 00000000..e8198006 --- /dev/null +++ b/internal/encoding/rm/unmarshal.go @@ -0,0 +1,160 @@ +package rm + +import ( + "bytes" + "encoding/binary" + "fmt" +) + +// UnmarshalBinary implements encoding.UnmarshalBinary for +// transforming bytes into a Rm page +func (rm *Rm) UnmarshalBinary(data []byte) error { + r := newReader(data) + if err := r.checkHeader(); err != nil { + return err + } + rm.Version = r.version + + nbLayers, err := r.readNumber() + if err != nil { + return err + } + + rm.Layers = make([]Layer, nbLayers) + for i := uint32(0); i < nbLayers; i++ { + nbLines, err := r.readNumber() + if err != nil { + return err + } + + rm.Layers[i].Lines = make([]Line, nbLines) + for j := uint32(0); j < nbLines; j++ { + line, err := r.readLine() + if err != nil { + return err + } + rm.Layers[i].Lines[j] = line + } + } + + return nil +} + +type reader struct { + bytes.Reader + version Version +} + +func newReader(data []byte) reader { + br := bytes.NewReader(data) + + // we set V5 as default but the real value is + // analysed when checking the header + return reader{*br, V5} +} + +func (r *reader) checkHeader() error { + buf := make([]byte, HeaderLen) + + n, err := r.Read(buf) + if err != nil { + return err + } + + if n != HeaderLen { + return fmt.Errorf("Wrong header size") + } + + switch string(buf) { + case HeaderV5: + r.version = V5 + case HeaderV3: + r.version = V3 + default: + return fmt.Errorf("Unknown header") + } + + return nil +} + +func (r *reader) readNumber() (uint32, error) { + var nb uint32 + if err := binary.Read(r, binary.LittleEndian, &nb); err != nil { + return 0, fmt.Errorf("Wrong number read") + } + return nb, nil +} + +func (r *reader) readLine() (Line, error) { + var line Line + + if err := binary.Read(r, binary.LittleEndian, &line.BrushType); err != nil { + return line, fmt.Errorf("Failed to read line") + } + + if err := binary.Read(r, binary.LittleEndian, &line.BrushColor); err != nil { + return line, fmt.Errorf("Failed to read line") + } + + if err := binary.Read(r, binary.LittleEndian, &line.Padding); err != nil { + return line, fmt.Errorf("Failed to read line") + } + + if err := binary.Read(r, binary.LittleEndian, &line.BrushSize); err != nil { + return line, fmt.Errorf("Failed to read line") + } + + // this new attribute has been added in v5 + if r.version == V5 { + if err := binary.Read(r, binary.LittleEndian, &line.Unknown); err != nil { + return line, fmt.Errorf("Failed to read line") + } + } + + nbPoints, err := r.readNumber() + if err != nil { + return line, err + } + + if nbPoints == 0 { + return line, nil + } + + line.Points = make([]Point, nbPoints) + + for i := uint32(0); i < nbPoints; i++ { + p, err := r.readPoint() + if err != nil { + return line, err + } + + line.Points[i] = p + } + + return line, nil +} + +func (r *reader) readPoint() (Point, error) { + var point Point + + if err := binary.Read(r, binary.LittleEndian, &point.X); err != nil { + return point, fmt.Errorf("Failed to read point") + } + if err := binary.Read(r, binary.LittleEndian, &point.Y); err != nil { + return point, fmt.Errorf("Failed to read point") + } + if err := binary.Read(r, binary.LittleEndian, &point.Speed); err != nil { + return point, fmt.Errorf("Failed to read point") + } + if err := binary.Read(r, binary.LittleEndian, &point.Direction); err != nil { + return point, fmt.Errorf("Failed to read point") + } + if err := binary.Read(r, binary.LittleEndian, &point.Width); err != nil { + return point, fmt.Errorf("Failed to read point") + } + if err := binary.Read(r, binary.LittleEndian, &point.Pressure); err != nil { + return point, fmt.Errorf("Failed to read point") + } + + return point, nil +} diff --git a/internal/storage/exporter/license.go b/internal/storage/exporter/license.go deleted file mode 100644 index 6092c503..00000000 --- a/internal/storage/exporter/license.go +++ /dev/null @@ -1,21 +0,0 @@ -package exporter - -import ( - "time" - //blah - _ "unsafe" - - "github.com/unidoc/unipdf/v3/common/license" -) - -//go:linkname licenseKey github.com/unidoc/unipdf/v3/internal/license._gbdb -var licenseKey *license.LicenseKey - -func init() { - lk := license.LicenseKey{} - lk.CustomerName = "community" - lk.Tier = license.LicenseTierCommunity - lk.CreatedAt = time.Now().UTC() - lk.CreatedAtInt = lk.CreatedAt.Unix() - licenseKey = &lk -} diff --git a/internal/storage/exporter/myarchive.go b/internal/storage/exporter/myarchive.go index 0c681830..70cf721a 100644 --- a/internal/storage/exporter/myarchive.go +++ b/internal/storage/exporter/myarchive.go @@ -3,15 +3,9 @@ package exporter import ( "io" - "github.com/juruen/rmapi/archive" - "github.com/juruen/rmapi/log" + "github.com/ddvk/rmfakecloud/internal/archive" ) -// rmapi's logging stuff -func init() { - log.InitLog() -} - // MyArchive but having the payload reader type MyArchive struct { archive.Zip diff --git a/internal/storage/exporter/pdf.go b/internal/storage/exporter/pdf.go deleted file mode 100644 index ac7bd46b..00000000 --- a/internal/storage/exporter/pdf.go +++ /dev/null @@ -1,237 +0,0 @@ -package exporter - -import ( - "errors" - "fmt" - "io" - - "github.com/juruen/rmapi/encoding/rm" - "github.com/sirupsen/logrus" - "github.com/unidoc/unipdf/v3/annotator" - "github.com/unidoc/unipdf/v3/contentstream" - "github.com/unidoc/unipdf/v3/contentstream/draw" - "github.com/unidoc/unipdf/v3/core" - "github.com/unidoc/unipdf/v3/creator" - pdf "github.com/unidoc/unipdf/v3/model" -) - -const ( - DeviceWidth = 1404 - DeviceHeight = 1872 -) - -var rmPageSize = creator.PageSize{445, 594} - -type PdfGenerator struct { - options PdfGeneratorOptions - pdfReader *pdf.PdfReader - template bool -} - -type PdfGeneratorOptions struct { - AddPageNumbers bool - AllPages bool - AnnotationsOnly bool //export the annotations without the background/pdf -} - -func normalized(p1 rm.Point, ratioX float64) (float64, float64) { - return float64(p1.X) * ratioX, float64(p1.Y) * ratioX -} - -func (p *PdfGenerator) Generate(zip *MyArchive, output io.Writer, options PdfGeneratorOptions) (err error) { - - p.options = options - - if len(zip.Pages) == 0 { - if zip.PayloadReader != nil { - _, err := io.Copy(output, zip.PayloadReader) - return err - } - - return errors.New("the document has no pages") - } - - if err = p.initBackgroundPages(zip.PayloadReader); err != nil { - return err - } - - c := creator.New() - if p.template { - // use the standard page size - c.SetPageSize(rmPageSize) - } - - if p.pdfReader != nil && p.options.AllPages { - logrus.Info("generating all pages") - outlines := p.pdfReader.GetOutlineTree() - c.SetOutlineTree(outlines) - } - - for i, pageAnnotations := range zip.Pages { - hasContent := pageAnnotations.Data != nil - - // do not add a page when there are no annotations - if !p.options.AllPages && !hasContent { - continue - } - - page, err := p.addBackgroundPage(c, i+1) - if err != nil { - return err - } - - ratio := c.Height() / c.Width() - - var scale float64 - if ratio < 1.33 { - scale = c.Width() / DeviceWidth - } else { - scale = c.Height() / DeviceHeight - } - if page == nil { - logrus.Fatal("page is null") - } - - if err != nil { - return err - } - if !hasContent { - continue - } - - contentCreator := contentstream.NewContentCreator() - contentCreator.Add_q() - - for _, layer := range pageAnnotations.Data.Layers { - for _, line := range layer.Lines { - if len(line.Points) < 1 { - continue - } - if line.BrushType == rm.Eraser || line.BrushType == rm.EraseArea { - continue - } - - if line.BrushType == rm.HighlighterV5 { - last := len(line.Points) - 1 - x1, y1 := normalized(line.Points[0], scale) - x2, _ := normalized(line.Points[last], scale) - // make horizontal lines only, use y1 - width := scale * 30 - y1 += width / 2 - - lineDef := annotator.LineAnnotationDef{X1: x1 - 1, Y1: c.Height() - y1, X2: x2, Y2: c.Height() - y1} - lineDef.LineColor = pdf.NewPdfColorDeviceRGB(1.0, 1.0, 0.0) //yellow - lineDef.Opacity = 0.5 - lineDef.LineWidth = width - ann, err := annotator.CreateLineAnnotation(lineDef) - if err != nil { - return err - } - page.AddAnnotation(ann) - } else { - path := draw.NewPath() - for i := 0; i < len(line.Points); i++ { - x1, y1 := normalized(line.Points[i], scale) - path = path.AppendPoint(draw.NewPoint(x1, c.Height()-y1)) - } - - contentCreator.Add_w(float64(line.BrushSize / 10)) - - switch line.BrushColor { - case rm.Black: - contentCreator.Add_rg(1.0, 1.0, 1.0) - case rm.White: - contentCreator.Add_rg(0.0, 0.0, 0.0) - case rm.Grey: - contentCreator.Add_rg(0.8, 0.8, 0.8) - } - - //TODO: use bezier - draw.DrawPathWithCreator(path, contentCreator) - - contentCreator.Add_S() - } - } - } - contentCreator.Add_Q() - drawingOperations := contentCreator.Operations().String() - pageContentStreams, err := page.GetAllContentStreams() - if err != nil { - return err - } - //hack: wrap the page content in a context to prevent transformation matrix misalignment - wrapper := []string{"q", pageContentStreams, "Q", drawingOperations} - page.SetContentStreams(wrapper, core.NewFlateEncoder()) - } - - return c.Write(output) -} - -func (p *PdfGenerator) initBackgroundPages(r io.ReadSeeker) error { - if r != nil { - pdfReader, err := pdf.NewPdfReader(r) - if err != nil { - return err - } - - encrypted, err := pdfReader.IsEncrypted() - if err != nil { - return nil - } - if encrypted { - valid, err := pdfReader.Decrypt([]byte("")) - if err != nil { - return err - } - if !valid { - return fmt.Errorf("cannot decrypt") - } - - } - - p.pdfReader = pdfReader - p.template = false - return nil - } - - logrus.Info("template") - p.template = true - return nil -} - -func (p *PdfGenerator) addBackgroundPage(c *creator.Creator, pageNum int) (*pdf.PdfPage, error) { - var page *pdf.PdfPage - - if !p.template && !p.options.AnnotationsOnly { - tmpPage, err := p.pdfReader.GetPage(pageNum) - if err != nil { - return nil, err - } - mbox, err := tmpPage.GetMediaBox() - if err != nil { - return nil, err - } - - // TODO: adjust the page if cropped - pageHeight := mbox.Ury - mbox.Lly - pageWidth := mbox.Urx - mbox.Llx - // use the pdf's page size - c.SetPageSize(creator.PageSize{pageWidth, pageHeight}) - c.AddPage(tmpPage) - page = tmpPage - } else { - page = c.NewPage() - } - - if p.options.AddPageNumbers { - c.DrawFooter(func(block *creator.Block, args creator.FooterFunctionArgs) { - p := c.NewParagraph(fmt.Sprintf("%d", args.PageNum)) - p.SetFontSize(8) - w := block.Width() - 20 - h := block.Height() - 10 - p.SetPos(w, h) - block.Draw(p) - }) - } - return page, nil -} diff --git a/internal/storage/exporter/pdf_cairo.go b/internal/storage/exporter/pdf_cairo.go new file mode 100644 index 00000000..6fba2018 --- /dev/null +++ b/internal/storage/exporter/pdf_cairo.go @@ -0,0 +1,393 @@ +//go:build cairo + +package exporter + +import ( + "bytes" + "fmt" + "io" + "os" + "unsafe" + + "github.com/ddvk/rmfakecloud/internal/encoding/rm" + "github.com/pdfcpu/pdfcpu/pkg/api" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" + "github.com/sirupsen/logrus" + "github.com/ungerik/go-cairo" +) + +/* +#cgo pkg-config: cairo +#include +#include +#include +*/ +import "C" + +const ( + DeviceWidth = 1404 + DeviceHeight = 1872 +) + +// rmPageSize is the default page size for blank templates (in PDF points: 1/72 inch) +var rmPageSize = struct{ Width, Height float64 }{445, 594} + +type PdfGenerator struct { + options PdfGeneratorOptions + backgroundPDF []byte + template bool +} + +type PdfGeneratorOptions struct { + AddPageNumbers bool + AllPages bool + AnnotationsOnly bool //export the annotations without the background/pdf +} + +func normalized(p1 rm.Point, scale float64) (float64, float64) { + return float64(p1.X) * scale, float64(p1.Y) * scale +} + +// setPDFPageSize sets the size for the current page in a PDF surface +func setPDFPageSize(surface *cairo.Surface, width, height float64) { + surfacePtr, _ := surface.Native() + C.cairo_pdf_surface_set_size((*C.cairo_surface_t)(unsafe.Pointer(surfacePtr)), C.double(width), C.double(height)) +} + +func (p *PdfGenerator) Generate(zip *MyArchive, output io.Writer, options PdfGeneratorOptions) error { + p.options = options + + if len(zip.Pages) == 0 { + if zip.PayloadReader != nil { + _, err := io.Copy(output, zip.PayloadReader) + return err + } + return fmt.Errorf("the document has no pages") + } + + if err := p.initBackgroundPages(zip.PayloadReader); err != nil { + return err + } + + // If we have a background PDF and not annotations-only mode, we need a two-step process + if p.backgroundPDF != nil && !p.options.AnnotationsOnly { + return p.generateWithBackground(zip, output) + } + + // Otherwise, simple case: just annotations or blank pages + return p.generateAnnotationsOnly(zip, output) +} + +func (p *PdfGenerator) generateAnnotationsOnly(zip *MyArchive, output io.Writer) error { + // Create a temporary file for PDF output (Cairo requires a file path) + tmpFile, err := os.CreateTemp("", "rmfakecloud-annotations-*.pdf") + if err != nil { + return fmt.Errorf("failed to create temp file: %w", err) + } + tmpPath := tmpFile.Name() + tmpFile.Close() + defer os.Remove(tmpPath) + + // Determine first page dimensions + var firstWidth, firstHeight float64 + if p.template { + firstWidth, firstHeight = rmPageSize.Width, rmPageSize.Height + } else { + // TODO: Get dimensions from background PDF + firstWidth, firstHeight = rmPageSize.Width, rmPageSize.Height + } + + // Create PDF surface + pdfSurface := cairo.NewPDFSurface(tmpPath, firstWidth, firstHeight, cairo.PDF_VERSION_1_5) + defer pdfSurface.Finish() + + pageCount := 0 + for _, pageAnnotations := range zip.Pages { + hasContent := pageAnnotations.Data != nil + + // Skip pages without content unless AllPages is set + if !p.options.AllPages && !hasContent { + continue + } + + pageCount++ + + // Set page size (for pages after the first) + if pageCount > 1 { + var pageWidth, pageHeight float64 + if p.template { + pageWidth, pageHeight = rmPageSize.Width, rmPageSize.Height + } else { + // TODO: Get dimensions from background PDF page + pageWidth, pageHeight = rmPageSize.Width, rmPageSize.Height + } + setPDFPageSize(pdfSurface, pageWidth, pageHeight) + } + + // Calculate scale + pageWidth := firstWidth + pageHeight := firstHeight + ratio := pageHeight / pageWidth + + var scale float64 + if ratio < 1.33 { + scale = pageWidth / DeviceWidth + } else { + scale = pageHeight / DeviceHeight + } + + // Draw annotations if present + if hasContent { + if err := p.drawAnnotations(pdfSurface, pageAnnotations.Data, scale, pageHeight); err != nil { + return err + } + } + + // Add page numbers if requested + if p.options.AddPageNumbers { + p.drawPageNumber(pdfSurface, pageCount, pageWidth, pageHeight) + } + + // Show page (prepare for next page) + if pageCount < len(zip.Pages) || p.options.AllPages { + pdfSurface.ShowPage() + } + } + + pdfSurface.Finish() + + // Copy temp file to output + tmpFileRead, err := os.Open(tmpPath) + if err != nil { + return fmt.Errorf("failed to open temp file: %w", err) + } + defer tmpFileRead.Close() + + _, err = io.Copy(output, tmpFileRead) + return err +} + +func (p *PdfGenerator) generateWithBackground(zip *MyArchive, output io.Writer) error { + // Step 1: Create annotations-only PDF + tmpAnnotations, err := os.CreateTemp("", "rmfakecloud-annotations-*.pdf") + if err != nil { + return fmt.Errorf("failed to create temp annotations file: %w", err) + } + tmpAnnotationsPath := tmpAnnotations.Name() + tmpAnnotations.Close() + defer os.Remove(tmpAnnotationsPath) + + // Generate annotations PDF to temp file + annotationsFile, err := os.Create(tmpAnnotationsPath) + if err != nil { + return fmt.Errorf("failed to create annotations file: %w", err) + } + if err := p.generateAnnotationsOnly(zip, annotationsFile); err != nil { + annotationsFile.Close() + return err + } + annotationsFile.Close() + + // Step 2: Write background PDF to temp file + tmpBackground, err := os.CreateTemp("", "rmfakecloud-background-*.pdf") + if err != nil { + return fmt.Errorf("failed to create temp background file: %w", err) + } + tmpBackgroundPath := tmpBackground.Name() + if _, err := tmpBackground.Write(p.backgroundPDF); err != nil { + tmpBackground.Close() + os.Remove(tmpBackgroundPath) + return fmt.Errorf("failed to write background PDF: %w", err) + } + tmpBackground.Close() + defer os.Remove(tmpBackgroundPath) + + // Step 3: Merge background and annotations using pdfcpu + tmpOutput, err := os.CreateTemp("", "rmfakecloud-merged-*.pdf") + if err != nil { + return fmt.Errorf("failed to create temp output file: %w", err) + } + tmpOutputPath := tmpOutput.Name() + tmpOutput.Close() + defer os.Remove(tmpOutputPath) + + outFile, err := os.Create(tmpOutputPath) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer outFile.Close() + + // Open both PDFs as ReadSeekers + bgFile, err := os.Open(tmpBackgroundPath) + if err != nil { + return fmt.Errorf("failed to open background PDF: %w", err) + } + defer bgFile.Close() + + annFile, err := os.Open(tmpAnnotationsPath) + if err != nil { + return fmt.Errorf("failed to open annotations PDF: %w", err) + } + defer annFile.Close() + + // Merge: background first, then overlay annotations + conf := model.NewDefaultConfiguration() + rsc := []io.ReadSeeker{bgFile, annFile} + if err := api.MergeRaw(rsc, outFile, false, conf); err != nil { + return fmt.Errorf("failed to merge PDFs: %w", err) + } + outFile.Close() + + // Copy merged result to output + mergedFile, err := os.Open(tmpOutputPath) + if err != nil { + return fmt.Errorf("failed to open merged file: %w", err) + } + defer mergedFile.Close() + + _, err = io.Copy(output, mergedFile) + return err +} + +func (p *PdfGenerator) drawAnnotations(surface *cairo.Surface, rmData *rm.Rm, scale, pageHeight float64) error { + surface.Save() + defer surface.Restore() + + for _, layer := range rmData.Layers { + for _, line := range layer.Lines { + if len(line.Points) < 1 { + continue + } + if line.BrushType == rm.Eraser || line.BrushType == rm.EraseArea { + continue + } + + if line.BrushType == rm.HighlighterV5 { + // Draw highlighter as semi-transparent rectangle + p.drawHighlighter(surface, line, scale, pageHeight) + } else { + // Draw regular stroke + p.drawStroke(surface, line, scale, pageHeight) + } + } + } + + return nil +} + +func (p *PdfGenerator) drawHighlighter(surface *cairo.Surface, line rm.Line, scale, pageHeight float64) { + if len(line.Points) < 2 { + return + } + + last := len(line.Points) - 1 + x1, y1 := normalized(line.Points[0], scale) + x2, _ := normalized(line.Points[last], scale) + + // Highlighter width + width := scale * 30 + y1 += width / 2 + + // Convert Y coordinate (Cairo origin is top-left, PDF is bottom-left) + y := pageHeight - y1 + + // Yellow color with 50% opacity + surface.SetSourceRGBA(1.0, 1.0, 0.0, 0.5) + surface.SetLineWidth(width) + surface.SetLineCap(cairo.LINE_CAP_BUTT) + + surface.MoveTo(x1, y) + surface.LineTo(x2, y) + surface.Stroke() +} + +func (p *PdfGenerator) drawStroke(surface *cairo.Surface, line rm.Line, scale, pageHeight float64) { + if len(line.Points) < 1 { + return + } + + // Set stroke color + var r, g, b float64 + switch line.BrushColor { + case rm.Black: + r, g, b = 0.0, 0.0, 0.0 + case rm.White: + r, g, b = 1.0, 1.0, 1.0 + case rm.Grey: + r, g, b = 0.5, 0.5, 0.5 + default: + r, g, b = 0.0, 0.0, 0.0 + } + surface.SetSourceRGB(r, g, b) + + // Set stroke width + // Formula from original: line.BrushSize*6.0 - 10.8 + strokeWidth := float64(line.BrushSize)*6.0 - 10.8 + if strokeWidth < 0.5 { + strokeWidth = 0.5 + } + surface.SetLineWidth(strokeWidth) + + // Set line cap + surface.SetLineCap(cairo.LINE_CAP_ROUND) + surface.SetLineJoin(cairo.LINE_JOIN_ROUND) + + // Draw path + for i, point := range line.Points { + x, y := normalized(point, scale) + // Convert Y coordinate + y = pageHeight - y + + if i == 0 { + surface.MoveTo(x, y) + } else { + surface.LineTo(x, y) + } + } + + surface.Stroke() +} + +func (p *PdfGenerator) drawPageNumber(surface *cairo.Surface, pageNum int, pageWidth, pageHeight float64) { + surface.Save() + defer surface.Restore() + + surface.SelectFontFace("sans-serif", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL) + surface.SetFontSize(8.0) + surface.SetSourceRGB(0, 0, 0) + + text := fmt.Sprintf("%d", pageNum) + surface.MoveTo(pageWidth-20, pageHeight-10) + surface.ShowText(text) +} + +func (p *PdfGenerator) initBackgroundPages(r io.ReadSeeker) error { + if r != nil { + // Read the PDF into memory + pdfBytes, err := io.ReadAll(r) + if err != nil { + return fmt.Errorf("failed to read background PDF: %w", err) + } + + // Check if PDF is encrypted and handle with pdfcpu + rs := bytes.NewReader(pdfBytes) + ctx, err := api.ReadContext(rs, model.NewDefaultConfiguration()) + if err != nil { + return fmt.Errorf("failed to read PDF: %w", err) + } + + // Check if encrypted by checking if Encrypt field exists + if ctx.XRefTable.Encrypt != nil { + logrus.Info("PDF is encrypted - pdfcpu will handle decryption") + // pdfcpu's ReadContext already handles decryption with empty password + } + + p.backgroundPDF = pdfBytes + p.template = false + return nil + } + + logrus.Info("template") + p.template = true + return nil +} diff --git a/internal/storage/exporter/pdf_stub.go b/internal/storage/exporter/pdf_stub.go new file mode 100644 index 00000000..0492470d --- /dev/null +++ b/internal/storage/exporter/pdf_stub.go @@ -0,0 +1,27 @@ +//go:build !cairo + +package exporter + +import ( + "errors" + "io" +) + +const ( + DeviceWidth = 1404 + DeviceHeight = 1872 +) + +type PdfGenerator struct { + options PdfGeneratorOptions +} + +type PdfGeneratorOptions struct { + AddPageNumbers bool + AllPages bool + AnnotationsOnly bool +} + +func (p *PdfGenerator) Generate(zip *MyArchive, output io.Writer, options PdfGeneratorOptions) error { + return errors.New("PDF generation with annotations requires building with Cairo support. Build with: go build -tags cairo") +} diff --git a/internal/storage/exporter/render.go b/internal/storage/exporter/render.go index 7d2fe0dd..974343f0 100644 --- a/internal/storage/exporter/render.go +++ b/internal/storage/exporter/render.go @@ -38,8 +38,8 @@ func RenderPoundifdef(input, output string) (io.ReadCloser, error) { return writer, nil } -// RenderRmapi renders with rmapi -func RenderRmapi(a *MyArchive, output io.Writer) error { +// RenderPDF renders a reMarkable archive to PDF using the Cairo-based PDF generator +func RenderPDF(a *MyArchive, output io.Writer) error { pdfgen := PdfGenerator{} options := PdfGeneratorOptions{ AllPages: true, diff --git a/internal/storage/fs/blobstore.go b/internal/storage/fs/blobstore.go index c73dbb48..f2f47a6e 100644 --- a/internal/storage/fs/blobstore.go +++ b/internal/storage/fs/blobstore.go @@ -85,7 +85,7 @@ func (fs *FileSystemStorage) Export(uid, docid string) (r io.ReadCloser, err err } reader, writer := io.Pipe() go func() { - err = exporter.RenderRmapi(archive, writer) + err = exporter.RenderPDF(archive, writer) if err != nil { log.Error(err) writer.Close() diff --git a/internal/storage/fs/documents.go b/internal/storage/fs/documents.go index f6a4335a..8e09d86b 100644 --- a/internal/storage/fs/documents.go +++ b/internal/storage/fs/documents.go @@ -98,7 +98,7 @@ func (fs *FileSystemStorage) ExportDocument(uid, id, outputType string, exportOp return nil, err } - err = exporter.RenderRmapi(arch, outputFile) + err = exporter.RenderPDF(arch, outputFile) if err != nil { return nil, err } diff --git a/internal/storage/models/archive.go b/internal/storage/models/archive.go index d244db6d..f1945494 100644 --- a/internal/storage/models/archive.go +++ b/internal/storage/models/archive.go @@ -6,10 +6,10 @@ import ( "path" "strings" + "github.com/ddvk/rmfakecloud/internal/archive" + "github.com/ddvk/rmfakecloud/internal/encoding/rm" "github.com/ddvk/rmfakecloud/internal/storage" "github.com/ddvk/rmfakecloud/internal/storage/exporter" - "github.com/juruen/rmapi/archive" - "github.com/juruen/rmapi/encoding/rm" log "github.com/sirupsen/logrus" )