PDFファイルの向きを変更したい!

複合機のスキャナーで A3 縦型の原稿をスキャンすると、A3 横置きの PDF ファイルとして出力・・・ つまり、縦型原稿は「横向きに回転された状態でデータ化」されます。

A3 縦置き原稿をそのまま(横向きにしないで)スキャンできる(一般ピーポーが使用できる)複合機は、僕が知る限り、多分ないんじゃないか・・・と思います。表示した際の見た目を A3 縦置きにしたい場合は、後で回転させれば事が足りるわけで、A3 縦でも横でもスキャンできるスキャナーは、普通に考えてその必要性が感じられません。

ただ、その「回転のひと手間」が問題となる場合を除いては・・・

この PDF ファイルを「そのまま印刷して利用する」のであれば、もちろん何も問題など生じませんが、紙媒体でなく、そのまま電子データとして、例えば、重い障害のある方が iPad の Goodnotes で読み込んで利用するような場合、正しい方向に戻す(=回転させる)ひと手間が(その方から見れば余計に)必要です。

たかが「ひと手間」ですが、この「ひと手間」が「ある」と「ない」とでは、当該 PDF ファイルを受け取った方の「気持ち」は大きく違ってくるのではないでしょうか?

しかも、それが毎回のことになると・・・

そのような観点から、手軽に PDF ファイルの向きを変換して、その状態を保存できるプログラムがないかと探してみたのですが、Web 上にデータをアップロードする必要があったり、例えその問題はクリアできても通信環境によっては、変換に「ちょっと我慢できないくらいの時間」を要したり、はたまたローカル環境 Only で作業できたとしても「単に向きを変換」するだけの工程の手順が、正直、とても使いにくいと感じてしまったり・・・、

「ただ向きを変える」それだけのことで、探し当てたどの方法を使っても、こんなにイライラするのであれば、(自分の知識と技術だけで PDF ファイルの向きを変更するプログラムなど、間違っても書けるわけがありませんので)サードパーティー製(?)ソフトウェアを使わせていただいて PDF ファイルを好きな向きに変更して保存できるプログラムを、自分で書けばいいのではないか? と思った次第です。

幸いなことに、僕の隣には Delphi がいてくれます。無料で使える Python 環境でも、この目標は実現できるともちろん感じましたが、こと GUI を用いて、誰に対しても優しいプログラムを書くなら、やっぱり Delphi です。それより、何より、エラーが出ないプログラム書くなら、絶対 Delphi です。

そんな理由から、PDF ファイルの向きの変換に特化したプログラムを書くことにしました!

【もくじ】

1.PDFtk Server
2.GUI を設計
3.ShellExecute で回転を実行
4.Path の表示方法を改良
5.CreateProcess で回転を実行
6.回転の実際
7.プログラムのダウンロード
8.お願いとお断り

1.PDFtk Server

自分の技術では PDF ファイルの内容をどうこうすることは到底できません。中身がどうなっているのかも、以前、ちょっとだけ勉強したことはあるのですが、今は全部忘れました。でも、他人様のお作りになられたとても良い Tool がたくさん公開されています。PDF ファイル操作のユーティリティは多数ありますが、あれこれダウンロードして実際に試用させていただき、今回は PDFtk Server を使わせていただくことにしました。

この PDFtk Server ですが、プラットフォームは、Windows、macOS、Linux に対応しており、PDF ファイルのマージ・分割・回転・その他、幅広い PDF 操作をコマンドラインで実行できる ユーティリティであるとのこと。

この「コマンドラインで実行」する部分を「 GUI 」から実行できるように、Delphi の力を借りて、インターフェイスを作ります。ただ、問題はライセンスです。

PDFtk Server のライセンスは、GNU GPL バージョン2 なので、非商用の個人利用であれば無償で使用可能です。ただし、GPLの下では自分のソフトウェアに PDFtk Server を同梱して、そのソフトウェアを配布する場合には、ソースコードの公開義務などが適用されますので、今回作成するソフトウェアでは PDFtk Server が動作に必要なことを明示して、利用者の責任で PDFtk Server のダウンロード他、プログラムの動作に必要な環境の整備を行ってもらう形をとりたいと思います。

2.GUI と Path の表示

Delphi の VCL を使えば、(慣れも必要ですが)ほんの数分で次のようなグラフィカル・ユーザー・インターフェイス(GUI)の作成が可能です(図は、プログラム実行時のものです)。

デフォルトでは、Form は最大化して表示されるようにしましたので、手動で幅と高さを変更しました。


操作方法は、回転させたい PDF ファイルを選択して、回転方向を選ぶ(オプションボタンをクリックする)だけです。回転を実行するボタンをクリックしなくても、回転方向を選んだだけで即回転が実行される機能を実現するチェックボックスも用意しました。

【注意】このプログラムは、ページを指定しての回転は実行することができません。

当初、回転した状態のプレビューを表示するような方向性も考えたのですが、たった3パターンの回転しかありませんし、ファイルの保存にもそれほど時間はかからない(何百ページもあるような PDF 文書はそもそも想定外で動作確認しておりませんので、それが必要な場合は利用者様各自の責任で検証作業を行っていただき、その結果に応じました運用をお願い申し上げます)ので、やや乱暴かもしれませんが、プログラムはオプションボタンをクリックするごとに回転を実行し、ファイルを固有の名称で(上書き)保存してしまう仕様としました。

責任逃れというわけではありませんが、処理が継続中であることを示すため、回転処理の手続き実行中は、マウスカーソルが待機状態になるよう try 文を入れてあります。

※ このプログラムでは、諸般の事情から try 文の中で待機状態を設定しています。

procedure TForm1.Button2Click(Sender: TObject);
begin
  //カーソルを待機状態に変更
  Screen.Cursor := crHourGlass;
  try 
    //処理を実行
    ・・・
  finally
    //カーソルを元の状態に変更
    Screen.Cursor := crDefault;
  end;
end;

オプションボタンをクリックした際の手続きは・・・

procedure TForm1.RadioGroup1Click(Sender: TObject);
begin
  Button2.Enabled:=True;
  if CheckBox1.Checked then
  begin
    Button2.Click;
  end;
end;

「回転実行」ボタン(=Button2)をクリックしたことにしてしまっています。

3.ShellExecute で回転を実行

で、最初に書いた PDF ファイルの回転手続きは・・・

  private
    { Private 宣言 }
    strSrcPDFName, strDstPDFName:string;
    PDFTK_PATH:string;
    //長いPath文字列の途中部分を省略して表示(どのコントロールでも使える汎用版に書き直したコード)
    function FitPathWithMiddleEllipsis(const FilePath: string; AFont: TFont; MaxWidth: Integer): string;

procedure TForm1.Button2Click(Sender: TObject);
var
  InputFile, OutputFile, RotateArg, strCommandLine: string;
begin

  PDFTK_PATH := ExtractFilePath(Application.ExeName)+'pdftk.exe';

  if not FileExists(PDFTK_PATH) then
  begin
    StatusBar1.SimpleText := 'pdftk.exe が見つかりません';
    Exit;
  end;

  InputFile := strSrcPDFName;
  if not FileExists(InputFile) then
  begin
    StatusBar1.SimpleText := 'PDFファイルが存在しません';
    Exit;
  end;

  case RadioGroup1.ItemIndex of
    0: RotateArg := 'west';  // 270°
    1: RotateArg := 'south'; // 180°
    2: RotateArg := 'east';  // 90°
  else
    StatusBar1.SimpleText := '回転方向を選択してください';
    Exit;
  end;

  //OutputFile := strDstPDFName;
  OutputFile := ChangeFileExt(strDstPDFName, '') + '_'+RotateArg+'.pdf';
  strDstPDFName:= OutputFile;

  //コマンド生成
  strCommandLine := Format('"%s" "%s" cat 1-end%s output "%s"', [
    PDFTK_PATH, InputFile, RotateArg, OutputFile
  ]);

  //実行(ダブルクオートでコマンド全体を囲む)
  if ShellExecute(0, 'open', 'cmd.exe', PChar('/C "' + Command + '"'), nil, SW_HIDE) <= 32 then
  begin
    StatusBar1.SimpleText := 'pdftk の実行に失敗しました';
  end else begin
    //長いPath文字列の途中を省略して表示(Create時にStatusBar1.SimplePanel:=True;あり)
    StatusBar1.SimpleText := FitPathWithMiddleEllipsis(
      OutputFile, StatusBar1.Font, StatusBar1.ClientWidth);

    //Application.ProcessMessages;
    Sleep(500); // 0.5秒待機
    //プレビューにPDFを表示(WebBrowser経由)
    WebBrowser1.Navigate('file:///' + StringReplace(OutputFile, '\', '/', [rfReplaceAll]));
  end;
end;

0.5 秒ほど待機時間を入れて、プレビューが失敗しないようにしています。なので、ちょっと処理が重たい感じにはなっちゃってますが、自分的には許容範囲かと・・・。

4.Path の表示方法を改良

この手続きの中で「長い文字列の途中を省略して表示」する FitPathWithMiddleEllipsis 関数を使っていますが、これは前回の記事でご紹介したものをさらに改良したものです。

前回の記事で使った FitPathWithMiddleEllipsis 関数は、TEdit と TLabel のみに対応したものでしたが、今回は StatusBar1 の SimpleText に Path 文字列を表示したかったので、次のように設計を変更し、汎用性を高めた新しい FitPathWithMiddleEllipsis 関数を使いました。

どのように汎用性を高めたかと言うと、つまり、やりたいことは「コントロールの表示幅に合わせた省略文字列を作る」ことだけ!なので、必要なのは「表示フォントと表示幅」です。そのため引数で指定するのは TControl ではなく、(「表示したい文字列」に加え)「Canvas.Font」と「最大幅(ピクセル)」にして、これを(関数側で用意した Canvas へ)渡すようにすれば、コントロール種別への依存をなくせます。こうすればどんな UI コントロールにもこの関数を適用できます。

前回、この関数は単一の手続き内から呼び出せる形式としましたが、今回は複数の手続きから呼び出して利用できるよう、Form のメンバーとして作成しました。

  private
    { Private 宣言 }
    ・・・
    //長いPath文字列の途中部分を省略して表示(どのコントロールでも使える汎用版」に書き直したコード)
    function FitPathWithMiddleEllipsis(const FilePath: string; AFont: TFont; MaxWidth: Integer): string;

関数を Private 部に宣言して、Shift+Ctrl+Cを押して、次の内容を記述します。

function TForm1.FitPathWithMiddleEllipsis(const FilePath: string; AFont: TFont;
  MaxWidth: Integer): string;
var
  Bitmap: TBitmap;
  Canvas: TCanvas;
  Ellipsis: string;
  DirPart, FilePart, DrivePart: string;
  Parts: TArray<string>;
  i, LeftCount, RightCount: Integer;
  TestPath: string;

  function MeasureTextWidth(const S: string): Integer;
  begin
    Result := Canvas.TextWidth(S);
  end;
begin
  Bitmap := TBitmap.Create;
  try
    Canvas := Bitmap.Canvas;
    Canvas.Font.Assign(AFont);

    Ellipsis := '...'+PathDelim;

    //全部入る場合
    if MeasureTextWidth(FilePath) <= MaxWidth then
      Exit(FilePath);

    //ファイル部分とディレクトリ部分を分離
    FilePart := ExtractFileName(FilePath);
    DirPart  := ExtractFilePath(FilePath);
    DrivePart := ExtractFileDrive(FilePath);

    //パスのディレクトリ部分を分解(ドライブ部分は除外)
    Parts := DirPart.Substring(Length(DrivePart) + 1).Split([PathDelim], TStringSplitOptions.ExcludeEmpty);

    //初期状態は全部表示
    TestPath := IncludeTrailingPathDelimiter(DirPart) + FilePart;

    //左右を削っていくアプローチ
    LeftCount := 0; //先頭から残すディレクトリ数
    RightCount := Length(Parts); //後ろから残すディレクトリ数

    while (LeftCount < Length(Parts)) and (MeasureTextWidth(TestPath) > MaxWidth) do
    begin
      Inc(LeftCount);
      TestPath := DrivePart + PathDelim;

      if LeftCount > 0 then
        TestPath := TestPath + Parts[0] + PathDelim;

      if LeftCount < Length(Parts) then
        TestPath := TestPath + Ellipsis;

      if RightCount > 0 then
      begin
        for i := Length(Parts) - RightCount to High(Parts) do
          if i >= 0 then
            TestPath := TestPath + Parts[i] + PathDelim;
      end;

      TestPath := TestPath + FilePart;
      Dec(RightCount);
      if RightCount < 0 then RightCount := 0;
    end;

    //収まる長さで返す
    Result := TestPath;

    //それでも収まらなければ中央省略だけで返す
    if MeasureTextWidth(Result) > MaxWidth then
    begin
      Result := Copy(FilePath, 1, 1) + '...' + Copy(FilePath, Length(FilePath), 1);
    end;

  finally
    Bitmap.Free;
  end;
end;

で、TEdit に表示したい場合は・・・

Edit1.Text:= FitPathWithMiddleEllipsis(strSrcPDFName, Edit1.Font, Edit1.ClientWidth);

TStatusBar に表示したい場合は・・・

StatusBar1.SimpleText := FitPathWithMiddleEllipsis(
      strDstPDFName, StatusBar1.Font, StatusBar1.ClientWidth);

ちなみに、ここで使っている TStatusBar は、次のように FormCreate 手続きで SimplePanel := True に設定しています。

procedure TForm1.FormCreate(Sender: TObject);
begin
  StatusBar1.SimplePanel := True;
  //Formを最大化して表示(幅も最大化される)
  Form1.WindowState:=wsMaximized;
end;

SimplePanel := True としていない場合は・・・(この場合の動作は未確認です!)

StatusBar1.SimpleText := 
  FitPathWithMiddleEllipsis(strDstPDFName, StatusBar1.Font, StatusBar1.Panels[0].Width);

・・・でしょうか?

さらに(今回のプログラムでは使用していませんが)TLabel に表示したい場合は・・・

Label1.Caption := FitPathWithMiddleEllipsis(strDstPDFName, Label1.Font, Label1.Width);

この関数に「表示したい文字列」と「コントロールのFont」と「コントロールの幅」を引数として渡してあげれば(余程コントロールの幅が狭くならない限り)末尾のファイル名と拡張子が見えるように Path の途中を省略する形で、長い Path 文字列を表示してくれます。

実行してみました!

長い Path が途中 … と省略され、末尾のファイル名と拡張子部分は表示されています。


コントロールが異なると、パスの区切り文字の表記が¥マークと \(バックスラッシュ)になるのは、それぞれのコントールの Font の違いによるものと思われます・・・。

5.CreateProcess で回転を実行

プログラムの設計当初、PDF ファイルの回転処理は先に記載した通り ShellExecute で実行していたのですが、プログラムの動作検証を行った際、200ページ以上ある PDF ファイルを回転元ファイルに指定したら、回転に失敗してしまいました。

ShellExecute では、何が起きて回転に失敗してしまったのかが皆目わかりませんので、原因を究明すべく、回転処理の実行( PDFtk Server の呼び出しと実行部分)を CreateProcess に変更し、エラーが発生した場合はメッセージを PDFtk Server から取得して表示できるよう、次のようにプログラムを修正しました。

  private
    { Private 宣言 }
    //PDFtkでコマンドを実行
    function RunPdftk(const ExePath, Params: string; out OutputStr: string): Boolean;

procedure TForm1.Button2Click(Sender: TObject);
var
  InputFile, OutputFile, RotateArg: string;
  Params, Msg:string;
  strMsg: string;
begin

  //カーソルを待機状態に変更
  Screen.Cursor:=crHourGlass;

  //CreateProcessで実行

  try
    PDFTK_PATH := ExtractFilePath(Application.ExeName) + 'pdftk.exe';

    if not FileExists(PDFTK_PATH) then
    begin
      StatusBar1.SimpleText := 'pdftk.exe が見つかりません';
      Exit;
    end;

    InputFile := strSrcPDFName;
    if not FileExists(InputFile) then
    begin
      StatusBar1.SimpleText := 'PDFファイルが存在しません';
      Exit;
    end;

    case RadioGroup1.ItemIndex of
      0: RotateArg := 'west';  // 270°
      1: RotateArg := 'south'; // 180°
      2: RotateArg := 'east';  // 90°
    else
      StatusBar1.SimpleText := '回転方向を選択してください';
      Exit;
    end;

    OutputFile := ChangeFileExt(strDstPDFName, '') + '_' + RotateArg + '.pdf';
    strDstPDFName := OutputFile;

    //end%sが正しい(end %sとしないこと:半角スペースは不要)
    Params := Format('"%s" cat 1-end%s output "%s"', [
      InputFile, RotateArg, OutputFile
    ]);

    if RunPdftk(PDFTK_PATH, Params, Msg) then
    begin
      StatusBar1.SimpleText := FitPathWithMiddleEllipsis(
        OutputFile, StatusBar1.Font, StatusBar1.ClientWidth);

      if Msg.Trim <> '' then
      begin
        //ShowMessage('pdftk 出力: ' + Msg);
        //コピーできるメッセージを表示する
        strMsg:= 'pdftk 出力: ' + Msg + #13#10 + #13#10 +
        '"Copied to clipboard"';
        //Clipboard.AsText := strMsg;  // クリップボードにコピー
        Clipboard.AsText := Msg;
        //ShowMonospaceMessage(strMsg);
        //ShowMessage(strMsg);
        Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);
      end;

      Sleep(500);
      WebBrowser1.Navigate('file:///' + StringReplace(OutputFile, '\', '/', [rfReplaceAll]));
    end
    else
    begin
      StatusBar1.SimpleText := 'pdftk の実行に失敗しました';
      if Msg.Trim <> '' then
        ShowMessage('エラー詳細: ' + Msg);
    end;
  finally
    //名前を元に戻しておく!
    strSrcPDFName := OpenDialog1.FileName;
    strDstPDFName := StringReplace(strSrcPDFName, 'SrcPDF', 'DstPDF', [rfReplaceAll, rfIgnoreCase]);
    Screen.Cursor := crDefault;
  end;

end;

function TForm1.RunPdftk(const ExePath, Params: string;
  out OutputStr: string): Boolean;
var
  SI: TStartupInfo;
  PI: TProcessInformation;
  SA: TSecurityAttributes;
  StdOutRead, StdOutWrite: THandle;
  Buffer: array[0..1023] of Byte;
  BytesRead: DWORD;
  OutputBytes: TBytes;
  CmdLine: string;
begin
  //Result := False;
  OutputStr := '';

  if not FileExists(ExePath) then
    raise Exception.CreateFmt('実行ファイルが見つかりません: %s', [ExePath]);

  ZeroMemory(@SA, SizeOf(SA));
  SA.nLength := SizeOf(SA);
  SA.bInheritHandle := True;

  if not CreatePipe(StdOutRead, StdOutWrite, @SA, 0) then
    RaiseLastOSError;
  try
    try
      SetHandleInformation(StdOutRead, HANDLE_FLAG_INHERIT, 0);

      ZeroMemory(@SI, SizeOf(SI));
      SI.cb := SizeOf(SI);
      SI.dwFlags := STARTF_USESHOWWINDOW or STARTF_USESTDHANDLES;
      SI.wShowWindow := SW_HIDE;
      SI.hStdOutput := StdOutWrite;
      SI.hStdError := StdOutWrite;

      ZeroMemory(@PI, SizeOf(PI));

      CmdLine := Format('"%s" %s', [ExePath, Params]);

      if not CreateProcess(
        nil, PChar(CmdLine), nil, nil, True,
        CREATE_NO_WINDOW, nil, nil, SI, PI) then
        RaiseLastOSError;

      CloseHandle(StdOutWrite);

      SetLength(OutputBytes, 0);
      repeat
        if not ReadFile(StdOutRead, Buffer, SizeOf(Buffer), BytesRead, nil) then
          Break;
        if BytesRead > 0 then
        begin
          //W1024 符号付型と符号無し型の演算による、オペランドの拡張」と警告される
          //SetLength(OutputBytes, Length(OutputBytes) + BytesRead);
          //対策1:BytesRead を明示的に Integer にキャストする
          SetLength(OutputBytes, Length(OutputBytes) + Integer(BytesRead));
          //対策2:Length を NativeInt にキャストする(より安全か?)
          //SetLength(OutputBytes, NativeInt(Length(OutputBytes)) + NativeInt(BytesRead));

          //W1024 符号付型と符号無し型の演算による、オペランドの拡張」と警告される
          //Move(Buffer[0], OutputBytes[Length(OutputBytes) - BytesRead], BytesRead);
          //対策1:BytesRead を明示的に Integer にキャストする
          Move(Buffer[0], OutputBytes[Length(OutputBytes) - Integer(BytesRead)], BytesRead);
        end;
      until BytesRead = 0;

      WaitForSingleObject(PI.hProcess, INFINITE);
      CloseHandle(PI.hProcess);
      CloseHandle(PI.hThread);

      if Length(OutputBytes) > 0 then
        OutputStr := TEncoding.UTF8.GetString(OutputBytes);

      Result := True;

    except
      on E: Exception do
      begin
        OutputStr := E.Message;
        Result := False;
      end;
    end;
  finally
    CloseHandle(StdOutRead);
  end;
end;

PDFtk Server の実行は、RunPdftk 関数側で行っています。

こうして CreateProcess での PDFtk Server の呼び出しに実行方法を変更し、何か問題が発生した場合には PDFtk Server 側からのエラーメッセージを取得して表示するようにできました。早速、先ほど回転に失敗した巨大な PDF ファイルを再度指定して、回転を実行してみました。

【わかったこと その①】

1つめは、問題の発生というより、正しくは、エラーの「真」の原因です。

“OWNER PASSWORD REQUIRED” と書いてあります・・・。

ぎぎぎ

( 効果音的歯軋り )

僕は、ただ、PDF を回転させようと・・・ 思っただけ・・・ なのですが、さんざん・・・ ほんとに 散々 苦労してたどり着いた 真実 は・・・

想像を遥かに絶するものでありました。

回転対象の PDF ファイルには、なんとパスワードが設定・・・ されていて当然でした。

・・・ と言うのも、もっともな理由があります。

正直に言うと、PDF ファイルを回転させるという今回のプログラムの動作検証に際し、手近に巨大な PDF ファイルが「なかった」ので、Web から簡単に入手できる 巨大 PDF ファイルはないか?と考え、思いをめぐらしたところ、すぐに思いついたのが「もう10年以上愛用しているプリンターの取扱説明書」でありました・・・ ので、さっそく愛用の 〇〇〇 社製プリンターの取扱説明書を Web から笑顔でダウンロード(何回目かなー?)して、この回転実験に使ったまではよかったのですが・・・、10年も使ったんだから許してもらえるだろうとわけのわかんないことを言い訳に、ラクしようとしたバチが当たったようです( 思いついた時は・・・ 実に!いい思いつきだと思ったのですが )。やはり、その動機が不純すぎました。

でも・・・ よく考えればこの「オーナーパスワード設定」があるのは当然です。取扱説明書、『なんでもできますー!!』みたいに勝手に書き換えられたら、それこそたいへんなコトになりますから・・・。

いやはや、これはもう・・・

手の出しようがないエラーでした!!

ま、原因がわかれば、わからないよりイイです(T_T)

ほとんど、七転八倒+四苦八苦 & いつも四面楚歌ばかり聞こえる人生(=ほぼ被害妄想)ですが、その中で学んだ最重要事項『転んでもタダでは起きるな』を、ここでもまた実践するのみです。

ぐやぐや なんじをいかんせん・・・

よくよく考えれば・・・(よくよく考えなくても・・・)

【わかったこと その②】

今回はタマタマ「手の出しようがないエラー」だったからよかった ♪ ものの、これが「手の出しようがある」エラーだった場合、OK をクリックする前に、エラーメッセージを暗記するか、「文字列」として写し取る(=メモする)必要があります。しかし、紙等に写し取るのは(自分的には)激しく面倒ですし、それより何より、このエラーメッセージはドラッグ等して、そのままクリップボードへコピーすることが、ShowMessage 関数の仕様上、出来ません!!

ちなみに、暗記はさらに無理です。

(そうだ。そのままコピペできたら・・・)

それこそ、全プログラマーの悲願です。

そう・・・

The universal wish of programmers.

それはまた・・・

The ultimate goal of all programmers.

そして、それこそは・・・

Every programmer’s long-cherished dream !

まさに、それを実現するべき時こそ、『今』です。

で、つくったのがコレ!

TMemo を Form に置いて Align := alClient としているだけですが・・・


もちろん、OK その他のボタンは、見渡すかぎり、どこにもありません。が・・・ ボタンがないかわりに・・・

好きな範囲を指定して、右クリックでコピーできます!

Delphi すごぉーイ!

( GUI が作れる全言語で、問題なく作成可能と思われますが・・・ )

OK ボタンなんて、どうせあってもただクリックするだけなんですから、その代替機能は Form 右上の「閉じる」ボタンにおまかせして、それよりエラーの原因テキストのコピペが出来れば、この際よしとしようではありませんか、皆さん!

僕は、もちろん「よし」としました☆

次が、その「エラーの原因メッセージをコピーできるようにする」コードです(表示する Form の幅と高さも自動で調整して表示するようにしてありますが、必要に応じて手動でさらに調整することも可能です)。

  private
    { Private 宣言 }
    strSrcPDFName, strDstPDFName:string;
    PDFTK_PATH:string;
    //PDFにオーナーパスワードがかかっているか調べる関数
    function IsOwnerPasswordRequired(const PdfPath, PdftkPath: string; out Output: string): Boolean;

procedure TForm1.Button1Click(Sender: TObject);
var
  OwnerPwdNeeded: Boolean;
  strMsg: string;
  strOutPut: string;

  //コピー可能なエラーメッセージを表示
  procedure ShowMonospaceMessage(const Msg: string);
  var
    Form: TForm;
    Memo: TMemo;
    CharWidth, CharHeight, MaxLineLength, LinesCount, I: Integer;
    MarginWidth, MarginHeight: Integer;
    Canvas: TCanvas;
  begin
    Form := TForm.Create(nil);
    try
      Form.Caption := 'The Real Truth Behind The Error!';
      Form.Position := poScreenCenter;

      Memo := TMemo.Create(Form);
      Memo.Parent := Form;
      Memo.Align := alClient;
      Memo.Lines.Text := Msg;
      Memo.ReadOnly := True;
      Memo.Font.Name := 'Consolas';
      Memo.Font.Size := 10;

      Form.HandleNeeded;
      Canvas := Form.Canvas;
      Canvas.Font.Assign(Memo.Font);

      CharWidth := Canvas.TextWidth('M');
      CharHeight := Canvas.TextHeight('M');

      MaxLineLength := 0;
      for I := 0 to Memo.Lines.Count - 1 do
        if Length(Memo.Lines[I]) > MaxLineLength then
          MaxLineLength := Length(Memo.Lines[I]);

      LinesCount := Memo.Lines.Count;

      //必要に応じて手動で Form の幅と高さを調整
      MarginWidth := 100;
      MarginHeight := 40;

      Form.ClientWidth := CharWidth * MaxLineLength + 10;
      Form.ClientHeight := CharHeight * LinesCount + 10;

      Form.Width := Form.ClientWidth + MarginWidth;
      Form.Height := Form.ClientHeight + MarginHeight;

      Form.ShowModal;
    finally
      Form.Free;
    end;
  end;

begin

  //ここで待機状態にしてもカーソルがすぐ元に戻ってしまう。
  //Screen.Cursor := crHourGlass;

  try
    ・・・ イロイロ設定 ・・・
    if OpenDialog1.Execute then
    begin
      ・・・ イロイロ設定 ・・・
      Screen.Cursor := crHourGlass;
      Application.ProcessMessages;
      try
        OwnerPwdNeeded := IsOwnerPasswordRequired(strSrcPDFName, PDFTK_PATH, strOutPut);
        if OwnerPwdNeeded then
        begin
          Screen.Cursor := crDefault;  // 必ず戻す
          strMsg := 'このPDFにはオーナーパスワードが設定されています。' + sLineBreak +
            strOutPut + sLineBreak +
            '処理を中止します。';
          ShowMonospaceMessage(strMsg);
          Exit;
        end;
      except
        on E: Exception do
        begin
          Screen.Cursor := crDefault;  // 必ず戻す
          strMsg := 'エラー: ' + E.Message;
          ShowMonospaceMessage(strMsg);
          Exit;
        end;
      end;
      ・・・ イロイロ設定 ・・・
    end;

  finally
    Screen.Cursor := crDefault;
  end;
end;

function TForm1.IsOwnerPasswordRequired(const PdfPath, PdftkPath: string; out Output: string): Boolean;
var
  CmdLine: string;
begin
  Result := False;

  if not FileExists(PdfPath) then
    raise Exception.Create('PDFファイルが存在しません。');

  if not FileExists(PdftkPath) then
    raise Exception.Create('pdftk.exeが見つかりません。');

  //pdftkのdump_dataコマンドでPDF情報を取得
  CmdLine := Format('"%s" "%s" dump_data', [PdftkPath, PdfPath]);

  if RunCommandAndGetOutput(CmdLine, Output) then
  begin
    //オーナーパスワードが必要ならエラーメッセージに含まれることが多い
    if Pos('OWNER PASSWORD REQUIRED', UpperCase(Output)) > 0 then
      Result := True;
  end
  else
    raise Exception.Create('pdftkの実行に失敗しました。');
end;

まぁ、イロイロありましたが、エラーメッセージだけはコピペできるようになりました☆

てか、ここでふと思ったのですが、
何もそこまでしなくても、Clipboard.AsText を使って、単に

uses
  Vcl.Clipbrd;

  strMsg := 'このPDFにはオーナーパスワードが設定されています。' + sLineBreak +
    strOutPut + sLineBreak +
    'クリップボードにエラーの内容を送信して、処理を中止します。';
  Clipboard.AsText:= strMsg;        // クリップボードにコピー
  ShowMessage(strMsg);
  Exit;

・・・としておいて、これを実行すれば、


「OK」をクリックして、メモ帳に貼り付けてみました。

この仕様の方がより親切でしょうか?

より、短く・・・

Clipboard.AsText := strOutPut;

なら・・・

エラーの核心部分のみ表示することも可能かと。


Delphi 12 Athens 以降では、 MessageDlg 関数で「警告」と「エラー」以外のアイコンが表示されなくなってしまいました。この Blog の過去記事にも書きましたが、これは Microsoft 社の UI ガイドライン変更に準拠した仕様変更によるものらしいのですが、ある日、突然、それまでずっと使い続けてきた MessageDlg 関数から「 i 」などのアイコンが消えてしまったあの時の衝撃、何か大切なものを失ったような、たまらない寂寥感が胸に広がったことを今も MessageDlg という文字を見る度に思い出します。

別に Microsoft 様の UI ガイドライン変更に反旗を翻すというような大それた意図はなく、ただメッセージにアイコンを表示したくてたまらなかった僕は必死で MessageDlg 関数の代替手段を探し、Application.MessageBox 関数がまだ生きていることを知って狂喜乱舞したのでした。・・・なので、最終的には、やっぱりいちばんのお気に入り Application.MessageBox 関数で・・・

  strMsg := 'このPDFにはオーナーパスワードが設定されています。' + sLineBreak +
    strOutPut + sLineBreak +
    'クリップボードにエラーの内容を送信して、処理を中止します。';
  Clipboard.AsText := strOutPut;
  Application.MessageBox(PChar(strMsg), PChar('エラー'), MB_ICONERROR);

だから、これが、僕の本当の理想かな?

古い人間ですので、Win32 API が好きなんです。


・・・

最終的にと言いながら、独自性にこだわって、それでもやっぱり TMemo も 「コピー」ボタンも必要なんだという場合には・・・

    { Private 宣言 }
    strMsg: string;
    procedure GetErrorMessage(Sender: TObject);

implementation

uses
  Vcl.Clipbrd;

{$R *.dfm}

procedure TForm1.Button2Click(Sender: TObject);
var
  dlg: TForm;
  btnCopy, btnClose: TButton;
  memoMsg: TMemo;
begin

  //エラーメッセージ
  strMsg := 'Error: Failed to open PDF file:' + sLineBreak +
            'C:\Users\XXX\Win32\Release\SrcPDF\TEST.PDF' + sLineBreak +
            'OWNER PASSWORD REQUIRED, but not given (or incorrect)' + sLineBreak +
            'Done.  Input errors, so no output created.';

  dlg := TForm.Create(nil);
  try
    dlg.Caption := 'メッセージ';
    dlg.Width := 400;
    dlg.Height := 240;
    dlg.Position := poScreenCenter;

    memoMsg := TMemo.Create(dlg);
    memoMsg.Parent := dlg;
    memoMsg.Left := 20;
    memoMsg.Top := 20;
    memoMsg.Width := dlg.ClientWidth - 40;
    memoMsg.Height := 120;
    memoMsg.ReadOnly := True;
    memoMsg.ScrollBars := ssVertical;
    memoMsg.Lines.Text := strMsg;

    btnCopy := TButton.Create(dlg);
    btnCopy.Parent := dlg;
    btnCopy.Caption := 'コピー';
    btnCopy.Left := 80;
    btnCopy.Top := 160;
    btnCopy.OnClick := GetErrorMessage;

    btnClose := TButton.Create(dlg);
    btnClose.Parent := dlg;
    btnClose.Caption := '閉じる';
    btnClose.Left := 200;
    btnClose.Top := 160;
    btnClose.ModalResult := mrClose;

    dlg.ShowModal;
  finally
    dlg.Free;
  end;
end;

procedure TForm1.GetErrorMessage(Sender: TObject);
begin
  Clipboard.AsText := strMsg;
end;

上のようにすれば・・・


やっぱりアイコンがないと・・・ という場合は、さらに

uses
  Vcl.Clipbrd, Vcl.ExtCtrls;

procedure TForm1.Button3Click(Sender: TObject);
var
  dlg: TForm;
  btnCopy, btnClose: TButton;
  memoMsg: TMemo;
  imgIcon: TImage;
begin
  strMsg := 'Error: Failed to open PDF file:' + sLineBreak +
            'C:\Users\XXX\Win32\Release\SrcPDF\TEST.PDF' + sLineBreak +
            'OWNER PASSWORD REQUIRED, but not given (or incorrect)' + sLineBreak +
            'Done.  Input errors, so no output created.';

  dlg := TForm.Create(nil);
  try
    dlg.Caption := 'エラー';
    dlg.Width := 420;
    dlg.Height := 260;
    dlg.Position := poScreenCenter;

    //アイコン追加
    imgIcon := TImage.Create(dlg);
    imgIcon.Parent := dlg;
    imgIcon.Left := 20;
    imgIcon.Top := 20;
    imgIcon.Width := 32;
    imgIcon.Height := 32;
    imgIcon.Picture.Icon.Handle := LoadIcon(0, IDI_ERROR); // Windows標準エラーアイコン

    //メモ表示
    memoMsg := TMemo.Create(dlg);
    memoMsg.Parent := dlg;
    memoMsg.Left := imgIcon.Left + imgIcon.Width + 10;
    memoMsg.Top := 20;
    memoMsg.Width := dlg.ClientWidth - imgIcon.Width - 50;
    memoMsg.Height := 120;
    memoMsg.ReadOnly := True;
    memoMsg.ScrollBars := ssVertical;
    memoMsg.Lines.Text := strMsg;

    //コピーボタン
    btnCopy := TButton.Create(dlg);
    btnCopy.Parent := dlg;
    btnCopy.Caption := 'コピー';
    btnCopy.Left := 80;
    btnCopy.Top := 160;
    btnCopy.OnClick := GetErrorMessage;

    //閉じるボタン
    btnClose := TButton.Create(dlg);
    btnClose.Parent := dlg;
    btnClose.Caption := '閉じる';
    btnClose.Left := 200;
    btnClose.Top := 160;
    btnClose.ModalResult := mrClose;

    dlg.ShowModal;
  finally
    dlg.Free;
  end;
end;

上のコードを実行すれば・・・


なんだか、記事の内容が本来意図した方向とずいぶん逸れてしまいました。なので、このへんで元に戻ります。

6.回転の実際

はるか上の方で、すでに示していますが、実際に PDF の回転を行った様子です。
結論から言えば、「ただ、コレがしたかった・・・ だけ」なのですが、今回もまた、なんか凄くたくさんのことに出会った気がします・・・。

最初に、左へ回転した場合です。


次に、上下反転です。


最後に、右へ回転した場合です。


連続して回転させることは、このプログラムでは考えておりません。・・・と言うか、このプログラムの仕様上、その必要性がありません。また、元の PDF ファイルは、これまたプログラムの仕様上、無加工で Src フォルダに残っていますので、「元に戻す」処理も、このプログラムには、もちろんありません。

7.プログラムのダウンロード

あくまでも自分用に作ったものですが、PDFtk Server 関連のファイルを除いたプログラム一式を以下からダウンロードできます。なお、ダウンロードとご使用にあたっては、免責事項及び使用条件への同意が必要です。免責事項及び使用条件の詳細は付属の License.txt 及び Readme.txt をご覧ください。

また、動作には PDFtk Server が必要です。

PDFtk Server のダウンロードサイト :https://www.pdflabs.com/tools/pdftk-server/

上記 Web サイトより、ダウンロードした pdftk_server-2.02-win-setup.exe をダブルクリックして起動すると、デフォルト設定では C:\Program Files (x86)\PDFtk Server にインストールが行われます。

PDFtk Server の利用にあたり、動作やライセンス内容についての詳細は、必ず公式サイトおよびライセンス文書をご確認ください。

インストール後、C:\Program Files (x86)\PDFtk Server\bin にある pdftk.exe を PDF_Rotator.exe があるPDF_Rotator フォルダ内へコピーしてください。

【プログラムが正常動作するために必要なフォルダ構成です】

PDF_Rotator\
 ├ DstPDF
 ├ SrcPDF
 ├ PDF_Rotator.exe
 ├ pdftk.exe
 ├ License.txt
 └ Readme.txt

PDF_Rotator フォルダは、下記リンク先からダウンロードできる PDF_Rotator.zip を展開すると生成されます。

回転させたい PDF ファイルは必ず SrcPDF フォルダ内に準備してください。なお、プログラムは起動時に SrcPDF フォルダ及び DstPDF フォルダの有無を調査し、それらが存在しない場合は exe と同じ階層に自動的に SrcPDF フォルダ及び DstPDF フォルダを生成します。予めご承知おきください。

PDF_Rotator.exe をダブルクリックして起動後、回転させたい PDF ファイルを選択し、回転方向を指定してください。デフォルト設定では、回転方向の指定と同時に PDF ファイルの回転と保存が行われます。回転後の PDF ファイルは、左へ回転した場合は「元のファイル名_west.pdf」、上下反転した場合は「元のファイル名_south.pdf」、右へ回転した場合は「元のファイル名_east.pdf」のように北を上とした場合の方角が付加されて DstPDF フォルダ内に保存されます。

なお、プログラムの初回起動時には、Windows Defender SmartScreen による警告画面が表示されます。この警告画面に関する詳細は、当 Blog の次の過去記事をご参照ください。

8.お願いとお断り

このサイトの内容を利用される場合は、自己責任でお願いします。記載した内容(プログラムを含む)を利用した結果、利用者および第三者に損害が発生したとしても、このサイトの管理者は一切責任を負えません。予め、ご了承ください。

また、pdftk.exe 他、PDFtk Server 関連のファイルを同梱した状態での PDF_Rotator.exe の再配布を禁じます。PDF_Rotator.exe を再配布される場合は、PDFtk Server 関連のファイルはすべて削除し、PDF_Rotator.zip に添付した License.txt 及び Readme.txt を必ず添付してください。