2018年12月30日日曜日

perl CGI HTML::Template で文字化け

Perl CGI での文字化け問題は何年も前に解決させたはずだったのですが(以前は Perl + Template の場合)
新しい CentOS (サーバに無料で入ってる Ver 6)の環境で、現在動作しているcgiを実行すると文字化けしてしまう現象に悩まされました。

ハマった手順を書いてみますね。

1)httpd.conf の変更
文字化けはこれでなおる!とネット上のあちこちに書いてあります。
# vi /etc/httpd/conf/httpd.conf

AddDefaultCharset off
または
#AddDefaultCharset off コメントアウト。


動作結果:NG
世界中でほとんど解決しているらしいのですが、うまくいきません。

2)php.ini の変更
# vi /etc/php.ini

;default_charset = "UTF-8"

default_charset = ""
に変更


動作結果:NG
そもそも php 使ってませんし。

ここでブラウザ側のレスポンスヘッダを確認してみますと utf-8 となっていました。
んー?なのに文字化け?理解できないぞ・・・

ちなみに、cgi のソースや HTML, CSS, JS, テンプレートファイルなど、すべてのファイルは UTF-8 で記述してます。(改行は LF)
なぜだろう、と、試験を実施することに。


3)簡単なHTMLを用意
[test.html]

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>テストページ</title>
    </head>
    <body>
        UTF-8で記述されたページ
    </body>
</html>


動作結果:なんと、うまく表示されます。

4)cgi だとダメなのかな?
[test.cgi]

#!/usr/bin/perl --
print "Content-Type: text/html; charset=utf-8\n\n";
print "<html><body><div>\n";
print 'クオート文字';
print '<br>';
print "ダブルクオート文字";
print '<br>';
print "<div></body></html>\n";
exit;

クオートとダブルクオートで分けたのは、 perl の動作で文字化けしているのかどうかを確認するためです。

動作結果:なんと、うまく表示されます。

5)HTML::Template が怪しい。
どうやら使用している HTML::Template が怪しいのではないかとあたりを付けて検索したら情報がありました。
HTML::Template 2.98 で UTF問題が解決してる!というのです。
移行先のサーバーの HTML::Template は Ver,2.97 です。
ちなみにうまくいっていた旧サーバーの HTML::Template は Ver,2.94 でした。

Ver 2.98 のソースはここにありました。
https://github.com/mpeters/html-template

CPAN からのインストールしかやったことないので、git からどうやってインストールするのかわからずw
ダウンロードした HTML/Template.pm, HTML/Template/FAQ.pm をそのまま perl5 のファイルに上書きw

文字化けする cgi を動作させてみました。

動作結果:NG
ここまで、大量に時間を食ったにもかかわらず、うまくいかないとは・・・

6)binmode => 'utf8' を記述
既存 cgi の new HTML::Template 構文で、binmode => 'utf8' を追加する、との記述があったので
試してみました。

動作結果:NG
この記述は、上記リンクをたどる際に対応したパッチを当てたソースの場合に有効だったもので、動かないことは予想できていました。

7)utf8 => 1 を記述
HTML/Template のドキュメントを読み、new 構文の utf8 オプションがあることを発見。(旧サーバにはないオプションでした。)

既存 cgi の new HTML::Template 構文で、utf8 => 1 オプションを追加して実行してみました。

my $tmpl = HTML::Template->new(die_on_bad_params => 0, filename => $file, utf8 => 1);

動作結果:おお!!!!ようやく文字化けせずに表示されました!

しかしながら、問題があります。
既存プログラムの cgi ファイルは山のようにあります。
その全部にフラグを追加するのは困難ですよ。
デグレードなんか発生させちゃった日にゃ目も当てられません。
なんとかならないかな・・・

8)HTML::Template を編集。
perl5 の HTML/Template.pm を開いてみると、次のような部分なありました。

my %OPTIONS;
BEGIN {
    %OPTIONS = (
        debug                       => 0,
        ;
        utf8                        => 0,
        ;

ええい、こいつを1にしてしまえ!

        utf8                        => 1,

と変更した後で、既存の cgi に戻して動作

動作結果:OK

文字化けが解消されました!!!

モジュールの挙動が変わるなんて、ほんと困りものです。

2018年10月15日月曜日

マルチディスプレイ環境で親ウィンドウの中心にウィンドウを表示するには

表題の通りです。

親ウィンドウの中央にダイアログなどを表示しようとすると、親ウィンドウそのものが画面から外れている場合に表示領域外に表示されてしまったりして困ったことになります。

マルチディスプレイ環境だったりすると、座標域に負数が含まれる場合などもあって、なかなかややこしい。

WEB上を検索しても、GetSystemMetrics(SM_CXSCREEN)とかGetSystemMetrics(SM_CXSFULLCREEN) などを使ったサンプルしか公開されていなくて、ちょっと不満だったので書いてみました。

ま、実際にはもっとややこしい環境もあると思うので完全ではないかもしれないけど、うちの環境では表示されるのでよしとしようw

ヘッダファイルの記述

extern BOOL SetWindowCenterParent(HWND handle, HWND parent, int width, int height, int min_width = -1, int min_heigh = -1);

ソースファイル

#include 
//
// ウィンドウを親ウィンドウの中心に表示する
//
// 【パラメータ】
//  HWND handle     対象となるウィンドウハンドル
//  HWND parent     親ウィンドウハンドル(HWND_DESKTOP も可)
//  int width       ウィンドウの幅
//  int height      ウィンドウの高さ
//  int min_width   最小ウィンドウ幅(不要な場合は負数を指定)
//  int min_heigh   最小ウィンドウ高さ(不要な場合は負数を指定)

BOOL SetWindowCenterParent(HWND handle, HWND parent, int width, int height, int min_width, int min_heigh)
{
 // 親ウィンドウの中心を取得
 RECT parent_rect;
 GetWindowRect(parent, &parent_rect);

 POINT center;
 center.x = (parent_rect.right + parent_rect.left) / 2;
 center.y = (parent_rect.bottom + parent_rect.top) / 2;

 // ディスプレイ情報
 SIZE display;
 display.cx = GetSystemMetrics(SM_CXVIRTUALSCREEN);
 display.cy = GetSystemMetrics(SM_CYVIRTUALSCREEN);
 
 // タスクバーの大きさを調整
 SIZE screen;
 screen.cx = GetSystemMetrics(SM_CXSCREEN);
 screen.cy = GetSystemMetrics(SM_CYSCREEN);
 RECT work_area;
 SystemParametersInfo(SPI_GETWORKAREA, 0, &work_area, 0);
 SIZE task_bar;
 task_bar.cx = screen.cx - (work_area.right - work_area.left);
 task_bar.cy = screen.cy - (work_area.bottom - work_area.top);
 display.cx -= task_bar.cx;
 display.cy -= task_bar.cy;

 POINT origin;
 origin.x = GetSystemMetrics(SM_XVIRTUALSCREEN);
 origin.y = GetSystemMetrics(SM_YVIRTUALSCREEN);

 // 最小サイズ、最大サイズの検査
 if ((0 < min_width) && (width < min_width))
 {
  width = min_width;
 }
 if ((0 < min_heigh) && (height < min_heigh))
 {
  height = min_heigh;
 }
 if (display.cx < width)
 {
  width = display.cx;
 }
 if (display.cy < height)
 {
  height = display.cy;
 }
 // 表示位置
 POINT place;
 place.x = center.x - (width / 2);
 place.y = center.y - (height / 2);

 // ディスプレイの座標範囲(マルチディスプレイ含む)からのはみだし検査
 if ((origin.x + display.cx) < (place.x + width))
 {
  place.x = origin.x + display.cx - width;
 }
 if (place.x < origin.x)
 {
  place.x = origin.x;
 }
 if ((origin.y + display.cy) < (place.y + height))
 {
  place.y = origin.y + display.cy - height;
 }
 if (place.y < origin.y)
 {
  place.y = origin.y;
 }
 return SetWindowPos(handle, HWND_TOP, place.x, place.y, width, height, 0);
}

2018年8月17日金曜日

Windows 10 でコントロールパネルを開く

同じような情報がWEB上にたくさんありますが、Windows10でコントロールパネルを開く方法です。

1.エクスプローラを開きます。
タスクバーのアイコンをクリックするのが早いですね。


2.メニュー下の「↑」ボタンを押します。
コントロールパネルがこんなところにありますね。
 
おや。デスクトップ上のアイコンもあるではないですか。
こいつは便利だ。
と思ったので書き留めておきました。


2018年7月30日月曜日

HTML 枠内にできるだけ大きめの表示。大きければ縮小して全体を表示

WEBページ表示で、「Excel のように縮小して全体を表示したい」という衝動には何度も駆られますね。
住所表示用の枠を用意していて、通常はうまいこと表示できるけど長い住所だとはみ出してしまう、とか。
あともう一つ。「枠内にできるだけ大きく表示させたい」というのもあります。
PDFの編集領域とか宛名ラベル用のアプリケーションなどにそういった枠があります。
今回はその両方、つまり
『文字列が短いときは枠内にできるだけ大きく表示』
『文字列が長いときは枠内に縮小して全体を表示』
という2つの要求を満たしたい、という仕様です。
まぁ印刷する必要のあるページなどには必須だと思います。

WEB上を探していくつかダウンロードしてみましたけど、思うように動いてくれなかったので自作しました。
突っ込みどころあると思いますが、とりあえず動いたので公開しときます。
(きっとたくさんの人がこういった情報を求めているだろうと想像したのでw)

住所表示用に用意した、span id="span_address" のタグに「秋田県北秋田郡上小阿仁村大字沖田面字小蒲野下タ川原」を放り込みたいときのサンプルコードを示します。
(※住所は長い住所で検索したら出てきたもの)

<html>
<head>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
  <script type="text/javascript">
    <!--
      // sample
      window.onload=function()
      {
        $('#span_address').text('秋田県北秋田郡上小阿仁村大字沖田面字小蒲野下タ川原');
        adjust_tagfont();
      }
      // 表示、再表示時に呼び出す
      function adjust_tagfont()
      {
        //はみ出しそうなタグの親idを列挙
        var tags = ['div_address'];
        tags.forEach(adjust_tag);
      }
      // タグの確認
      function adjust_tag(tag, index, array_name)
      {
        if(document.getElementById(tag) !== null)
        { 
            $('#' + tag).autoSizeByWidth();
        }
      }
      // width/height のクラス
      function ssize(_width, _height)
      {
        this.width = _width;
        this.height = _height;
      }
      // 文字列表示の幅と高さを計算する
      function string_size(str, font_size)
      {
        var chk_span = $("#for_check");
        chk_span.text(str);
        chk_span.css('fontSize', font_size + 'px');
        var width = chk_span.outerWidth();
        var height = chk_span.outerHeight();
        chk_span.empty();
        return new ssize(width, height);
      }
      // 
      jQuery.fn.extend(
      {
        autoSizeByWidth: function()
        {
          $this = $(this);    // 枠
          this.each(function()
          {
            $this = $(this);
            var p_width = $this.width()
            var p_height = $this.height();
            var str = $.trim($this.text()); //センタリングなどの処理をはずす
            var fontSize = parseInt($this.css('fontSize').replace('px', ''));
            var child_size = string_size(str);
            // できるだけ大きく
            while((child_size.width < p_width) && (child_size.height < p_height))
            {
              child_size = string_size(str, ++fontSize);
            }
            //はみ出しているとき
            while((p_width < child_size.width) || (p_height < child_size.height))
            {
              child_size = string_size(str, --fontSize);
            }
            $this.css('fontSize', fontSize + 'px');
          });
          return this;
        },
      });
    -->
  </script>
</head>

<body>
  <span id="for_check" style="visibility:hidden;position:absolute;white-space:nowrap;">
  </span>
  <table width="400" border="1">
    <tr>
      <td width="300" height="50">
        <div id="div_address">
            <span id="span_address">&nbsp;</span>
        </div>
      </td>
      <td width="100">
        &nbsp;
      </td>
    </tr>
</body>
</html>

table タグで物理的な大きさを指定してます。
任意のサイズにして確認してみてください。

いちおう解説しますと・・・
まず枠の大きさを取得します。
フォントサイズを拡張して枠からはみ出すところまで確認した後で枠の中に納まるようフォントサイズを縮小していきます。
これだけなんだけど、まわりっくどいっすねw

お役に立てれば幸いです。



2018年7月25日水曜日

Perl CGI でバイナリファイルのアップロードに失敗する

use CGI;
my $cgi = new CGI();
my $fh = $cgi->upload(file);
my $temp = $cgi->tmpFileName($fh);
File::Copy::move($temp, $target_filename);

これでうまくいくはずなんですよね。
png ファイルをアップロードしても表示されない・・・
なかなかはまってしまいました。
コピーされたファイルのサイズが大きくなってるんです。

ファイルの中身をバイナリダンプしてみると、EF BF BD というバイト列が多数。
つまりバイト単位に変換できてないので無効な情報とされているわけ。

ネット上を検索していろいろ試しました。
binmode(STDIN);
とか
binmode(STDIN, ':raw');
とか
コピーを実装したりとか。

で、何時間も試行錯誤して、ふとスクリプトの上部を見てみると
use Encode;
use encoding 'utf-8';

ここで入力がすべてUTF-8 とみなされてしまい、バイナリ値が FF BF BD に変換されてしまっていたわけです。
この2行、削除したらまんまと動いてくれました。

2018年5月15日火曜日

Time Adapter for Excel のカスタムUI 実行時エラー

突如、Excel 2016 を起動するたびにこんなエラーメッセージが出るようになってしまいました。


どうしたものかと試した結果、こんな感じでうまくいきました。

「ファイル」-「オプション」-「アドイン」を開いて、一番下のコンボボックスで「COMアドイン」を選択して「設定」を押します。

Time Adapter for Excel なるアドインとやらのチェックをすべてオフにしてあげました。


これでうまくいきました。
そもそも何のためのアドインなのか、とかいうのは気にせずに切り捨ててしまいました(笑

2018年1月31日水曜日

Perl CGI でWEBサーバから高速に最新値を取得する

node.js あたりを使って非同期に動作させていればこんな苦労はないのでしょうがw

運用してるWEBサーバで、クライアント側の更新が必要かどうかだけを問い合わせたい時があります。

めったに変更されない値が変更されたかどうか、とか、

表示している内容が最新なのかどうか、とか。

jquery.timers.js を使って、everyTime, stopTime メソッドを呼び出せば定期的にサーバーに問い合せすることができます。

今回必要だったのが、データベース上のシーケンシャルなIDの更新状態です

ブラウザ上には一覧が表示されてるのですが、サーバー上では不定期にIDが追加されますので、現在表示している内容が最新かどうかを問い合わせたいわけです。

select count(*) from table where id>last_id; 

SQLを発行すれば、最新の件数がすぐに取得できます。

でもね、id ってときどきしか増えないのに、できるだけ早く検知するには everyTimeのインターバルを短くしなきゃならないわけで、無駄だなーと思うわけです。

しかも CGI 側では DBI::connect, disconnect, 戻り値のjsonだのXMLの出力を実施するんで、大量に利用者がいるとサーバーの負荷たるや、半端ない状態に陥ることまちがいなしです。

最新の値だけをファイルに書き込んでおいてそれを読みだすという方法も考えました。

それも open~close のオーバーヘッド考えるとわざわざ最新IDをファイルに保存するのは余計な処理と思ってしまいます。



そこで!


ファイルのタイムスタンプを利用してID管理できるんじゃなかろうか、という発想のもと作成したクラスが、これ。

package FastSerialHolder;
#============================================
# 2018/01/26 
#============================================
# 最新のIDを管理するクラス
# ファイルのタイムスタンプを使ってデータを保持させる
# ファイルの内容はアプリケーションごとに任意に書き込み
# atime<<32+mtime で63bitの値を管理する。
# ただし、atime はマウントにより使用していない場合もあるので注意
#============================================
our $FshBasePath = '任意のフォルダ';

#
#   コンストラクタ
#
sub new
{
    my ($self, $name, $group, $def_value) = @_;
    my $path = $FastSerialHolder::FshBasePath;
    #再帰的なディレクトリ作成はCGIでエラーになるので個別に作る
    #グループを階層化したい場合には、あらかじめディレクトリを作っておく
    if(! -d $path)
    {
        mkdir($path);
    }
    if(defined($group))
    {
        $path .= '/';
        $path .= $group;
        if(! -d $path)
        {
            mkdir($path);
        }
    }
    $def_value = 0 if(!defined($def_value));
    $def_value = 0 if($def_value !~ /^\d+$/);
    my $filename = $path . '/' . $name . '.fsh';
    my $object = bless {
  path => $path,
  name => $name,
  filename => $filename,
  current => $def_value,
  defualt => $def_value,
  handle => undef
 }, $self;
    if(! -e $filename)
    {
 $object->SetSerial($def_value);
    }
    return $object;
}

#
# 最新IDを取得する
#
sub GetSerial
{
    my ($self) = @_;
    if(! -e $self->{filename})
    {
        $self->{current} = $self->{default};
        return $self->{current};
    }
    my @st = stat($self->{filename}); # atime=$st[8], mtime=$st[9]
    my $hi = $st[8] << 32;
    my $lo = $st[9] & 0xFFFFFFFF;
    my $serial = ($hi + $lo) >> 1;
    return $serial;
}

sub SetSerial
{
    my ($self, $serial) = @_;
    if($serial < 0)
    {
        $serial = $self->{defualt};
    }
    if(! -e $self->{filename})
    {
        #ファイル作成
        if(open(my $fh, ">", $self->{filename}))
        {
            close($fh);
        }
    }
    my $rc = false;
    my $val = ($serial << 1);
    my $lo = ($val & 0xFFFFFFFF);
    my $hi = ($val >> 32);
    $rc = (0 < utime($hi, $lo, $self->{filename}));
    if($rc)
    {
        $self->{current} = $serial;
    }
    return $rc;
}
 

コメントなくてすみませんw

ようするに、ファイルシステムの更新時間とアクセス時間を変更して数値として取り扱うわけです。

実際には派生クラスを使って特定のグループごとにフォルダを分けて使うようにしてますが

使い方としては、最新値を取り出す場合は

my $idManager = new FastSerialHolder(名前,グループ名,初期値);
my $current = $idManager->GetSerial();
データベースが更新されたタイミングで

my $idManager = new FastSerialHolder(名前,グループ名,初期値);
$idManager->SetSerial(最新値);
とするだけです。



CGIでは、まず最新値があるかどうかの比較だけを行っておき、「あるよ」って返事が来たらデータベースにアクセスさせる別のCGIを呼び出すようにすればいいわけです。

実装して動かしてみると、とってもサクサクで快適です!

【備考というか補足というか】
実際に動かしたのは、CentOS 6.4 64bit です。

値を1ビットシフトしてるのは、ファイルシステムが秒の記録を2秒単位にしてた(昔の)記憶に頼ったもので、正しいかどうかは不明ですw

mkdir じゃなくて mkpath 使えと言われそうですが、初回生成時に STDOUTにprintするので、使わないようにしてます

stat で返される、atime と mtime が実際にファイルシステム上で何ビットなのか調べたけどわからなかったので(笑)、32bit だと思い込んで書いてます。

ファイルアクセスを高速化するために atime を無効にマウントされてる場合は 31bit しか扱えません。注意してください。(未検証)

0バイトでもクラスタ使っちゃうのかも・・・(そうなのかな?


画期的な方法だと思い込んでますが、きっと既出か、あるいはよくない実装って怒られるのか、どちらなのかもしれません。