複合機のスキャナーで 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)の作成が可能です(図は、プログラム実行時のものです)。

操作方法は、回転させたい 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 文字列を表示してくれます。
実行してみました!

コントロールが異なると、パスの区切り文字の表記が¥マークと \(バックスラッシュ)になるのは、それぞれのコントールの 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つめは、問題の発生というより、正しくは、エラーの「真」の原因です。

ぎぎぎ
( 効果音的歯軋り )
僕は、ただ、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 !
まさに、それを実現するべき時こそ、『今』です。
で、つくったのがコレ!

もちろん、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);
だから、これが、僕の本当の理想かな?

・・・
最終的にと言いながら、独自性にこだわって、それでもやっぱり 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 を必ず添付してください。