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 ならプラグインなしで読み込めました。
ベクトルデータなので平滑化などいろいろできると思うのですが,今回のプログラムではやっていません。
