.NET MAUIでAndroid端末上からPDFを生成し、A4にテキストと画像をきれいに配置して保存・共有したい——。ところがPDF生成ライブラリは完全互換ではなく、特に画像の縮尺・回転・解像度で崩れがちです。この記事では、要件整理からサンプル起点の実装手順、ハマりやすいポイントの潰し方までまとめます。
.NET MAUI(Android)で「端末上PDF生成」がつまずきやすい理由
PDFは「描画して終わり」ではなく、座標系・単位・フォント・画像の扱い・保存先の制約が同時に絡みます。特に.NET MAUI(Android)では、次の要素が重なって難易度が上がります。
- クロスプラットフォーム:同じコードで動かしたくても、PDFの実装や依存ライブラリの対応状況がプラットフォームごとに異なる
- 画像まわり:ピクセル(px)とPDFのポイント(pt)の変換、EXIF回転、アスペクト比維持、メモリ消費が原因でレイアウトが崩れる
- 日本語テキスト:フォント埋め込み・文字幅計測・改行処理が必要になりやすい
- 保存と共有:Androidのストレージ(Scoped Storage)により「どこに保存するか」で挙動が変わる
| 症状 | よくある原因 | 最初に確認するポイント |
|---|---|---|
| 画像が出ない/真っ白 | リソースの読み込み方法が違う、拡張子や大文字小文字不一致、パス前提の実装 | ファイルを「アプリパッケージからStreamで読む」方式に統一する |
| 画像が小さすぎる/大きすぎる | pxをそのままptとして扱っている、dpi換算が不明確 | ページ単位(pt)でレイアウトし、画像は「枠に合わせて縮小」する |
| 写真が90度回転している | EXIF Orientationを無視している | 画像の自動回転(Auto-Orient)を入れる |
| テキストが□になる/欠ける | フォント未埋め込み、フォントに日本語グリフがない | 日本語対応フォントを同梱して明示的に使う |
| 共有(Share)で開けない | 一時ファイルの場所が不適切、拡張子がない、共有対象のURIが取れない | まずはアプリ内保存→Shareの順に分けて確認する |
最初にやるべきは要件整理:PDF生成の「種類」と「出口」を決める
PDF生成と一口に言っても、実装方針を左右する分岐が多いです。ここを曖昧にすると「動くけど使えない」「画像だけ崩れる」といった遠回りになりやすいので、最初に整理します。
| 確認項目 | 選択肢 | 実装への影響 |
|---|---|---|
| 作りたいPDFは? | 新規生成/既存PDFの編集/他形式(HTMLなど)から変換 | 新規生成なら「描画・レイアウト」が中心。変換はレンダラー依存が強い |
| ページ構成 | 1枚固定/複数ページ(ページ送り) | 複数ページは「残り高さ」管理と改ページルールが必須 |
| レイアウトの性質 | A4固定の帳票/可変レポート/写真中心 | A4固定なら座標設計で安定。可変は測定と折り返しが必要 |
| 文字種 | 英数字のみ/日本語・多言語あり | 日本語ありはフォント埋め込み前提で考えるのが安全 |
| 出力先 | アプリ内(内部ストレージ)/ユーザーが見える場所/共有(他アプリ) | Androidでは保存先によって権限・取り扱いが変わる |
| オフライン要件 | 完全オフライン/ネット前提 | オフラインなら「端末内生成」に寄せる。サーバ生成は通信必須 |
今回の前提(A4・テキスト+画像・端末上で生成・保存や共有までしたい)なら、レイアウトを数値化(ページサイズ・余白・本文領域・画像領域)して組み立てるのが成功率を上げます。
実装方針は大きく3つ:何を選べば失敗しにくいか
.NET MAUI(Android)でPDFを作る方法は、現場では大きく次の3パターンに分かれます。まずは「自分の要件に合うルート」を選ぶのが近道です。
| 方針 | 概要 | 向いているケース | 注意点 |
|---|---|---|---|
| PDFライブラリ型 | C#でPDFを直接組み立てる(テキスト・画像・表など) | A4帳票、レポート、領収書、画像+文章の定型出力 | ライセンス、フォント埋め込み、画像のdpi/回転の吸収が必要 |
| AndroidネイティブAPI型 | AndroidのPDF生成APIを使い、Canvas感覚で描画してPDF化 | Android専用でよい、描画自由度を高くしたい | iOSなど他OSに展開するときは別実装が必要 |
| HTML→PDF変換型 | HTML/CSSでレイアウトし、レンダラーでPDF化 | Webのデザイン資産を流用、複雑な表組みや装飾が多い | 変換エンジン依存が強く、端末内で重い/挙動差が出やすい |
「A4にテキスト+画像を配置して出す」目的なら、まずはPDFライブラリ型か、Android専用でもよければネイティブAPI型が現実的です。どちらを選ぶにしても、学習コストを下げるために動く最小構成のサンプルから着手するのが効きます。
サンプル起点で進める:MauiGeneratePdfSampleをベースにすると何が楽か
.NET MAUI界隈では、jfversluis が公開している「MauiGeneratePdfSample」が「まず動く」を作るのに役立ちます。実際にこのサンプルを起点に、A4のPDF生成に成功したという報告もあり、最短距離でゴールへ近づけます。
- 「PDFをどこで組み立て、どのタイミングで書き出すか」の流れが掴める
- 画像をPDFに入れる最小例があるので、座標とスケーリングの考え方を真似できる
- 生成後のファイルを扱う(保存・開く・共有する)ところまで一気通貫で確認しやすい
実装を進めるときは、いきなり自社帳票のレイアウトを全部書くより、次の順番が安定します。
- サンプルをそのままビルドして「PDFが生成される」状態を作る
- PDFのページサイズをA4に固定する
- テキストを1ブロックだけ出す(固定位置でよい)
- 画像を1枚だけ出す(縮小・アスペクト維持)
- 最後に「複数ブロック」「表」「複数ページ」に拡張する
A4固定レイアウトを安定させるコツ:単位と余白を先に決める
A4のような帳票は「自由レイアウト」に見えて、実は固定の枠を決めれば一気に安定します。まずはページサイズと余白を定数として持ち、そこから本文領域や画像枠を切り出します。
| 項目 | 値 | メモ |
|---|---|---|
| A4(mm) | 210 × 297 | 縦向き(Portrait)の基本 |
| A4(pt) | 約 595 × 842 | PDFの一般的な単位。1 inch = 72 pt |
| mm→pt換算 | mm × 72 ÷ 25.4 | レイアウトをmmで考えると帳票設計が楽 |
例えば「上下左右15mmの余白」を作るなら、まず余白をptに変換し、本文領域を次のように決めます。
- ページ全体:A4(595×842pt)
- 余白:15mm ≒ 42.5pt
- 本文領域:幅 = 595 – 2×42.5、高さ = 842 – 2×42.5
この「本文領域」を起点に、上から順に「タイトル」「本文」「画像」「フッター」のように積み上げていくと、座標の迷子になりにくいです。横向き(Landscape)が必要な場合は、A4の縦横を入れ替えたうえで同じ考え方で余白と領域を切り出します。
レイアウト設計の基本パターン(例)
| 領域 | 高さの目安 | 内容例 |
|---|---|---|
| ヘッダー | 30〜60pt | 帳票タイトル、発行日、管理番号 |
| 本文 | 可変(残りを使う) | 説明文、注記、明細テキスト |
| 画像枠 | 150〜300pt | 写真、署名画像、QRコード等 |
| フッター | 20〜40pt | ページ番号、注意文 |
AndroidネイティブAPIでA4 PDFを生成する最小例(.NET MAUI向け)
「とにかくAndroid端末で確実に出したい」「クロスプラットフォームよりまずAndroidを固めたい」なら、AndroidのPdfDocumentを使う選択肢があります。MAUIの共有プロジェクトにインターフェースを置き、Platforms/Androidで実装するのが定石です。
インターフェース(共有プロジェクト)
public interface IPdfGenerator
{
Task<byte[]> CreateA4PdfAsync(string title, string body, Stream? imageStream);
}
Android実装の概念コード(Platforms/Android)
#if ANDROID
using Android.Graphics;
using Android.Graphics.Pdf;
public class AndroidPdfGenerator : IPdfGenerator
{
public Task CreateA4PdfAsync(string title, string body, Stream? imageStream)
{
// A4: 595x842 pt相当(AndroidのPdfDocumentはページサイズをPostScriptポイント(1/72インチ)で指定)
var pageInfo = new PdfDocument.PageInfo.Builder(595, 842, 1).Create();
using var doc = new PdfDocument();
var page = doc.StartPage(pageInfo);
var canvas = page.Canvas;
using var paint = new Paint(PaintFlags.AntiAlias);
paint.Color = Color.Black;
paint.TextSize = 14;
// 余白(pt相当)
int margin = 42;
// タイトル
canvas.DrawText(title, margin, margin + 20, paint);
// 本文(簡易:複雑な折り返しは別途実装)
paint.TextSize = 11;
canvas.DrawText(body, margin, margin + 60, paint);
// 画像(あれば縮小して貼る)
if (imageStream != null)
{
using var ms = new MemoryStream();
imageStream.CopyTo(ms);
var bytes = ms.ToArray();
using var bmp = BitmapFactory.DecodeByteArray(bytes, 0, bytes.Length);
if (bmp != null)
{
// 画像枠(例:横幅いっぱい、縦は200)
int frameX = margin;
int frameY = 300;
int frameW = 595 - margin * 2;
int frameH = 200;
// アスペクト比維持で枠に収める
float scale = Math.Min((float)frameW / bmp.Width, (float)frameH / bmp.Height);
int drawW = (int)(bmp.Width * scale);
int drawH = (int)(bmp.Height * scale);
var dest = new Rect(
frameX + (frameW - drawW) / 2,
frameY + (frameH - drawH) / 2,
frameX + (frameW - drawW) / 2 + drawW,
frameY + (frameH - drawH) / 2 + drawH
);
canvas.DrawBitmap(bmp, null, dest, null);
}
}
doc.FinishPage(page);
using var outStream = new MemoryStream();
doc.WriteTo(outStream);
return Task.FromResult(outStream.ToArray());
}
}
#endif
上記は「考え方を掴むための最小例」です。日本語の折り返しや複数ページ、EXIF回転補正まで入れるなら、後述のテクニックを組み合わせて実装します。
画像で崩れないための実装ポイント:縮小・向き・解像度を「明示」する
「テキストは出たけど、画像だけ変」になりやすいのは、画像が持つ情報が多いからです。帳票用の画像配置は、次の3点を押さえると安定します。
画像枠を先に決めて、画像は“枠に収める”
PDFの見た目を崩さない最強の方法は、画像を「指定サイズの枠」に対してContain(アスペクト比維持で内側に収める)で配置することです。縦横比が違っても、はみ出さず、意図しない拡大も防げます。
| 配置モード | 挙動 | 帳票用途の相性 |
|---|---|---|
| Contain | 枠に収まるよう縮小(余白が出る場合あり) | ◎ 署名・証跡写真など「欠けると困る」画像向き |
| Cover | 枠いっぱいに拡大(はみ出た分は切り捨て) | △ サムネ用途ならOK。欠けるとNGな帳票には不向き |
| Stretch | 縦横比を無視して枠に合わせる | × ほぼ確実に歪む |
EXIF回転(スマホ写真の90度問題)を吸収する
スマホで撮った写真は「実ピクセルは横向きだが、EXIFで回転表示する」ケースがあります。PDF生成側がEXIFを見ないと、90度回転した状態で貼り付くことがあります。対策はシンプルで、PDFに入れる前に画像を正しい向きに変換(Auto-Orient)してから埋め込むことです。
dpi/解像度は“結果としての見た目”で調整する
帳票に貼る画像は、見た目のサイズ(pt)が重要です。元画像が4000pxでも、枠が200ptなら最終的に縮小されます。ただし巨大画像をそのまま読み込むとメモリを食うので、PDFに入れる前に「長辺を必要サイズにリサイズ」するのが現実的です。
| やりたいこと | 現実的な対策 | 効果 |
|---|---|---|
| 画像が粗い | 枠サイズに対して元画像解像度が不足。可能なら高解像度を使う | 印刷・拡大時の品質が上がる |
| 画像が重い/生成が遅い | 枠に合わせて事前リサイズ(例:長辺1000〜1500px程度) | 速度改善、メモリ節約 |
| 画像がにじむ | 縮小時の補間方式や変換処理の見直し | 可読性の改善 |
枠に収めるための計算例(概念)
ライブラリによってAPIは異なりますが、計算自体は共通です。画像(w,h)を枠(W,H)に収める場合、縮尺は次で決めます。
// 元画像サイズ(pxでもptでもよい。比率だけ使う)
double w = imageWidth;
double h = imageHeight;
// 枠サイズ(PDF座標系のpt)
double W = frameWidth;
double H = frameHeight;
// アスペクト比維持で内側に収める
double scale = Math.Min(W / w, H / h);
// 実際の描画サイズ
double drawW = w * scale;
double drawH = h * scale;
// 枠の中央に配置
double x = frameX + (W - drawW) / 2.0;
double y = frameY + (H - drawH) / 2.0;
この「枠→縮尺→中央寄せ」の考え方に統一すると、画像が増えてもレイアウト崩れを抑えられます。
テキスト配置の現場ポイント:折り返しと行間を先に決める
「A4にテキストを置く」だけなら簡単に見えますが、帳票として使うなら次が重要になります。
- 折り返し(自動改行):本文領域の幅を超えたら改行する
- 行間:フォントサイズに対して何ptの行間を取るかを固定する
- 禁則や長い英単語:完全対応が難しい場合は「想定入力」を決め、折り返しルールを単純化する
最低限の折り返し戦略
ライブラリの自動段落機能が使えるならそれが最短です。自前で行分割する場合は「文字列を1文字ずつ足して幅を超えたら改行」という方式が確実ですが、処理コストは上がります。帳票用途なら、まずは次の落としどころが現実的です。
- 「全角中心の日本語」は文字幅が揃いやすいので、文字数ベースの折り返しでも実用になる場合がある
- 英数字やURLが混じるなら、単語(スペース)単位で折り返す
- どうしても崩れる項目は「最大文字数」「入力ガイド」をUI側で制限する
日本語フォントの考え方(実務メモ)
Android端末でもPDF内に使うフォントが同じとは限りません。PDFを他端末やPCで開いたときに崩れないようにするなら、PDF側で日本語対応フォントを埋め込める設計が安心です。ここはライブラリ依存になるため、まずは「日本語が必要か」「必要ならフォント同梱が可能か」を要件として押さえておくと後戻りが減ります。
保存と共有を安定させる:まずはアプリ内保存→必要ならShare
Androidはバージョンによってストレージの扱いが変わるため、「いきなりダウンロードフォルダに保存」よりも、まずはアプリが確実に書ける場所に出してから共有する方がトラブルが減ります。
| 保存先 | ユーザーが直接見える? | 用途 | 注意点 |
|---|---|---|---|
| AppDataDirectory | 基本的に見えない | 確実に保存したい、履歴として保持 | ユーザーがファイル管理アプリで探しにくい |
| CacheDirectory | 見えない | 一時生成→共有(Share) | OSに削除される可能性がある |
| ユーザーが選ぶ保存先 | 見える | 提出用、エクスポート | プラットフォームごとのファイルピッカー対応が必要 |
MAUIでの保存と共有の流れ(例)
PDF生成処理が「byte[]やStreamで返せる」形になっていれば、保存部分は共通化できます。
using Microsoft.Maui.ApplicationModel.DataTransfer;
async Task<string> SavePdfAsync(byte[] pdfBytes, string fileName)
{
// 例:アプリ内に保存(確実に書ける)
var dir = FileSystem.AppDataDirectory;
var path = Path.Combine(dir, fileName);
await File.WriteAllBytesAsync(path, pdfBytes);
return path;
}
async Task SharePdfAsync(string filePath)
{
await Share.Default.RequestAsync(new ShareFileRequest
{
Title = "PDFを共有",
File = new ShareFile(filePath)
});
}
「保存に成功したか」「共有で他アプリに渡せるか」を分けて確認できるので、原因切り分けが楽になります。さらに、保存したPDFを端末で直接開いて確認したい場合は、Launcherで開く導線を作っておくとデバッグが速くなります。
実装でハマりやすい点と対処:画像・A4固定・保存の3点を重点的に
画像:読み込み経路を“1本化”する
.NET MAUIでは、プロジェクト内の画像を「ファイルパスで読む」のは基本的に失敗しやすいです。ビルド時にパッケージングされ、実行時の実体パスが期待どおりではないことがあるからです。帳票に入れる画像は次のどちらかに寄せると安定します。
- アプリに同梱する固定画像:アプリパッケージからStreamとして読み込む
- ユーザーが選ぶ画像(写真など):ピッカーで取得したStreamを使い、必要なら事前に回転補正・リサイズする
A4固定:ページサイズ→余白→本文領域→画像領域の順で決める
A4帳票は、最初にページサイズを固定してからレイアウトを積むだけで、崩れにくさが段違いです。逆に「とりあえず描画して、最後に合わす」は高確率で詰みます。特に画像は余白を食い潰しやすいので、画像枠を先に確保してから本文を流し込む設計が安全です。
保存と共有:権限トラブルを減らす流れにする
生成後に「ユーザーが別アプリで開く」ケースまで考えるなら、次の流れが鉄板です。
- まずはアプリ内ストレージ(AppDataDirectory)に保存して“生成できた”を確定させる
- 必要ならShareで他アプリへ渡す(ユーザーがメールやクラウドへ送れる)
- ユーザーが任意場所に保存したい場合は、最後にファイル保存ダイアログ(ファイルピッカー)を追加する
トラブルシューティング:原因を早く切り分けるチェックリスト
PDF生成は「どこで崩れたか」を切り分ければ、修正は案外シンプルです。詰まったときは次の順で確認すると復帰が早いです。
| チェック | 見るべきポイント | 次の一手 |
|---|---|---|
| PDFファイルは生成されているか | ファイルサイズが0ではない、拡張子が.pdf | まずは1ページにテキスト1行だけ出して生成する |
| ページサイズはA4になっているか | 縦横(pt相当)が想定通り | ページサイズ定数をログ/デバッグ表示する |
| 座標はページ内か | x,y,width,heightが余白内に収まる | 枠線(デバッグ用)を描いて可視化する |
| 画像の読み込みは成功しているか | Streamがnullではない、バイト数がある | 画像は一度リサイズしてから埋め込む(巨大画像の事故回避) |
| 共有は別問題として検証したか | 保存とShareを分けて確認 | まずは保存したPDFを端末で開いて確認する |
よくある質問(FAQ)
ライブラリの互換性が微妙なとき、最初に疑うべきはどこですか?
最初に疑うべきは「画像(読み込み経路・回転・縮尺)」「フォント(日本語グリフ・埋め込み)」「保存先(ストレージ制約)」です。テキストだけ出す最小PDFを作り、次に画像1枚を追加して崩れ方を確認すると原因が見えやすくなります。
A4サイズの数値はどれを使えばいいですか?
PDFの世界では一般的にポイント(pt)が使われ、A4はおおむね 595×842pt(縦向き)として扱われます。まずはこの値で固定し、余白をptで引く設計にするとブレません。
生成したPDFをユーザーに渡すおすすめの方法は?
最初は「アプリ内に保存→Shareで渡す」が安全です。ユーザーがフォルダを指定して保存したい場合は、ファイルピッカーを追加してエクスポート機能として切り出すと、権限や端末差の影響を受けにくくなります。
実運用で強くなる工夫:テンプレート化・テスト・将来拡張
帳票が増えると、PDF生成コードが「座標ベタ書きの塊」になりがちです。後から直すコストを下げるには、最初からテンプレート化の余地を残しておくと効きます。
- レイアウト定数を一箇所に集める(A4サイズ、余白、フォントサイズ、行間、画像枠など)
- 描画ロジックを部品化(見出し、段落、表、画像ブロック、フッター)
- 入力データ(DTO)と描画を分離して、UIやAPIが変わってもPDF側が壊れにくい構造にする
- デバッグ用に枠線表示を切り替えできると、画像・座標ズレの修正が速い
まとめ:サンプルを起点に“レイアウトの数値化”と“画像の扱い”を固める
.NET MAUI(Android)で端末上PDF生成を成功させる鍵は、「どの方法でPDFを作るか」を先に決め、A4固定ならページサイズと余白を定数化し、画像は枠に収めるルールで扱うことです。MauiGeneratePdfSampleのような動くサンプルを起点に、テキスト1行+画像1枚から段階的に増やしていけば、互換性や画像の崩れに悩まされる時間を最小化できます。

コメント