MVPen (Pegasus Mobile NoteTaker) の解析 (9)

MVPenLinux から使う試みシリーズ。モバイルノートデータの構造について。

※注意※ 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 バイト)

オフセットはモバイルノートデータ全体の先頭からになります。バイトオーダーはリトルエンディアン(86 系)です。

フラグ(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 ならプラグインなしで読み込めました。


ベクトルデータなので平滑化などいろいろできると思うのですが,今回のプログラムではやっていません。