わいえむねっと

Contents
Categories
Calendar
2011/04
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Monthly Archives
~2000/01
Recent Entries
RSS1.0
Templates
Information
Processed: 0.020 sec
Chashed: -
2011/04/03 Sun
「オライリーのebookから空白を除去……」に関する補遺(2) バイナリパッチによる空白の除去 - ただのにっき(2011-03-30)
http://sho.tdiary​.net/20110330.h​tml
一方、面白い手法の紹介もあった。わいえむねっと - 2011/03/30で紹介されているのは、PDFファイルの「CropBox」指定を直接書き換える方法。バイナリパッチだ! これはいいな(笑)。

ただただし氏に先日のスクリプトを紹介していただいてから、ちょっとした反響がありました。
せっかくなので、

あー、いや、メタ情報の問題が残ってるか。ううむ。

メタデータについても考えてみました。


PDF の仕様について


PDF は元のファイル内容を保持したまま、ファイル末尾に更新内容を追加することでファイル内容を編集することが可能なフォーマットになっています。
今回購入したオライリーの Ebook に限っていえば、そもそもメタデータを持っていないので、単純にメタデータのオブジェクトを追加してやればよさそうです。

なお、更新内容を追加するには、オブジェクト以外に

  • クロスリファレンステーブル
  • ファイルトレーラ

が必要になります。


暗号化ついて


オライリーの Ebook は 40-bit RC4 で暗号化されています。(パスワードは空ですが)
追加するオブジェクトも暗号化する必要があります。

暗号化の手順については、

PDF protection - SCHOOL OF COMPUTER SCIENCE, Carnegie Mellon
http://www.cs.cmu​.edu/~dst/Adobe/Gallery/anon21jul0​1-pdf-encryption​.txt

を参考にしました。


格納する情報について


Amazon API で取得しようかと思ったら、

JSON形式による書誌情報の提供をはじめました - O'Reilly Japan Community Blog
http://www.oreill​y.co.jp/community/blog/2010/11/bibliograp​hical-info-in-json.html
以前は手作業でファイル名を変更していたのですが、100タイトルを超えた辺りで心が折れました。Amazon APIを使って書名を取得すればいいことは分かっていたのですが、発行元がそれをしてしまったら負けな気がします。

そこでWebサイトの書誌情報を以下のようなURLで取得できるように、テンプレートを1つ追加しました。ファイル名をご覧いただくと分かるように、簡単な書誌情報をJSON形式でご提供しています。

オライリー自身が提供してくれていたので利用してみました。


スクリプト


例によって Perl です。

use strict;
use warnings;
use Crypt::RC4;
use Digest::MD5;
use Encode;
use JSON;
use LWP::UserAgent;

undef $/;

my $pass = '';
my $url = 'http://www.oreilly.co.jp/books/%s/biblio.json';

my $ua = LWP::UserAgent->new;

while(<*.pdf>)
{
    next unless /-(\d{13})/;
    my $bookinfo = get_bookinfo($1);

    open my $in, $_ or die;
    open my $out, '>', $bookinfo->{title}.'.pdf' or die;
    binmode $in;
    binmode $out;

    $_ = <$in>;
    my $len = length;

    my($owner, $perm, $trailer, $prev);
    $owner   = $1 while /\/O\s*\((.+?)\)/gs;
    $perm    = $1 while /\/P\s+(-?\d+)/g;
    $trailer = $1 while /(trailer\n<<.+?>>)/gs;
    $prev    = $1 while /startxref\n(\d+)/g;
    die unless $trailer;
    die unless $prev;
    die unless $owner;
    die unless $perm;

    $trailer =~ /<(.+?)>/;
    my $id = $1;
    $trailer =~ /\/Size (\d+)/;
    my $info = $1;

    my $filekey = generate_filekey($pass, $owner, $perm, $id);
    my $key = generate_key($filekey, $info, 0);

    my $title  = RC4($key, $bookinfo->{original});
    my $author = RC4($key, $bookinfo->{authors});
    escape($title);
    escape($author);

    my $obj = <<EOS;
$info 0 obj
<<
/Title ($title)
/Author ($author)
>>
endobj
EOS

    my $size = $info + 1;
    $trailer =~ s/(\/Size\s+)\d+/$1$size/;
    $trailer =~ s/(>>)/\/Prev $prev \/Info $info 0 R $1/;

    my $offset = sprintf '%010d', $len;
    my $startxref = $len + length $obj;
    my $xref = <<EOS;
xref
$info 1
$offset 00000 n

$trailer
startxref
$startxref
%%EOF
EOS

    print $out $_;
    print $out $obj;
    print $out $xref;
}

sub get_bookinfo
{
    my $req = HTTP::Request->new(GET => sprintf $url, $1);
    my $res = $ua->request($req);
    $res->is_success or die $res->status_line;

    my $bookinfo = decode_json $res->content;

    exists $bookinfo->{title} or die;
    exists $bookinfo->{authors} or die;
    exists $bookinfo->{original} or die;

    $bookinfo->{title} =~ s/[:\/\\]/ /g;
    $bookinfo->{title} =~ s/\s+/ /g;
    utf8::encode $bookinfo->{title};
    Encode::from_to $bookinfo->{title}, 'utf8', 'shiftjis';

    my @authors;
    foreach(@{$bookinfo->{authors}})
    {
        foreach(split /\,/, $_)
        {
            next unless /[a-zA-Z]/;
            s/[^\x20-\x7E]//g;
            s/^\s*(.*?)\s*$/$1/;
            push @authors, $_;
        }
    }
    $bookinfo->{authors} = join ', ', @authors;

    $bookinfo->{original} =~ s/[^\x20-\x7E]+//g;

    return $bookinfo;
}

sub generate_filekey
{
    my $pad = pack 'H*',
        '28BF4E5E4E758A4164004E56FFFA0108'.
        '2E2E00B6D0683E802F0CA9FE6453697A';

    my $md5 = Digest::MD5->new;
    $md5->add(substr $_[0].$pad, 0, 32);
    $md5->add($_[1]);
    $md5->add(pack 'V', $_[2]);
    $md5->add(pack 'H*', $_[3]);
    substr $md5->digest, 0, 5;
}

sub generate_key
{
    my $md5 = Digest::MD5->new;
    $md5->add($_[0]);
    $md5->add(substr pack('V', $_[1]), 0, 3);
    $md5->add(substr pack('V', $_[2]), 0, 2);
    substr $md5->digest, 0, 10;
}

sub escape
{
    $_[0] =~ s/\\/\\\\/g;
    $_[0] =~ s/\n/\\n/g;
    $_[0] =~ s/\r/\\r/g;
    $_[0] =~ s/\t/\\t/g;
    $_[0] =~ s/\f/\\f/g;
    $_[0] =~ s/\(/\\(/g;
    $_[0] =~ s/\)/\\)/g;
}

おおまかな処理の流れ

  1. ファイル名からISBNを抽出し、書籍情報を取得
  2. ファイルから更新内容の追加に必要な情報を抽出
  3. メタデータオブジェクトを作成して、暗号化した書籍情報を格納
  4. クロスリファレンステーブルとファイルトレーラを作成
  5. 書籍名でファイルを複製
  6. メタデータを追加


実行結果




できました!


既知の問題とか制限事項とか


あくまで、「今回自分が購入した6冊が Adobe Reader に怒られない」ようにした程度の実装なので、いろいろいい加減です。

  • 既存のメタデータがある場合を考慮していない
  • /O や /P の検索がいい加減
  • /Encrypt で指定したオブジェクトから取得するようにするべき
  • 追加データの /ID を更新していない
  • 暗号化時に「Adobe Reader が誤認しなさそうな結果が得られるまで、シードを変えて繰り返す」とかいう日和ったことをしている
  • 「あんまりだ」と言われたので、エスケープ処理を追加
  • 全角文字は非対応
  • データ構成が別物になるようなので。なんでだ。

などなど。


雑感


さすがに前回ほど簡潔にはなりませんでした。
書籍情報取得のあたりは含めないほうがよかったかなと後悔。

あと、DRM の解除をしているわけではありませんが、パスワードが分かっていることを前提としているので、行為的には DRM の解除と大して変わらないです。微妙。
まぁ、こんなこともできなくはない的な実験として。
Comments
> 書籍情報の取得を切り出すなど、ちょと修正。 -- 2011/04/05 00:28:43