トップページ

2006年01月20日

AA(アスキーアート)を画像に変換

新年お蔵入りPHPスクリプト集そのさん。

2ちゃんねる及び2ちゃんねると同形式のDATファイルを利用する、まちBBSや、したらば、ぜろちゃんねるスクリプト(0ch BBS Script)などの各掲示板のレスを画像に変換する、要するにAASモドキですが、AASとは比べものにならない低脳。単にGDのimageTTFText()でなんか描いてみるテスツで作り始めて、ソッコーで飽きたため。
なお、p2機能拡張パックに、(真っ当な)PHP版のAASクローンがあるでございます。

PCサイト->携帯変換スクリプトで、画像化されたAAを表示 レスを画像に変換するニーズは、携帯端末などでAAを確認したい場合が主なわけで、これはPCサイト->携帯変換スクリプト(pc2m)との連携を前提にしていて、こいつ単独では、レスをPNG化する機能しかありません。
必要なパラメータを含む、こいつへのリクエストURLの生成や、こいつが吐き出したPNGを、携帯端末が対応している画像形式や画面サイズに応じて変換するあたりは、すべてpc2mに任せるという仕様で、pc2mを、これらの掲示板にアクセスした際に、こいつにリクエストして、そのレスポンスに再びpc2mでアクセスするようなリンクを付け足すように改造しておくという寸法になってます(URLから板とスレはわかるので、あとはレス番号ごとに、適当なところにリンクを付ける)。

負荷軽減をねらって、取得したDATと生成したPNGはローカルに一時保存しておくようになってます。
リクエストに合致するPNGが保存されていれば、それを単に表示して終了。なければ、該当のレスを含むDATがあればそこからPNGを生成し、それもなければDATの取得から始めます。
一定時間経過後のDATとPNGは、以下ではDAT取得時に掃除するようになってます。

注意点とか

<?php

// --------------------------------------------------------------------
// 設定
// --------------------------------------------------------------------

// BBSメニューHTMLファイルのローカルパス
// あらかじめmenu.2ch.net/bbsmenu.htmlなどを以下に保存しとくこと
define('LOCAL_BBSMENU', './bbsmenu.html');

// DAT&画像キャッシュの保存ディレクトリのローカルパス
// あらかじめ作成&パーミッション設定しとくこと
define('DAT_STORE', './dat_store/');

// TrueTypeフォントファイルのローカルパス
define('TTF_FILE', './ipagp-mona.ttf');

// フォントサイズ(ポイント単位)
define('FONT_SIZE', 12);

// 行間(ポイント単位)
define('LINE_HEIGHT', 18);

// 出力する画像の横幅(ピクセル単位)
define('IMAGE_WIDTH', 500);

// 掃除の際に最終アクセス時刻がこの秒数以上経過しているファイルは削除
define('GC_TIME_THRESHOLD', 6 * 60 * 60);

// --------------------------------------------------------------------
// メイン
// --------------------------------------------------------------------

// URLのパラメータがないか不正なら終了
// 板名(b)は任意の数の英数字、スレ番(t)は10桁の数字、レス番(r)は1~4桁の数字
if (!isset($_GET['b']) or !$_GET['b'] or !preg_match('/^\w+$/', $_GET['b'])
  or !isset($_GET['t']) or !$_GET['t'] or !preg_match('/^\d{10}$/', $_GET['t'])
  or !isset($_GET['r']) or !$_GET['r'] or !preg_match('/^\d{1,4}$/', $_GET['r'])) {
  exit('板とスレとレスをちゃんと指定するある');
}

// 板名
$board = $_GET['b'];
// スレ
$thread = $_GET['t'];
// レス
$res = $_GET['r'];

// DAT_STOREに「板_スレ_レス.png」という名前のファイルが
// 既にあればそれを表示し、なければ新たに作成する
if (!file_exists(DAT_STORE.$board.'_'.$thread.'_'.$res.'.png')) {

  // 「板_スレ.dat」ファイルがなければ新たに取得しレス番の行を抽出
  if (!file_exists(DAT_STORE.$board.'_'.$thread.'.dat')) {
    $_row = getDatFile($board, $thread, $res);

  // 「板_スレ.dat」ファイルがあればレス番の行を抽出
  // レス番がなければDATを新たに取得しレス番の行を抽出
  } else {
    $_handle = fopen(DAT_STORE.$board.'_'.$thread.'.dat', 'r');
    $i = 0;
    while (!feof($_handle)) {
      $i++;
      $_row = fgets($_handle);
      if ($i == $res) {
        break;
      }
      $_row = null;
    }
    fclose($_handle);
    if ($_row === null) {
      $_row = getDatFile($board, $thread, $res);
    }
  }

  // 抽出した行から本文部分を抜き出し
  // Aタグを除去&HTMLエンティティを置換
  // <br>で分割
  $array = explode('<>', $_row);
  $x = preg_replace('/<a\s[^>]+?>(.+?)<\/a>/si', '$1', $array[3]);
  $x = str_replace('&quot;', '"', $x);
  $x = str_replace('&gt;', '>', $x);
  $x = str_replace('&lt;', '<', $x);
  $x = str_replace('&amp;', '&', $x);
  $x = str_replace('&nbsp;', ' ', $x);
  $_rows = explode('<br>', $x);

  // 画像オブジェクトの作成
  // 横幅=IMAGE_WIDTH、縦長=LINE_HEIGHT×行数
  // 背景色=白、文字色=黒
  $_im = imageCreateTrueColor(IMAGE_WIDTH, LINE_HEIGHT * count($_rows));
  imagefill($_im, 0, 0, imageColorAllocate($_im, 255, 255, 255));
  $_black = imageColorAllocate($_im, 0, 0, 0);

  // ベースラインの計算
  $_baseline = FONT_SIZE + floor((LINE_HEIGHT - FONT_SIZE) /2);

  // <br>で分割した行ごとに文字を描画する
  // imageTTFTextはUTF-8でないとだめぽ
  for ($i = 0; $i < count($_rows); $i++) {
    mb_convert_variables('UTF-8', 'SJIS', $_rows[$i]);
    imageTTFText($_im, FONT_SIZE, 0, 0, $_baseline, $_black, TTF_FILE, $_rows[$i]);
    $_baseline += LINE_HEIGHT;
  }

  // 2値のPNG画像を「板_スレ_レス.png」というファイル名でDAT_STOREに保存
  // セーフモードでは、imagePNGなどでファイルを書き出す場合事前にtouchやfopenが必要
  imagetruecolortopalette($_im, false, 2);
  touch(DAT_STORE.$board.'_'.$thread.'_'.$res.'.png');
  imagePNG($_im, DAT_STORE.$board.'_'.$thread.'_'.$res.'.png');
  imageDestroy($_im);
}

// DAT_STOREの「板_スレ_レス.png」にリダイレクトで終了
header('Location:'.DAT_STORE.$board.'_'.$thread.'_'.$res.'.png');

// --------------------------------------------------------------------
// DAT取得→ローカルに保存してレス番の行を抽出する関数
// --------------------------------------------------------------------

function getDatFile($_board, $_thread, $_res) {

  // ここでDAT_STOREの掃除
  cleanUpDatStore();

  // LOCAL_BBSMENUからDATのURLを取得
  $_fetched = file_get_contents(LOCAL_BBSMENU);
  if (!preg_match('/http:\/\/\w+?\.2ch\.net\/'.$_board.'\//si', $_fetched, $matches)) {
    exit('指定の板がBBSメニューに見当たりません。。。');
  } else {
    if (false === $_fetched = @file_get_contents($matches[0].'dat/'.$_thread.'.dat')) {
      exit('<a href="'.$matches[0].'dat/'.$_thread.'.dat">'.$matches[0].'dat/'.$_thread.'.dat'.
        '</a>にアクセスできなかったようです。。。');

    // リモートファイルの内容が「^[^<]*?<>」じゃなければDATじゃない(DAT落ちで人大杉とか)
    } elseif (!preg_match('/^[^<]*?<>/', $_fetched)) {
      exit('<a href="'.$matches[0].'dat/'.$_thread.'.dat">'.$matches[0].'dat/'.$_thread.'.dat'.
        '</a>はDATじゃないと思われ。。。');
    } else {

      // DAT_STOREに「板_スレ番.dat」ファイルを作成し
      // リモートから取得したDATの内容を書き出す
      $i = 0;
      $_handle = fopen(DAT_STORE.$_board.'_'.$_thread.'.dat', 'w');
      if (!flock($_handle, LOCK_EX)) {
        fclose($_handle);
        exit('DATファイルがロックできません。。。');
      } else {
        if (0 >= $i = fwrite($_handle, $_fetched)) {
          fclose($_handle);
          exit('DATが保存できなかったようです。。。');
        }
      }
      fclose($_handle);
    }
  }

  // レス番の行を抽出
  // レス番がなければ不正なレス番指定とみなしエラーで終了
  $_handle = fopen(DAT_STORE.$_board.'_'.$_thread.'.dat', 'r');
  $i = 0;
  while (!feof($_handle)) {
    $i++;
    $_row = fgets($_handle);
    if ($i == $_res) {
      fclose($_handle);
      return $_row;
      break;
    }
  }
  fclose($_handle);
  exit('指定のレスは存在しないようです。。。');
}

// --------------------------------------------------------------------
// DAT_STOREを掃除する関数
// --------------------------------------------------------------------

function cleanUpDatStore() {

  // DAT_STOREにある*.datと*.pngについて
  // 最終アクセス時刻(ATIME)からGC_TIME_THRESHOLD秒経過しているものは削除
  clearstatcache();
  $_handle  = opendir(DAT_STORE);
  while (false !== ($_filename = readdir($_handle))) {
    if (!is_dir(DAT_STORE.$_filename)
      and preg_match('/^.+\.(?:dat|png)$/', $_filename)
      and fileatime(DAT_STORE.$_filename) < time() - GC_TIME_THRESHOLD) {
      unlink(DAT_STORE.$_filename);
    }
  }
}

?>

お約束

  • このプログラムはGNU GPLでライセンスされます。著作権は放棄されていませんが、なんの保証もありません。利用や改造はGNU GPLの定めに従って、自前のリスクでご自由に。お蔵入りにするくらいなので、ちゃんと検証してたりするわけもなく、あれこれ不具合がある筈ですが、なんらかの不利益を被っても一切関知しません。

フォントについて

AAを(作者の意図通り)正しく描画するには、モナーフォントなど、MS Pゴシックと同じ文字幅のTrueTypeフォントが必要です。
試しにMS Pゴシックを利用してみようという場合、MS Pゴシックは、Windowsのフォントフォルダにある「msgothic.ttc」という、複数のTrueTypeフォントをまとめたコレクションファイル内にあり、そのままでは利用できないので、マイクロソフトのTrueType SDKに含まれる「breakttc.exe」というコマンドラインツールで、単独のTTFファイルに分解してください(このSDKは既に配布が中止されてますが、こちらで入手できるかもしれません)。

Category: ウェブ制作
Posted 2006年01月20日 21:32

トラックバック

このエントリーのトラックバックURL:
http://www.rcdtokyo.com/mt/mt-rcdtokyo5428-tb.cgi/699

コメント

コメントをどうぞ



保存しますか?


Aoaka Style Valid Aoaka