diff --git a/CHANGELOG.md b/CHANGELOG.md index b92dd4f..a830149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## rmapi master +* Added geta and mgeta command to read annotated notebooks + * Increased http timeout to 5 minutes to enable upload of larger files * Add user-agent header to be a good reMarkable citezen diff --git a/README.md b/README.md index b8d7d41..6785f2b 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,12 @@ documents through a FUSE system. You can read further at the bottom of this file Install and build the project: -`go get -u github.com/juruen/rmapi` +`go get -u github.com/peerdavid/rmapi` + ## Binary -You can download an already built version for either Linux or OSX from [releases](https://github.com/juruen/rmapi/releases). +You can download an already built version for either Linux or OSX from [releases](https://github.com/peerdavid/rmapi/releases). # API support @@ -88,6 +89,21 @@ mput /Papers Use `get path_to_file` to download a file from the cloud to your local computer. +## [optional] Download a file with annotations +Use `geta path_to_file` to download a file from the cloud to your local computer. +This command is only available, if all necessary tools are installed: + - convert (Note: Policy must be set to read|write for pdfs: https://stackoverflow.com/questions/42928765/convertnot-authorized-aaaa-error-constitute-c-readimage-453) + - rsvg-convert + - pdf270 + - pdftk + - python3 + + +Note: If there exists a template folder under `%HOME/Templates/Remarkable`, +then those are used for notebook backgrounds. Otherwise the background is +simply white. + + ## Recursively download directories and files Use `mget path_to_dir` to recursively download all the files in that directory. @@ -98,6 +114,18 @@ E.g: download all the files mget . ``` +## [optional] Recursively download directories and files with annotations + +Use `mgeta path_to_dir` to recursively download all the annotated files in that directory. +Note: This command is only available if the geta command is also available. + +E.g: download all the files + +``` +mgeta . +``` + + ## Create a directoy Use `mkdir path_to_new_dir` to create a new directory @@ -148,3 +176,8 @@ If you want to give it a go, you can run: ```bash rmapi --fuse-mount mount_point ``` + + +# Thanks to +[1] rmapi, https://github.com/peerdavid/rmapi
+[1] rM2svg, https://github.com/reHackable/maxio
diff --git a/api/api.go b/api/api.go index 53d317c..3abf186 100644 --- a/api/api.go +++ b/api/api.go @@ -7,11 +7,11 @@ import ( "io/ioutil" "os" - "github.com/juruen/rmapi/filetree" - "github.com/juruen/rmapi/log" - "github.com/juruen/rmapi/model" - "github.com/juruen/rmapi/transport" - "github.com/juruen/rmapi/util" + "github.com/peerdavid/rmapi/filetree" + "github.com/peerdavid/rmapi/log" + "github.com/peerdavid/rmapi/model" + "github.com/peerdavid/rmapi/transport" + "github.com/peerdavid/rmapi/util" ) type ApiCtx struct { diff --git a/api/auth.go b/api/auth.go index af2af9f..356220a 100644 --- a/api/auth.go +++ b/api/auth.go @@ -6,10 +6,10 @@ import ( "os" "strings" - "github.com/juruen/rmapi/config" - "github.com/juruen/rmapi/log" - "github.com/juruen/rmapi/model" - "github.com/juruen/rmapi/transport" + "github.com/peerdavid/rmapi/config" + "github.com/peerdavid/rmapi/log" + "github.com/peerdavid/rmapi/model" + "github.com/peerdavid/rmapi/transport" uuid "github.com/satori/go.uuid" ) diff --git a/config/config.go b/config/config.go index 2deb2c3..3adc495 100644 --- a/config/config.go +++ b/config/config.go @@ -6,8 +6,8 @@ import ( "os" "os/user" - "github.com/juruen/rmapi/log" - "github.com/juruen/rmapi/model" + "github.com/peerdavid/rmapi/log" + "github.com/peerdavid/rmapi/model" "gopkg.in/yaml.v2" ) diff --git a/config/config_test.go b/config/config_test.go index 2195584..d2a3954 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -6,7 +6,7 @@ import ( "os" "testing" - "github.com/juruen/rmapi/model" + "github.com/peerdavid/rmapi/model" "github.com/stretchr/testify/assert" ) diff --git a/docs/tutorial-print-macosx.md b/docs/tutorial-print-macosx.md index 4a4ecb9..b452bee 100644 --- a/docs/tutorial-print-macosx.md +++ b/docs/tutorial-print-macosx.md @@ -1,6 +1,6 @@ # How to directly print to your ReMarkable on Mac -This tuorial wil show you how to leverage [rmapi](https://github.com/juruen/rmapi) and `Automator` to print +This tuorial wil show you how to leverage [rmapi](https://github.com/peerdavid/rmapi) and `Automator` to print to your ReMarkable tablet from your Mac using the Cloud API. This way you won't need to take the extra step of using the desktop app. @@ -19,7 +19,7 @@ Use `terminal` or `iterm` to get a terminal to run commands from it. Download `rmapi` with the following command: ```bash -curl -L https://github.com/juruen/rmapi/releases/download/v0.0.4/rmapi-macosx-v0.0.4.zip -o rmapi.zip +curl -L https://github.com/peerdavid/rmapi/releases/download/v0.0.4/rmapi-macosx-v0.0.4.zip -o rmapi.zip ``` Alternatively, you can build it from sources. diff --git a/filetree/filetree.go b/filetree/filetree.go index 36e72e3..f8a154c 100644 --- a/filetree/filetree.go +++ b/filetree/filetree.go @@ -3,8 +3,8 @@ package filetree import ( "errors" - "github.com/juruen/rmapi/model" - "github.com/juruen/rmapi/util" + "github.com/peerdavid/rmapi/model" + "github.com/peerdavid/rmapi/util" ) type FileTreeCtx struct { diff --git a/filetree/filetree_test.go b/filetree/filetree_test.go index eb13f39..75635cb 100644 --- a/filetree/filetree_test.go +++ b/filetree/filetree_test.go @@ -3,7 +3,7 @@ package filetree import ( "testing" - "github.com/juruen/rmapi/model" + "github.com/peerdavid/rmapi/model" "github.com/stretchr/testify/assert" ) diff --git a/filetree/treeutil.go b/filetree/treeutil.go index 15bf663..c07418f 100644 --- a/filetree/treeutil.go +++ b/filetree/treeutil.go @@ -3,7 +3,7 @@ package filetree import ( "strings" - "github.com/juruen/rmapi/model" + "github.com/peerdavid/rmapi/model" ) const ( diff --git a/fusefs/file.go b/fusefs/file.go index fe3e03c..172808d 100644 --- a/fusefs/file.go +++ b/fusefs/file.go @@ -6,7 +6,7 @@ import ( "github.com/hanwen/go-fuse/fuse" "github.com/hanwen/go-fuse/fuse/nodefs" - "github.com/juruen/rmapi/log" + "github.com/peerdavid/rmapi/log" ) type fuseFile struct { diff --git a/fusefs/node.go b/fusefs/node.go index e77b275..6c249e4 100644 --- a/fusefs/node.go +++ b/fusefs/node.go @@ -5,13 +5,13 @@ import ( "os" "time" - "github.com/juruen/rmapi/config" - "github.com/juruen/rmapi/log" - "github.com/juruen/rmapi/model" + "github.com/peerdavid/rmapi/config" + "github.com/peerdavid/rmapi/log" + "github.com/peerdavid/rmapi/model" "github.com/hanwen/go-fuse/fuse" "github.com/hanwen/go-fuse/fuse/nodefs" - "github.com/juruen/rmapi/api" + "github.com/peerdavid/rmapi/api" ) type fuseFs struct { diff --git a/main.go b/main.go index 1c4b187..3d809b3 100644 --- a/main.go +++ b/main.go @@ -5,10 +5,10 @@ import ( "github.com/hanwen/go-fuse/fuse" "github.com/hanwen/go-fuse/fuse/nodefs" - "github.com/juruen/rmapi/api" - "github.com/juruen/rmapi/fusefs" - "github.com/juruen/rmapi/log" - "github.com/juruen/rmapi/shell" + "github.com/peerdavid/rmapi/api" + "github.com/peerdavid/rmapi/fusefs" + "github.com/peerdavid/rmapi/log" + "github.com/peerdavid/rmapi/shell" ) const AUTH_RETRIES = 3 diff --git a/shell/fs_completer.go b/shell/fs_completer.go index 0e396ed..559fe6b 100644 --- a/shell/fs_completer.go +++ b/shell/fs_completer.go @@ -7,7 +7,7 @@ import ( "path" "strings" - "github.com/juruen/rmapi/log" + "github.com/peerdavid/rmapi/log" ) func prefixToDir(s []string) string { diff --git a/shell/geta.go b/shell/geta.go new file mode 100644 index 0000000..7a3c510 --- /dev/null +++ b/shell/geta.go @@ -0,0 +1,255 @@ +package shell + +import ( + "errors" + "fmt" + "time" + "os/exec" + "strings" + "archive/zip" + "github.com/peerdavid/rmapi/log" + "os" + "io" + "github.com/abiosoft/ishell" + "path/filepath" + "github.com/peerdavid/rmapi/model" +) + + +func verifyGetaCmdTools() [] string { + var missingTools [] string + + missingTools = verifyCommand("python3", missingTools) + missingTools = verifyCommand("convert", missingTools) + missingTools = verifyCommand("rsvg-convert", missingTools) + missingTools = verifyCommand("pdftk", missingTools) + missingTools = verifyCommand("pdf270", missingTools) + + return missingTools +} + + +// See also https://siongui.github.io/2018/03/16/go-check-if-command-exists/ +func verifyCommand(name string, missingTools [] string) [] string { + cmd := exec.Command("/bin/sh", "-c", "command -v "+name) + if err := cmd.Run(); err != nil { + return append(missingTools, name) + } + return missingTools +} + + +func getaCmd(ctx *ShellCtxt) *ishell.Cmd { + return &ishell.Cmd{ + Name: "geta", + Help: "copy remote file to local with annotations.", + Completer: createEntryCompleter(ctx), + Func: func(c *ishell.Context) { + // Parse cmd args + if len(c.Args) == 0 { + c.Err(errors.New("missing source file")) + return + } + srcName := c.Args[0] + + // Download document as zip + node, err := ctx.api.Filetree.NodeByPath(srcName, ctx.node) + if err != nil || node.IsDirectory() { + c.Err(errors.New("file doesn't exist")) + return + } + + err = getAnnotatedDocument(ctx, node, "") + if err != nil { + c.Err(err) + return + } + + fmt.Println("OK") + }, + } +} + + +func getAnnotatedDocument(ctx *ShellCtxt, node *model.Node, path string) error { + zipFile := fmt.Sprintf(".%s.zip", node.Name()) + + // Set output name and output file name + output := node.Name() + if(path != ""){ + output = fmt.Sprintf("%s/%s", path, node.Name()) + } + outputFileName := fmt.Sprintf("%s.pdf", output) + + // Parse last modified time of document on api + modifiedClientTime, err := time.Parse(time.RFC3339Nano, node.Document.ModifiedClient) + if err != nil { + // If we could not parse the time correctly, we still continue + // with the execution such that the pdf is downloaded... + log.Warning.Println("Could not parse modified time. Overwrite existing file.") + modifiedClientTime = time.Now().Local() + } + + // If document has not changed since last update skip pdf convertion + outputFile, err := os.Stat(outputFileName) + if !os.IsNotExist(err) { + outputFileModTime := outputFile.ModTime() + if(outputFileModTime.Equal(modifiedClientTime)){ + log.Trace.Println("Nothing changed since last download. Skip. ") + cleanup("", zipFile); + return nil + } + } + + // Download document if content has changed + err = ctx.api.FetchDocument(node.Document.ID, zipFile) + if err != nil { + return errors.New(fmt.Sprintf("Failed to download file %s with %s", node.Name(), err.Error())) + } + + // Unzip document + tmpFolder := fmt.Sprintf(".%s", node.Document.ID) + _, err = unzip(zipFile, tmpFolder) + if err != nil { + cleanup(tmpFolder, zipFile); + return err + } + + // Currently we don't support the annotation of epub files + if(isEpub(tmpFolder, node)){ + cleanup(tmpFolder, zipFile); + return errors.New("Could not annotate epub files."); + } + + // If pdf is annotated convert it, otherwise move the pdf file + if(documentIsAnnotated(tmpFolder, node)){ + exportPdf := os.Getenv("GOPATH") + "/src/github.com/peerdavid/rmapi/tools/exportAnnotatedPdf" + rM2svg := os.Getenv("GOPATH") + "/src/github.com/peerdavid/rmapi/tools/rM2svg" + output, err := exec.Command( + "/bin/bash", + exportPdf, + tmpFolder, + node.Document.ID, + output, + rM2svg).CombinedOutput() + + log.Trace.Println(fmt.Sprintf("%s", output)) + + if err != nil { + cleanup(tmpFolder, zipFile); + return err + } + + } else { + log.Trace.Println("Document is not annotated. Move original file.") + + // Two cases exist: Pdf file or a notebook. The second case + // Could not occur because a notebook has always at least one page. + // Therefore we know that its a pdf file. + plainPdfFile := fmt.Sprintf("%s/%s.pdf", tmpFolder, node.Document.ID) + err := os.Rename(plainPdfFile, outputFileName) + if err != nil { + log.Error.Println("Could not move pdf file.") + cleanup(tmpFolder, zipFile); + return err + } + } + + // Set creation time + err = os.Chtimes(outputFileName, modifiedClientTime, modifiedClientTime) + if err != nil { + fmt.Println("(Warning) Could not set modified time of pdf file.") + } + + // Cleanup + cleanup(tmpFolder, zipFile); + return nil +} + + +func cleanup(tmpFolder string, zipFile string) { + _, err := os.Stat(tmpFolder); + if(!os.IsNotExist(err)){ + os.RemoveAll(tmpFolder) + } + + _, err = os.Stat(zipFile); + if(!os.IsNotExist(err)){ + os.Remove(zipFile) + } +} + + +// From https://golangcode.com/unzip-files-in-go/ +func unzip(src string, dest string) ([]string, error) { + + var filenames []string + + r, err := zip.OpenReader(src) + if err != nil { + return filenames, err + } + defer r.Close() + + for _, f := range r.File { + + rc, err := f.Open() + if err != nil { + return filenames, err + } + defer rc.Close() + + // Store filename/path for returning and using later on + fpath := filepath.Join(dest, f.Name) + + // Check for ZipSlip. More Info: http://bit.ly/2MsjAWE + if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { + return filenames, fmt.Errorf("%s: illegal file path", fpath) + } + + filenames = append(filenames, fpath) + + if f.FileInfo().IsDir() { + + // Make Folder + os.MkdirAll(fpath, os.ModePerm) + + } else { + + // Make File + if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + return filenames, err + } + + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return filenames, err + } + + _, err = io.Copy(outFile, rc) + + // Close the file without defer to close before next iteration of loop + outFile.Close() + + if err != nil { + return filenames, err + } + + } + } + return filenames, nil +} + + +func documentIsAnnotated(tmpFolder string, node *model.Node) bool { + annotationFolder := fmt.Sprintf("%s/%s", tmpFolder, node.Document.ID) + _, err := os.Stat(annotationFolder); + return !os.IsNotExist(err) +} + + +func isEpub(tmpFolder string, node *model.Node) bool { + annotationFolder := fmt.Sprintf("%s/%s.epub", tmpFolder, node.Document.ID) + _, err := os.Stat(annotationFolder); + return !os.IsNotExist(err) +} \ No newline at end of file diff --git a/shell/mget.go b/shell/mget.go index 2308efa..6589868 100644 --- a/shell/mget.go +++ b/shell/mget.go @@ -7,8 +7,8 @@ import ( "path" "github.com/abiosoft/ishell" - "github.com/juruen/rmapi/filetree" - "github.com/juruen/rmapi/model" + "github.com/peerdavid/rmapi/filetree" + "github.com/peerdavid/rmapi/model" ) func mgetCmd(ctx *ShellCtxt) *ishell.Cmd { diff --git a/shell/mgeta.go b/shell/mgeta.go new file mode 100644 index 0000000..e569bf0 --- /dev/null +++ b/shell/mgeta.go @@ -0,0 +1,67 @@ +package shell + +import ( + "errors" + "fmt" + "os" + "path" + + "github.com/abiosoft/ishell" + "github.com/peerdavid/rmapi/filetree" + "github.com/peerdavid/rmapi/model" +) + +func mgetaCmd(ctx *ShellCtxt) *ishell.Cmd { + return &ishell.Cmd{ + Name: "mgeta", + Help: "recursively copy remote directory to local with annotations", + Completer: createDirCompleter(ctx), + Func: func(c *ishell.Context) { + if len(c.Args) == 0 { + c.Err(errors.New(("missing source dir"))) + return + } + + srcName := c.Args[0] + + node, err := ctx.api.Filetree.NodeByPath(srcName, ctx.node) + + if err != nil || node.IsFile() { + c.Err(errors.New("directory doesn't exist")) + return + } + + visitor := filetree.FileTreeVistor{ + func(currentNode *model.Node, currentPath []string) bool { + idxDir := 0 + if srcName == "." && len(currentPath) > 0 { + idxDir = 1 + } + + dst := "./" + filetree.BuildPath(currentPath[idxDir:], currentNode.Name()) + dir := path.Dir(dst) + os.MkdirAll(dir, 0766) + + if currentNode.IsDirectory() { + return filetree.ContinueVisiting + } + + c.Printf("downloading [%s]...", dst) + + err = getAnnotatedDocument(ctx, currentNode, fmt.Sprintf("%s", dir)) + + if err == nil { + c.Println(" OK") + return filetree.ContinueVisiting + } + + c.Err(errors.New(fmt.Sprintf("Failed to downlaod file %s", currentNode.Name()))) + + return filetree.ContinueVisiting + }, + } + + filetree.WalkTree(node, visitor) + }, + } +} diff --git a/shell/mput.go b/shell/mput.go index b5097b5..84f5a97 100644 --- a/shell/mput.go +++ b/shell/mput.go @@ -8,7 +8,7 @@ import ( "strings" "github.com/abiosoft/ishell" - "github.com/juruen/rmapi/util" + "github.com/peerdavid/rmapi/util" ) func mputCmd(ctx *ShellCtxt) *ishell.Cmd { diff --git a/shell/put.go b/shell/put.go index a5b0423..3342b88 100644 --- a/shell/put.go +++ b/shell/put.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/abiosoft/ishell" - "github.com/juruen/rmapi/util" + "github.com/peerdavid/rmapi/util" ) func putCmd(ctx *ShellCtxt) *ishell.Cmd { diff --git a/shell/rmfs_completer.go b/shell/rmfs_completer.go index f472647..8ad9333 100644 --- a/shell/rmfs_completer.go +++ b/shell/rmfs_completer.go @@ -5,8 +5,8 @@ import ( "path" "strings" - "github.com/juruen/rmapi/log" - "github.com/juruen/rmapi/model" + "github.com/peerdavid/rmapi/log" + "github.com/peerdavid/rmapi/model" ) func prefixToNodeDir(ctx *ShellCtxt, s []string) (*model.Node, string) { diff --git a/shell/shell.go b/shell/shell.go index df97b21..668ba80 100644 --- a/shell/shell.go +++ b/shell/shell.go @@ -3,10 +3,12 @@ package shell import ( "fmt" "os" + "strings" "github.com/abiosoft/ishell" - "github.com/juruen/rmapi/api" - "github.com/juruen/rmapi/model" + "github.com/peerdavid/rmapi/api" + "github.com/peerdavid/rmapi/model" + "github.com/peerdavid/rmapi/log" ) type ShellCtxt struct { @@ -49,6 +51,18 @@ func RunShell(apiCtx *api.ApiCtx) error { shell.AddCmd(versionCmd(ctx)) shell.AddCmd(statCmd(ctx)) + // Add geta and mgeta if all necessary tools are installed + missingTools := verifyGetaCmdTools() + hasAllToolsForGetaInstalled := len(missingTools) <= 0 + if(hasAllToolsForGetaInstalled){ + shell.AddCmd(getaCmd(ctx)) + shell.AddCmd(mgetaCmd(ctx)) + } else { + log.Warning.Println(fmt.Sprintf("Commands geta and mgeta are disabled" + + " because the following tools are not installed: \n %v", + strings.Join(missingTools, "\n "))) + } + setCustomCompleter(shell) if len(os.Args) > 1 { diff --git a/shell/version.go b/shell/version.go index 8476de5..7355de1 100644 --- a/shell/version.go +++ b/shell/version.go @@ -2,7 +2,7 @@ package shell import ( "github.com/abiosoft/ishell" - "github.com/juruen/rmapi/version" + "github.com/peerdavid/rmapi/version" ) func versionCmd(ctx *ShellCtxt) *ishell.Cmd { diff --git a/tools/exportAnnotatedPdf b/tools/exportAnnotatedPdf new file mode 100755 index 0000000..2f3fe71 --- /dev/null +++ b/tools/exportAnnotatedPdf @@ -0,0 +1,104 @@ +#!/bin/bash +# +# Script to annotate remarkable notebooks. Based on the work of: +# Copyright © 2018, Eric S Fraga, e.fraga@ucl.ac.uk +# $Id: rmannotated.sh,v 1.12 2018/10/07 15:19:09 ucecesf Exp $ +# +# Changed by peer david for the usage of rmapi +# + + +# Parse input and set defaults +base=$1 +id=$2 +output=$3 +rM2svg=$4 +templatesdir="${HOME}/Templates/Remarkable" + +# Open folder that contains the .rm files +cd ${base} + +# Write annotated pdf +doc="${id}.pdf" +if [ -f ${doc} ] +then + # Get width and height of pdf + width=$(pdfinfo ${doc} | grep 'Page size:' | awk '{print $3}') + height=$(pdfinfo ${doc} | grep 'Page size:' | awk '{print $5}') + + # comparing floating point numbers in bash directly is not possible + if (( $(echo "$height >= $width" |bc -l) )); then + rotated="no" + else + rotated="yes" + swap=$width + width=$height + height=$swap + fi + + usetemplates="no" +else + rotate="no" + width=1404 + height=1872 + + if [ -d "${templatesdir}" ]; + then + usetemplates="yes" + else + usetemplates="no" + fi +fi + +# Create svg files +$rM2svg -c -i ${id} --width ${width} --height ${height} -o "" + +# Read possible templates +if [ "$usetemplates" == "yes" ]; +then + readarray -t templates < "$id.pagedata" +fi + +# Create a pdf for every svg file +page=0 +for svg in *.svg +do + p=$(basename $svg .svg) + + if [ "$usetemplates" == "yes" ] && [ "${page}" -lt "${#templates[@]}" ]; + then + # Convert template to pdf + if [ ! -f "${templatesdir}/${templates[$page]}.pdf" ] + then + convert "${templatesdir}/${templates[$page]}.png" "${templatesdir}/${templates[$page]}.pdf" + fi + + # Convert notes to pdf + rsvg-convert -f pdf -o "${id}_$p_tmp.pdf" "$p.svg" + #inkscape "$p.svg" --without-gui --export-pdf="${id}_$p_tmp.pdf" + + # Merge template and notes + pdftk "${id}_$p_tmp.pdf" background "${templatesdir}/${templates[$page]}.pdf" output "${id}_$p.pdf" + rm "${id}_$p_tmp.pdf" + else + rsvg-convert -f pdf -o ${id}_$p.pdf $p.svg + #inkscape "$p.svg" --without-gui --export-pdf="${id}_$p.pdf" + fi + + if [ "$rotated" == "yes" ] + then + pdf270 ${id}_$p.pdf 2>/dev/null + mv ${id}_$p-rotated270.pdf ${id}_$p.pdf + fi + page=$((page+1)) +done + +pdftk ${id}_*.pdf cat output ${id}_annotations.pdf + +if [ -f ${doc} ]; then + pdftk ${doc} multistamp ${id}_annotations.pdf output ../"${output}.pdf" +else + mv ${id}_annotations.pdf ../"${output}.pdf" +fi + +rm ${id}_*.pdf \ No newline at end of file diff --git a/tools/rM2svg b/tools/rM2svg new file mode 100755 index 0000000..f70b019 --- /dev/null +++ b/tools/rM2svg @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +# +# Script for converting a reMarkable tablet lines file to an SVG +# image. Originally from +# +# https://github.com/lschwetlick/maxio/tree/master/tools +# +# but hacked to allow for specification of desired size of resulting +# SVG image in terms of width and height. Log of changes at the end +# of the file. +# +# Changed by Eric S Fraga; see log of changes at end. +# Changed by Peer David; see log of changes at end. +import sys +import struct +import os.path +import argparse + + +__prog_name__ = "rM2svg" +__version__ = "0.0.1beta $Revision: 1.7 $" + + +# Size +default_x_width = 1404 +default_y_width = 1872 + +# Mappings +stroke_colour={ + 0 : "black", + 1 : "grey", + 2 : "white", +} +'''stroke_width={ + 0x3ff00000 : 2, + 0x40000000 : 4, + 0x40080000 : 8, +}''' + + +def main(): + parser = argparse.ArgumentParser(prog=__prog_name__) + parser.add_argument('--height', + help='Desired height of image', + type=float, + default=default_y_width) + parser.add_argument('--width', + help='Desired width of image', + type=float, + default=default_x_width) + parser.add_argument("-i", + "--input", + help=".lines input file", + required=True, + metavar="FILENAME", + #type=argparse.FileType('r') + ) + parser.add_argument("-o", + "--output", + help="prefix for output files", + required=True, + metavar="NAME", + #type=argparse.FileType('w') + ) + parser.add_argument("-c", + "--coloured_annotations", + help="Colour annotations for document markup.", + action='store_true', + ) + parser.add_argument('--version', + action='version', + version='%(prog)s {version}'.format(version=__version__)) + args = parser.parse_args() + + if not os.path.exists(args.input): + parser.error('The file "{}" does not exist!'.format(args.input)) + + if args.coloured_annotations: + global stroke_colour + stroke_colour = { + 0: "darkblue", + 1: "gray", + 2: "white", + 3: "yellow" + } + + lines2svg(args.input, args.output, args.coloured_annotations, + args.width, args.height) + + +def abort(msg): + print(msg, file=sys.stderr) + sys.exit(1) + + +def lines2svg(path, output_name, coloured_annotations=False, + x_width=default_x_width, y_width=default_y_width): + + if output_name.endswith(".svg"): + output_name = output_name[:-4] + + used_pages = [] + # Iterate through pages (There is at least one) + for f in os.listdir(path): + if(not f.endswith(".rm")): + continue + + input_file = "%s/%s" % (path, f) + page = int(f[:-3]) + used_pages.append(page) + + with open(input_file, 'rb') as f: + data = f.read() + offset = 0 + + # Is this a reMarkable .lines file? + expected_header=b'reMarkable .lines file, version=3 ' + if len(data) < len(expected_header) + 4: + abort('File too short to be a valid file') + + fmt = '<{}sI'.format(len(expected_header)) + header, nlayers = struct.unpack_from(fmt, data, offset); offset += struct.calcsize(fmt) + if header != expected_header or nlayers < 1: + abort('Not a valid reMarkable file: '.format(header, nlayers)) + + output = open("{}{:05}.svg".format(output_name, page+1), 'w') + output.write('\n'.format(y_width, x_width)) # BEGIN page + + # Iterate through layers on the page (There is at least one) + for layer in range(nlayers): + fmt = '\n') # END stroke + + output.write('') # END page + output.close() + + + # For every intermediate page that is empty create a blank page + if len(used_pages) == 0: + return + + for page in range(max(used_pages)+2): # Note: last page is iterated by pdftk, so also add blank there + if page in used_pages: + continue + + output = open("{}{:05}.svg".format(output_name, page+1), 'w') + output.write('\n'.format(y_width, x_width)) # BEGIN page + output.write('') # END page + output.close() + + +if __name__ == "__main__": + main() + +# +# revision 1.8 +# Changed for usage of rmapi +# +# $Log: rM2svg,v $ +# Revision 1.7 2018/09/26 11:08:52 ucecesf +# Summary: allow for floating point numbers for geometry +# +# revision 1.6 +# date: 2018/09/24 06:18:19; author: ucecesf; state: Exp; lines: +14 -9 +# Summary: adjust for different page width/height ratios +# +# revision 1.5 +# date: 2018/09/23 18:21:43; author: ucecesf; state: Exp; lines: +15 -5 +# Summary: added height and width arguments +# +# revision 1.4 +# date: 2018/09/23 18:08:05; author: ucecesf; state: Exp; lines: +1 -1 +# Summary: added revision info +# +# revision 1.3 +# date: 2018/09/20 17:39:21; author: ucecesf; state: Exp; lines: +2 -0 +# Summary: adjusted pen stroke width for new transformation +# +# revision 1.2 +# date: 2018/09/20 17:24:22; author: ucecesf; state: Exp; lines: +6 -4 +# Summary: transformation aiming at standard A4 LaTeX PDF document +# +# Line widths etc. still need adjusting. +# +# revision 1.1 +# date: 2018/09/14 08:56:07; author: ucecesf; state: Exp; +# Initial revision \ No newline at end of file diff --git a/transport/transport.go b/transport/transport.go index 243ec4d..b5a0d29 100644 --- a/transport/transport.go +++ b/transport/transport.go @@ -11,9 +11,9 @@ import ( "strings" "time" - "github.com/juruen/rmapi/log" - "github.com/juruen/rmapi/model" - "github.com/juruen/rmapi/util" + "github.com/peerdavid/rmapi/log" + "github.com/peerdavid/rmapi/model" + "github.com/peerdavid/rmapi/util" ) type AuthType int diff --git a/util/util.go b/util/util.go index decfa80..c3a706f 100644 --- a/util/util.go +++ b/util/util.go @@ -8,7 +8,7 @@ import ( "path" "strings" - "github.com/juruen/rmapi/model" + "github.com/peerdavid/rmapi/model" ) func DocPathToName(p string) string { diff --git a/util/zipdoc.go b/util/zipdoc.go index 657b28f..7c9fa84 100644 --- a/util/zipdoc.go +++ b/util/zipdoc.go @@ -7,12 +7,13 @@ import ( "io/ioutil" "strings" - "github.com/juruen/rmapi/log" + "github.com/peerdavid/rmapi/log" ) type zipDocumentContent struct { ExtraMetadata map[string]string `json:"extraMetadata"` FileType string `json:"fileType"` + PageCount int `json:"pageCount"` LastOpenedPage int `json:"lastOpenedPage"` LineHeight int `json:"lineHeight"` Margins int `json:"margins"` @@ -110,6 +111,7 @@ func createZipContent(ext string) (string, error) { make(map[string]string), ext, 0, + 0, -1, 180, 1, diff --git a/vendor/vendor.json b/vendor/vendor.json index 2b98fa9..213d6ab 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -98,5 +98,5 @@ "revisionTime": "2018-01-09T11:43:31Z" } ], - "rootPath": "github.com/juruen/rmapi" + "rootPath": "github.com/peerdavid/rmapi" }