Skip to content
This repository was archived by the owner on Jul 2, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

* Ignore hidden files/directories by default

* Initial support for annotations

## rmapi 0.0.4 (October 1, 2018)

* Windows fixes
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ You can download an already built version for either Linux or OSX from [releases
- [x] upload a specific file
- [ ] live syncs

# Annotations

- Initial support to generate a PDF with annotations from `Quick Sheet` and notebooks.

# Shell ergonomics

- [x] autocomplete
Expand Down Expand Up @@ -102,17 +106,25 @@ E.g: download all the files
mget .
```

## Download a file and generate a PDF with its annoations

Use `geta` to download either a `Quick Sheets` or `notebook` file and generate a PDF document
with its annotations.

Please note that its support is very basic for now and only supports one type of pen for now, but
there's work in progress to improve it.

## Create a directoy

Use `mkdir path_to_new_dir` to create a new directory

## Remove a directory or a file
## Remove a directory or a file

Use `rm directory_or_file` to remove. If it's directory, it needs to be empty in order to be deleted.

You can remove multiple entries at the same time.

## Move/rename a directory or a file
## Move/rename a directory or a file

Use `mv source destination` to move or rename a file or directory.

Expand Down
138 changes: 138 additions & 0 deletions annotations/pdf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package annotations

import (
"github.com/jung-kurt/gofpdf"
"io/ioutil"
"os"
"path"
"sort"
"strconv"
"strings"
)

const (
RmX = 1404
RmY = 1872
A4X = 210
A4Y = 297
ratioA4X = float32(A4X) / float32(RmX)
ratioA4Y = float32(A4Y) / float32(RmY)
linesExtension = ".rm"
)

type PdfGenerator struct {
inputDirPath string
outputFilePath string
}

type page struct {
layers []Layer
}

func CreatePdfGenerator(inputDirPath, outputFilePath string) PdfGenerator {
return PdfGenerator{inputDirPath: inputDirPath, outputFilePath: outputFilePath}
}

func (p PdfGenerator) Generate() error {
files, err := ioutil.ReadDir(p.inputDirPath)
if err != nil {
return err
}

pages := make([]page, 0)
for _, f := range p.rmFiles(files) {
reader, err := CreateRmReader(path.Join(p.inputDirPath, f))
if err != nil {
return err
}

layers, err := reader.Parse()
if err != nil {
return err
}

pages = append(pages, page{layers})
}

if err = p.generatePages(pages); err != nil {
return err
}

return nil
}

func (p PdfGenerator) rmFiles(files []os.FileInfo) []string {
rmFiles := make([]string, 0)
for _, f := range files {
if f.IsDir() || !strings.HasSuffix(f.Name(), linesExtension) {
continue
}

pageNumStr := strings.TrimSuffix(f.Name(), linesExtension)
_, err := strconv.Atoi(pageNumStr)

if err != nil {
continue
}

rmFiles = append(rmFiles, f.Name())
}

sort.Slice(rmFiles, lineFileSorter(rmFiles))

return rmFiles
}

func lineFileSorter(rmFiles []string) func(i int, j int) bool {
return func(i int, j int) bool {
a, _ := strconv.Atoi(strings.TrimSuffix(rmFiles[i], linesExtension))
b, _ := strconv.Atoi(strings.TrimSuffix(rmFiles[j], linesExtension))

return a < b
}
}


func (p PdfGenerator) generatePages(pages []page) error {
pdf := gofpdf.New("P", "mm", "A4", "")

for _, page := range pages {
pdf.AddPage()

for _, layer := range page.layers {
for _, stroke := range layer.Strokes {

if len(stroke.Segments) < 1 {
continue
}

for i := 1; i < len(stroke.Segments); i++ {
s := stroke.Segments[i-1].toA4()
x1 := s.X
y1 := s.Y

s = stroke.Segments[i].toA4()
x2 := s.X
y2 := s.Y

pdf.Line(float64(x1), float64(y1), float64(x2), float64(y2))
}
}
}
}

return pdf.OutputFileAndClose(p.outputFilePath)
}

func (p PdfGenerator) setStyle(pdf *gofpdf.Fpdf, stroke Stroke) {
pdf.SetDrawColor(0, 0,0)
pdf.SetLineWidth(0)
pdf.SetLineCapStyle("round")
}

func (s Segment) toA4() Segment {
s.X *= ratioA4X
s.Y *= ratioA4Y

return s
}
200 changes: 200 additions & 0 deletions annotations/rm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package annotations

import (
"encoding/binary"
"fmt"
"io"
"os"
)

const (
rmHeader = "reMarkable .lines file, version=3 "
rmHeaderLen = 43
)

type rmReader struct {
file io.Reader
path string
}

type Layer struct {
Strokes []Stroke
}

type Stroke struct {
Pen uint32
Color uint32
Unknown uint32
Width float32
Segments []Segment
}

type Segment struct {
X float32
Y float32
Pressure float32
Tilt float32
Unknown1 float32
Unknown2 float32
}

func CreateRmReader(path string) (rmReader, error) {
file, err := os.Open(path)
if err != nil {
return rmReader{}, err
}

return rmReader{file: file, path: path}, nil
}

func (rm rmReader) Parse() ([]Layer, error) {
if err := rm.checkHeader(); err != nil {
return nil, err
}

numLayers, err := rm.numLayers()

if err != nil {
return nil, err
}

layers := make([]Layer, numLayers)

for i := uint32(0); i < numLayers; i++ {
numStrokes, err := rm.numStrokes()
if err != nil {
return nil, err
}

strokes := make([]Stroke, numStrokes)
for j := uint32(0); j < numStrokes; j++ {
stroke, err := rm.parseStroke()
if err != nil {
return nil, err
}

strokes[j] = stroke
}

layers[i].Strokes = strokes
}

return layers, nil
}

func (rm rmReader) checkHeader() error {
headerLen := rmHeaderLen
buf := make([]byte, rmHeaderLen)

n, err := rm.file.Read(buf)

if err != nil {
return err
}

if n != headerLen {
return fmt.Errorf("%s is not a valid rm file, invalid header length", rm.path)
}

fileId := string(buf[:rmHeaderLen])

if fileId != rmHeader {
return fmt.Errorf("%s is not a valid rm file, wrong header %s", rm.path, fileId)
}

return nil
}

func (rm rmReader) numLayers() (uint32, error) {
var numLayers uint32

if err := binary.Read(rm.file, binary.LittleEndian, &numLayers); err != nil {
return 0, fmt.Errorf("%s is not a valid rm file, wrong number of layers", rm.path)
}

return numLayers, nil
}

func (rm rmReader) numStrokes() (uint32, error) {
var numStrokes uint32

if err := binary.Read(rm.file, binary.LittleEndian, &numStrokes); err != nil {
return 0, fmt.Errorf("%s is not a valid rm file, wrong number of strokes", rm.path)
}

return numStrokes, nil
}

func (rm rmReader) parseStroke() (Stroke, error) {
var stroke Stroke

if err := binary.Read(rm.file, binary.LittleEndian, &stroke.Pen); err != nil {
return stroke, fmt.Errorf("%s: failed to parse stroke", rm.path)
}

if err := binary.Read(rm.file, binary.LittleEndian, &stroke.Color); err != nil {
return stroke, fmt.Errorf("%s: failed to parse stroke", rm.path)
}

if err := binary.Read(rm.file, binary.LittleEndian, &stroke.Unknown); err != nil {
return stroke, fmt.Errorf("%s: failed to parse stroke", rm.path)
}

if err := binary.Read(rm.file, binary.LittleEndian, &stroke.Width); err != nil {
return stroke, fmt.Errorf("%s: failed to parse stroke", rm.path)
}

var numSegments uint32
if err := binary.Read(rm.file, binary.LittleEndian, &numSegments); err != nil {
return stroke, fmt.Errorf("%s: failed to parse stroke", rm.path)
}

if numSegments == 0 {
return stroke, nil
}

stroke.Segments = make([]Segment, numSegments)

for i := uint32(0); i < numSegments; i++ {
s, err := rm.parseSegment()

if err != nil {
return stroke, fmt.Errorf("%s: failed to parse segment", rm.path)
}

stroke.Segments[i] = s
}

return stroke, nil
}


func (rm rmReader) parseSegment() (Segment, error) {
var segment Segment

if err := binary.Read(rm.file, binary.LittleEndian, &segment.X); err != nil {
return segment, fmt.Errorf("%s: failed to parse segment", rm.path)
}

if err := binary.Read(rm.file, binary.LittleEndian, &segment.Y); err != nil {
return segment, fmt.Errorf("%s: failed to parse segment", rm.path)
}

if err := binary.Read(rm.file, binary.LittleEndian, &segment.Pressure); err != nil {
return segment, fmt.Errorf("%s: failed to parse segment", rm.path)
}

if err := binary.Read(rm.file, binary.LittleEndian, &segment.Tilt); err != nil {
return segment, fmt.Errorf("%s: failed to parse segment", rm.path)
}

if err := binary.Read(rm.file, binary.LittleEndian, &segment.Unknown1); err != nil {
return segment, fmt.Errorf("%s: failed to parse segment", rm.path)
}

if err := binary.Read(rm.file, binary.LittleEndian, &segment.Unknown2); err != nil {
return segment, fmt.Errorf("%s: failed to parse segment", rm.path)
}

return segment, nil
}
Loading