MVPen (Pegasus Mobile NoteTaker) の解析 (9)
MVPen をLinux から使う試みシリーズ。モバイルノートデータの構造について。
※注意※ MVPen が故障する可能性があります
モバイルノートデータのサンプル
実際のモバイルノートデータ(を前回のプログラムで取り出したもの)の抜粋をダンプしてみました。
00000000 8e 05 00 3f 01 01 ff ff ff ff ff ff ff ff d8 fc |...?............| 00000010 d5 12 d5 fc d5 12 cf fc d2 12 ca fc cc 12 c7 fc |................| 00000020 c1 12 c0 fc b4 12 b5 fc a5 12 ab fc 96 12 a5 fc |................| 00000030 85 12 a0 fc 6d 12 a0 fc 54 12 a3 fc 37 12 ab fc |....m...T...7...| ...... snip snip snip ...... 000002c0 5f 19 8b 00 5c 19 8a 00 57 19 8d 00 54 19 94 00 |_...\...W...T...| 000002d0 56 19 9a 00 5c 19 9a 00 59 19 00 00 00 80 ce 04 |V...\...Y.......| 000002e0 db 16 cb 04 db 16 c3 04 db 16 c2 04 db 16 c3 04 |................| ...... snip snip snip ...... 00000580 92 17 ae 0a 92 17 b6 0a 8f 17 00 00 00 80 a4 05 |................| 00000590 00 1f 02 02 ff ff ff ff ff ff ff ff c2 fb f2 28 |...............(| 000005a0 00 00 00 80 00 00 00 00 00 00 00 00 00 00 00 00 |................| ...... snip snip snip ......
カンのいい方なら,これをみただけでデータ構造を類推できると思います。
モバイルノートデータの構造
では構造について説明します。
フラグ(2 バイト)
たいてい 0x00 0x3f
です。空ノートの場合,0x00 0x1f
というものもありました。いずれにしても意味は不明です。
ノートインデックス(1 バイト×2)
ノートのインデックスが 1 バイト×2(同じもの)で表象されます。たとえば 1 番目のノートデータの場合,0x01 0x01
となります。1 オリジンです。
ノート数が 255 を超えた場合どのようになるのか不思議ですが,液晶インジケータも2桁しかありませんし,どこかで「メモリオーバー」になるのでしょう。
データ開始シグニチャ(8 バイト)
0xff
× 8 バイトになります。
筆跡座標データ(2 バイト×2)
符号つき16ビット整数が 2 つになります。先頭から X 座標,Y 座標です。前回ポジションからの相対値ではなく,絶対値です。バイトオーダーはリトルエンディアン(86 系)です。
たとえば 0xd8 0xfc 0xd5 0x12
を解釈すると (-808, 4812) となります。このように座標系は (0, 0) センターになっています。
ペンアップ(2 バイト×2)
0x00 0x00 0x00 0x80
という座標データになります。翻訳?すると (0, -32768) になります。もし本当にこのようなデータがあったらどうするのでしょう,と思いましたが,かなり端っこのデータなので存在しえないのでしょう。
モバイルノートデータをパースするプログラム in Perl
以上の仕様をもとに,バイナリのモバイルノートデータをパースするスクリプトを Perl で書いてみました。
だらだらと書いたので Perl のプログラムとしてはアレですけど,サンプルということでご容赦を。
#!/usr/bin/perl use strict; use warnings; use IO::Handle; use IO::File; use Getopt::Long; use Pod::Usage; our $SCALE = 8; our $WIDTH = 3; our $COLOR = 'blue'; our $HELP; GetOptions( 'scale|s=f' => \$SCALE, 'width|w=o' => \$WIDTH, 'color|c=s' => \$COLOR, 'help|h|?' => \$HELP, ) or pod2usage(2); pod2usage(1) if $HELP; pod2usage("$0: No files given.") if @ARGV == 0 && -t \*STDIN; my $h; if (@ARGV) { $h = IO::File->new(shift @ARGV, 'r'); die $! if ! $h; } else { $h = IO::Handle->new; die $! if ! $h; $h->fdopen(fileno(\*STDIN), 'r') or die $!; } # cannot use objective interface for IO::Handle binmode $h, ':raw' or die $!; print <<'END_HEADER'; <?xml version="1.0"?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg"> <head> </head> <body> END_HEADER my $cur = 0; my $buf; while (1) { # position to next record $h->read($buf, 2) or die $!; $cur += 2; last if ! $buf; my $next = unpack 'v', $buf; printf {*STDERR} "current = %u / next = %u\n", $cur, $next; last if $next == 0; # flag? $h->read($buf, 2) or die $!; $cur += 2; last if ! $buf; my $sign = unpack 'v', $buf; die if $sign & 0x1f00 != 0x1f00; # index of notes $h->read($buf, 2) or die $!; $cur += 2; last if ! $buf; my ($idx1, $idx2) = unpack 'CC', $buf; die if $idx1 != $idx2; # signature $h->read($buf, 8) or die $!; $cur += 8; last if ! $buf; die if $buf ne "\x{ff}" x 8; my @stroke; my @strokes; my ($min_x, $max_x, $min_y, $max_y) = ( 99999, -99999, 99999, -99999 ); my $first = 1; while ($cur < $next) { $h->read($buf, 2) or die $!; $cur += 2; last if ! $buf; my $x = unpack 'v', $buf; $x -= 0x10000 if $x >= 0x8000; $h->read($buf, 2) or die $!; $cur += 2; last if ! $buf; my $y = unpack 'v', $buf; $y -= 0x10000 if $y >= 0x8000; if ($x == 0 && $y == -0x8000) { $first = 1; next; } if ($first) { if (@stroke) { push @strokes, [ @stroke ]; @stroke = (); } } push @stroke, [ $x, $y ]; $min_x = $x if $x < $min_x; $max_x = $x if $x > $max_x; $min_y = $y if $y < $min_y; $max_y = $y if $y > $max_y; $first = 0; } if (@stroke) { push @strokes, [ @stroke ]; } next if ! @strokes; next if @strokes == 1 && @{$strokes[0]} == 1; $min_x -= 100; $max_x += 100; $min_y -= 100; $max_y += 100; my $width = $max_x - $min_x; my $height = $max_y - $min_y; $width /= $SCALE; $height /= $SCALE; printf qq{<svg:svg width="%u" height="%u">\n}, $width, $height; foreach my $stroke (@strokes) { my $pos = shift @$stroke; next if ! defined $pos; my ($x, $y) = @$pos; print qq{<svg:path d="}; printf "M %d %d ", ($x - $min_x) / $SCALE, ($y - $min_y) / $SCALE; foreach $pos (@$stroke) { ($x, $y) = @$pos; printf "L %d %d ", ($x - $min_x) / $SCALE, ($y - $min_y) / $SCALE; } printf qq{" fill="none" stroke="%s" stroke-width="%u"/>\n}, $COLOR, $WIDTH; } print qq{</svg:svg>\n}; # skip to next position if ($h->can('seek')) { $h->seek($next - $cur, SEEK_CUR) or die $!; } else { if ($next - $cur > 0) { $h->read($buf, $next - $cur) or die $!; } } $cur = $next; } print qq{</body>\n}; print qq{</html>\n}; __END__ =head1 NAME parse_mvpen.pl - Parse MVPen notes data =head1 SYNOPSIS parse_mvpen.pl [options] [file] Options: -scale <scale factor> scale factor of SVG document (default: 8) -width <pen width> pen width (default: 3) -color <pen color> pen color (default: blue) -help print this help message =head1 OPTIONS =over 4 =item B<-scale> =item B<-width> =item B<-color> =item B<-help> =back =head1 DESCRIPTION blah, blah, blah =cut
SVG フォーマットで標準出力に吐き出します。
SVG フォーマットの詳細については詳しく説明しませんが,今回使った機能に限定していうと
<svg width="317" height="219"> <path d="M 13 65 L 12 65 L 11 65 ..." fill="none" stroke="blue" stroke-width="3"/> <path d="M 90 210 L 89 210 L 88 210 ..."/> ...... </svg>
のような構造をしています。<path>
の d
属性の中身,M
が MoveTo, L
が LineTo です。
今回のプログラムでは XHTML として xmlns
を下記のように設定しています。
<?xml version="1.0"?> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:svg="http://www.w3.org/2000/svg"> <body> <svg:svg width="317" height="219"> <svg:path d="M 13 65 L 12 65 ..."/> <svg:path d="M 90 210 L 89 210 L 88 210 ..."/> ...... </svg:svg> </body> </html>
SVG のタグについては svg
というプリフィックスをつけるようにして,[]<svg:svg>[]
,[]<svg:path>[]
を使っています。このように作成された XHTML ドキュメントは Firefox ならプラグインなしで読み込めました。
ベクトルデータなので平滑化などいろいろできると思うのですが,今回のプログラムではやっていません。