├── README.org ├── neosmartpen.py ├── pen2pdf.py └── pen2reveal.py /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: README for python-neosmartpen 2 | #+AUTHOR: Alexei Gilchrist 3 | #+DATE: 2019-06-15 4 | 5 | Python library for reading and manipulating Neo Smartpen data 6 | 7 | * neosmartpen 8 | 9 | Requirements: just python3 10 | 11 | Usage: 12 | #+BEGIN_SRC python 13 | import neosmartpen 14 | pages = neosmartpen.parse_pages(path_to_neonotes_file) 15 | #+END_SRC 16 | 17 | - ~pages~ will be a list of page dictionaries: 18 | - Page dictionaries contain a bunch of parameters and a list of ~strokes~: 19 | |--------------+-------------------------| 20 | | Field | Description | 21 | |--------------+-------------------------| 22 | | neo | 'neo' (check) | 23 | | file_version | File version | 24 | | note_type | | 25 | | page | Page number | 26 | | width | Notebook width | 27 | | height | Notebook height | 28 | | ctime | Created timestamp (ms) | 29 | | mtime | Modified timestamp (ms) | 30 | | modified | Flag if data modified | 31 | | strokes | Stroke list | 32 | |--------------+-------------------------| 33 | - ~strokes~ are dictionaries with properties like color, and list of ~dots~: 34 | |-----------+------------------------| 35 | | Field | Description | 36 | |-----------+------------------------| 37 | | type | 0=stroke, 1=voice memo | 38 | | color | (A,R,G,B) [0-255] | 39 | | thickness | [0-2] | 40 | | time | Start timestamp (ms) | 41 | | dots | List of point data | 42 | | extra | ? | 43 | |-----------+------------------------| 44 | - ~dots~ are (x,y,pressure,dt) tuples that define the stroke. 45 | ~dt~ is the time delta between points in ms. 46 | 47 | * pen2pdf 48 | 49 | Convert neonotes file to multi-page pdf. 50 | 51 | Requirements: ~python3~ and ~reportlab~ 52 | 53 | Usage: 54 | #+BEGIN_SRC bash 55 | pen2pdf.py pen_file.zip output.pdf 56 | pen2pdf.py -h 57 | #+END_SRC 58 | 59 | * pen2reveal 60 | 61 | Convert neonotes file to ~index.html~ for use with [[https://revealjs.com][reveal.js]] 62 | 63 | Usage: 64 | #+BEGIN_SRC bash 65 | pen2reveal.py neonotes_file index.html 66 | pen2reveal -h 67 | #+END_SRC 68 | -------------------------------------------------------------------------------- /neosmartpen.py: -------------------------------------------------------------------------------- 1 | import os, sys, re, glob 2 | import struct 3 | from zipfile import ZipFile 4 | 5 | def parse_pagedata(raw): 6 | ''' 7 | Parse the binary page.data format into python structure 8 | ''' 9 | # Spec from https://github.com/NeoSmartpen/Documentations/blob/master/NeoNote_data_Eng_V1.0.pdf 10 | # Appears to be little endian 11 | # page.data: 12 | # 13 | # bytes desc 14 | # 3 'neo' (verification string) 15 | # 4 File version 16 | # 4 Note type 17 | # 4 Page Number 18 | # 4 (Float 32) notebook width 19 | # 4 (Float 32) notebook height 20 | # 8 created timestamp (millisecond) 21 | # 8 modified timestamp (millisecond) 22 | # 1 Flag if data modified 23 | # 4 number of strokes 24 | # ... stroke data ... 25 | # 4 Length of guid string 26 | # * Page guid string (for evernote?) 27 | # 28 | # stroke: 29 | # 1 Type (0=stroke, 1=voice memo) 30 | # 4 Color 32 bits RGBA 31 | # 1 Thickness (0,1,2) 32 | # 4 Number of dots 33 | # 8 Start timestamp (millisecond) 34 | # ... dots ... 35 | # 1 Length of extra data (0-255) 36 | # ... extra data ... (Pen type: 0=Neo smart pen, 1=pem from neo notes, 2=highlighter from neo notes) 37 | # 38 | # dot data 13 bytes: 39 | # 4 x (float 32) 40 | # 4 y (Float 32) 41 | # 4 pressure (Float 32) 42 | # 1 time diff from previous dot 43 | # 44 | # x and y where normalised by max(width,height) 45 | 46 | o = struct.unpack('<3s3i2f2Q?I', raw[:44]) 47 | 48 | if o[0]!=b'neo': 49 | raise Exception("Not a valid neopen data file") 50 | 51 | data = {'neo':o[0], 'file_version':o[1], 'note_type':o[2], 'page':o[3], 52 | 'width':o[4], 'height':o[5], 'ctime':o[6], 'mtime':o[7], 53 | 'modified':o[8], 'strokes':[]} 54 | 55 | nstrokes = o[9] 56 | 57 | start = 44 58 | for n in range(nstrokes): 59 | o = struct.unpack('>(8*i))&0xff for i in range(3,-1,-1)] 68 | 69 | stroke = {'type':o[0], 'color':col, 'thickness':o[2], 'time':o[4]} 70 | ndots = o[3] 71 | start2 = start+18 72 | dots = [] 73 | if ndots > 1000: 74 | # something has gone wrong 75 | raise Exception('Something has gone wrong parsing dots in stroke') 76 | for m in range(ndots): 77 | d = struct.unpack('page['width']: 41 | PAGE=(A4[0],A4[1]) 42 | else: 43 | PAGE=(A4[1],A4[0]) 44 | 45 | canvas.setPageSize(PAGE) 46 | S = PAGE[0]/page['width'] 47 | 48 | for s in strokes: 49 | dots = s['dots'] 50 | x0,y0,p0,dt0 = dots[0] 51 | 52 | for x,y,p,dt in dots[1:]: 53 | # the factor 1.5 visually matches the thickness in neonote app 54 | canvas.setLineWidth((s['thickness']+1.5)*p) 55 | col = s['color'] 56 | canvas.setStrokeColorRGB(col[1]/255,col[2]/255,col[3]/255) 57 | canvas.line(S*x0,S*y0,S*x,S*y) 58 | x0,y0,p0=x,y,p 59 | 60 | canvas.showPage() 61 | 62 | 63 | for n,data in enumerate(pages): 64 | logging.info('Generating Page: %d', n+1) 65 | strokes = [] 66 | for s in data['strokes']: 67 | dots = s['dots'] 68 | x0,y0,p0,dt0 = dots[0] 69 | if transition(x0,y0): 70 | addPage(c, data, strokes) 71 | else: 72 | strokes.append(s) 73 | addPage(c, data, strokes) 74 | 75 | c.save() 76 | 77 | -------------------------------------------------------------------------------- /pen2reveal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import neosmartpen 4 | import argparse 5 | import logging 6 | logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO) 7 | 8 | parser = argparse.ArgumentParser(description='Convert Neo SmatPen file to reveal.js slides') 9 | parser.add_argument('input', help='Input smartpen file') 10 | parser.add_argument('output', help='Output html file') 11 | parser.add_argument('-t', nargs=4, default=(0,0,10,10), metavar=('x1','y1','x2','y2'), type=int, help='Transition trigger region') 12 | 13 | # 14 | # 16 | # 17 | 18 | args = parser.parse_args() 19 | 20 | TRANSITION_REGION = args.t 21 | 22 | def transition(x,y): 23 | if TRANSITION_REGION[0] 30 | 31 | 32 | 33 | 34 | reveal.js 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 51 | 52 | 53 |
54 |
55 |
56 | %s 57 |
58 |
59 |
60 | 61 | 62 | 63 | 76 | 77 | 78 | ''' 79 | 80 | pages = neosmartpen.parse_pages(args.input) 81 | 82 | def makeFragment(page, strokes, S=10): 83 | paths = [] 84 | 85 | if len(strokes)==0: 86 | return '' 87 | 88 | # Find limits of fragment 89 | xm,ym,xM,yM = neosmartpen.bounding_box(strokes) 90 | 91 | for s in strokes: 92 | dots = s['dots'] 93 | x0,y0,p0,dt0 = dots[0] 94 | D=[] 95 | 96 | for x,y,p,dt in dots[1:]: 97 | L='\n'%((S*(x0-xm)), (S*(y0-ym)), (S*(x-xm)), (S*(y-ym)), (s['thickness']+4)*p) 98 | 99 | D.append( L ) 100 | x0,y0,p0=x,y,p 101 | 102 | hexcol = neosmartpen.col2hex(s['color']) 103 | g = '%s'%(hexcol, ' '.join(D)) 104 | paths.append(g) 105 | 106 | # style="position: fixed; left: 0px; top: 0px;" 107 | svg = '
\n\n'%(round(S*(xM-xm)), round(S*(yM-ym))) 108 | for p in paths: 109 | svg+=p 110 | svg+='
\n' 111 | 112 | return svg 113 | 114 | for n,data in enumerate(pages): 115 | logging.info('Generating Slide: %d', n+1) 116 | fragments = [] 117 | strokes = [] 118 | 119 | for s in data['strokes']: 120 | dots = s['dots'] 121 | x0,y0,p0,dt0 = dots[0] 122 | if transition(x0,y0): 123 | f = makeFragment(data, strokes) 124 | fragments.append(f) 125 | strokes = [] 126 | else: 127 | strokes.append(s) 128 | f = makeFragment(data, strokes) 129 | fragments.append(f) 130 | 131 | frags = '\n'.join(fragments) 132 | 133 | fp = open(args.output, 'w') 134 | fp.write(TEMPLATE%frags) 135 | fp.close() 136 | 137 | --------------------------------------------------------------------------------