Skip to content

Latest commit

 

History

History
493 lines (390 loc) · 26.3 KB

CLAUDE.md

File metadata and controls

493 lines (390 loc) · 26.3 KB

Utsushi.Win — 指示書(Claude Code 用)

複数ページ PDF を複数の PNG に変換する Windows デスクトップアプリ。 C++/WinRT + WinUI 3(Windows App SDK)。Qt6 版 Utsushi の Windows ネイティブ移植。


1. 結論

  • 描画エンジンは Windows.Data.Pdf(OS 組み込み)を使う。 PDFium も Poppler も Qt も入れない。 外部 PDF ライブラリ 0 個。結果としてライセンスは MIT で出せる(Qt6 版の LGPL/GPL 制約が消える)。
  • コア(UtsushiCore)は XAML に一切依存しない静的ライブラリにする。UI から切り離して単体テストする。
  • 純粋関数を先に固める。 寸法計算・出力名生成・ページ範囲パースは WinRT を触らない純粋関数として TDD で書く。 WinRT を触るのは PdfRenderer 1 クラスだけに閉じ込める。
  • UI より先に CLI ハーネスを作る。 GUI なしで E2E 検証できる状態を Phase 2 で確保してから WinUI に進む。
  • 開発順序: 雛形 → 純粋関数 → レンダラ+CLI → WinUI → アクセシビリティ → 配布。

2. 前提

2.1 環境

項目 値
OS Windows 11 22H2 以降(最低動作: Windows 10 1809)
言語標準 C++20(/std:c++20)
ツールチェーン Visual Studio 2026(または 2022)+ MSBuild、C++/WinRT
Windows App SDK 2.3.x 系(安定版)。バージョンは Directory.Packages.props の 1 箇所で固定する
Windows SDK 10.0.22621.0 以降
パッケージ管理 NuGet(Windows App SDK)+ vcpkg マニフェストモード(テスト用 Catch2 のみ)
テスト Catch2 v3
CI GitHub Actions windows-latest

2.2 使う WinRT API(これ以外の PDF ライブラリを追加しない)

  • winrt::Windows::Data::Pdf::PdfDocument — LoadFromFileAsync(file) / LoadFromFileAsync(file, password)
  • PdfDocument::PageCount(), GetPage(index)(index は 0 始まり。UI 表示は 1 始まり。境界で必ず変換する)
  • PdfPage::Size() — 96 DPI 基準の DIP サイズ(Windows::Foundation::Size, float)
  • PdfPage::RenderToStreamAsync(stream, options)
  • PdfPageRenderOptions — DestinationWidth / DestinationHeight / BitmapEncoderId / BackgroundColor / IsIgnoringHighContrast / SourceRect
  • winrt::Windows::Graphics::Imaging::BitmapEncoder::PngEncoderId()
  • ファイル選択は Windows App SDK の Microsoft.Windows.Storage.Pickers(1.8 で追加)を使う。 旧 Windows.Storage.Pickers + IInitializeWithWindow の HWND 差し込みは使わない。

2.3 API の落とし穴(実装前に必ず読むこと)

  1. DestinationWidth と DestinationHeight は片方だけ設定する。 片方だけならアスペクト比が保たれる。両方設定すると比率を無視して歪む。本プロジェクトでは常に DestinationWidth のみ設定し、高さは API に計算させる。ただし期待高さは自前でも計算し、 出力 PNG のデコード結果と照合する(テストで検証)。
  2. IsIgnoringHighContrast は必ず true(既定値)のままにする。 false にすると OS のハイコントラスト設定が出力 PNG に混入し、環境によって成果物が変わる。 「出力は環境非依存」を不変条件とする。
  3. PdfDocument / PdfPage はスレッドセーフではない。 1 つの PdfDocument インスタンスに対する描画は 直列化する。並列化する場合は PdfDocument をワーカ数だけ独立に開く(Phase 5 の任意課題、既定は 1 ワーカ)。
  4. アパートメント。 CLI とテストは winrt::init_apartment(apartment_type::multi_threaded)。 WinUI アプリの UI スレッドは STA なので、描画ループは co_await winrt::resume_background() で 必ずバックグラウンドへ逃がす。UI 更新前に resume_foreground(DispatcherQueue) で戻る。
  5. 出力ファイル書き込みに StorageFolder を使わない。 InMemoryRandomAccessStream に描画 → バイト列を取り出し → Win32(std::ofstream binary)で書く。 ブローカ経由の権限問題とパフォーマンス劣化を避けるため。 入力側だけは PdfDocument の要求により StorageFile::GetFileFromPathAsync を使う。
  6. 巨大ページのメモリ。 幅×高さ×4 バイトが素で確保される。上限ガードを必須にする(§4.5)。

2.4 Qt6 版との差分(移植ではなく作り直し)

Qt6 版 Utsushi Utsushi.Win
描画 Poppler / PDFium OS 標準 Windows.Data.Pdf
ライセンス LGPL/GPL 考慮が必要 MIT で可
非同期 QtConcurrent / シグナル C++/WinRT コルーチン(IAsyncOperationWithProgress)
文字列 QString std::wstring / winrt::hstring
移植方針 — コードは流用しない。UI の操作フローと出力仕様のみ踏襲する

3. スコープ

3.1 やること

  • 単一 PDF ファイルを選び、全ページまたは指定ページ範囲を PNG に一括変換
  • 出力解像度指定(DPI 指定 / 長辺ピクセル指定 / 倍率指定の 3 モード)
  • 出力先フォルダ指定、連番ファイル名の桁数・接頭辞・開始番号指定
  • 進捗表示とキャンセル
  • パスワード付き PDF への対応(パスワード入力を求める)
  • エラーの日本語表示(ページ単位の失敗を握りつぶさず一覧化する)
  • キーボード操作・スクリーンリーダ対応・ハイコントラスト対応

3.2 やらないこと(Phase 5 まで明示的に対象外)

  • 複数 PDF の一括処理/フォルダ再帰(v0.2 以降)
  • PNG 以外の出力形式(JPEG/WebP/TIFF)
  • ページの回転・トリミング UI(SourceRect は API を通すだけで UI 露出なし)
  • OCR、テキスト抽出、PDF 編集、結合・分割
  • 印刷、クラウド連携、自動更新

4. 仕様

4.1 リポジトリ構成

Utsushi.Win/
  CLAUDE.md                    ← このファイル
  docs/
    decisions/                 ← ADR(要設計判断の記録)
  Directory.Build.props        ← 全プロジェクト共通のコンパイラ設定
  Directory.Packages.props     ← NuGet バージョン集中管理
  vcpkg.json                   ← Catch2 のみ
  Utsushi.sln
  src/
    UtsushiCore/               ← 静的ライブラリ。XAML 非依存
      include/utsushi/Types.h
      include/utsushi/Geometry.h
      include/utsushi/Naming.h
      include/utsushi/PageRange.h
      include/utsushi/Errors.h
      include/utsushi/PdfRenderer.h
      src/*.cpp
    UtsushiCli/                ← コンソールハーネス(開発・CI 検証用)
    UtsushiApp/                ← WinUI 3 デスクトップアプリ
  tests/
    UtsushiCore.Tests/         ← Catch2 v3
    fixtures/                  ← テスト用 PDF(生成スクリプト同梱)
  tools/
    New-FixturePdf.ps1         ← 決定的なテスト用 PDF を生成
  .github/workflows/ci.yml

4.2 コア型定義(utsushi/Types.h)

namespace utsushi {

enum class ScaleMode { Dpi, LongestEdge, Scale };

struct RenderOptions {
    ScaleMode  mode            = ScaleMode::Dpi;
    double     dpi             = 150.0;   // mode == Dpi
    std::uint32_t longestEdgePx = 2000;   // mode == LongestEdge
    double     scale           = 1.0;     // mode == Scale
    std::uint32_t maxEdgePx    = 20000;   // 安全上限(§4.5)
    std::uint64_t maxPixelBytes = 512ull * 1024 * 1024;
    bool       transparentBackground = false;
};

struct NamingOptions {
    std::wstring  stem;                 // 空なら入力ファイルの stem
    std::wstring  separator = L"_";
    std::uint32_t minDigits = 0;        // 0 = 総ページ数の桁数から自動決定
    std::uint32_t startIndex = 1;
};

struct PixelSize { std::uint32_t width; std::uint32_t height; };

enum class ErrorCode {
    None, FileNotFound, NotAPdf, PasswordRequired, WrongPassword,
    PageOutOfRange, OutputNotWritable, DimensionTooLarge,
    OutOfMemory, Canceled, RenderFailed, Unknown
};

struct PageResult {
    std::uint32_t         pageNumber;   // 1 始まり(UI 表示と同じ)
    std::filesystem::path output;
    PixelSize             size;
    ErrorCode             error = ErrorCode::None;
    std::int32_t          hr    = 0;
};

struct RenderProgress {
    std::uint32_t completed;
    std::uint32_t total;
    std::uint32_t currentPageNumber;
};

struct RenderReport {
    std::uint32_t requested = 0;
    std::uint32_t succeeded = 0;
    std::uint32_t failed    = 0;
    bool          canceled  = false;
    std::vector<PageResult> pages;
};

} // namespace utsushi

4.3 純粋関数(WinRT に触れない。ここから TDD で書く)

// Geometry.h — PdfPage::Size() は 96 DPI 基準の DIP
PixelSize computePixelSize(double pageWidthDip, double pageHeightDip,
                           const RenderOptions& opt);

// Naming.h — 出力ファイル名(拡張子込み)を返す
std::wstring makeOutputFileName(const NamingOptions& n,
                                std::uint32_t pageNumber,
                                std::uint32_t totalPages);

// PageRange.h — "1-3,7,10-" 形式を 1 始まりの昇順・重複なしページ番号列へ
std::expected<std::vector<std::uint32_t>, ErrorCode>
parsePageRange(std::wstring_view spec, std::uint32_t pageCount);

// Errors.h
ErrorCode   classifyHresult(std::int32_t hr);
std::wstring messageFor(ErrorCode code);   // 日本語メッセージ

確定させる仕様(テストで固定する)

computePixelSize

  • Dpi: round(dip * dpi / 96.0)。丸めは四捨五入。
  • LongestEdge: 長辺が longestEdgePx になるよう等倍スケール。短辺は比率維持で四捨五入。
  • Scale: round(dip * scale)。
  • 結果は幅・高さとも最低 1px にクランプ。
  • どちらかの辺が maxEdgePx を超える、または width*height*4 > maxPixelBytes の場合は DimensionTooLarge として扱えるよう、判定関数 bool exceedsLimits(PixelSize, const RenderOptions&) を分離する。

makeOutputFileName

  • 桁数 = max(minDigits, 総ページ数の 10 進桁数)。例: 全 9 ページ→doc_1.png、全 10 ページ→doc_01.png。
  • 出力番号 = startIndex + (pageNumber - 1)。
  • stem に Windows で使えない文字 \ / : * ? " < > | が含まれる場合は _ に置換する。
  • 予約名(CON, PRN, AUX, NUL, COM1〜COM9, LPT1〜LPT9)に一致する stem は末尾に _ を付ける。

parsePageRange

  • 空文字列 → 全ページ。
  • N 単一、N-M 閉区間、N- は末尾まで、-M は先頭から。区切りは ,。空白は無視。
  • 昇順ソート+重複除去して返す。
  • M < N、0 や非数字、pageCount 超過 → ErrorCode::PageOutOfRange。

4.4 レンダラ(PdfRenderer.h)— WinRT に触るのはここだけ

class PdfRenderer {
public:
    // 文書を開く。パスワード不要なら password は空。
    static winrt::Windows::Foundation::IAsyncOperation<OpenResult>
        OpenAsync(std::filesystem::path pdfPath, std::wstring password);

    // 指定ページを PNG 出力。進捗通知とキャンセルに対応。
    winrt::Windows::Foundation::IAsyncOperationWithProgress<RenderReport, RenderProgress>
        RenderAsync(std::vector<std::uint32_t> pageNumbers,
                    std::filesystem::path      outputDir,
                    RenderOptions              options,
                    NamingOptions              naming);
};

実装規約:

  • RenderAsync の先頭で co_await winrt::resume_background()。
  • ページごとに co_await winrt::get_cancellation_token() の token() を確認し、 真なら canceled = true にして即座に RenderReport を返す(例外を投げない)。
  • 1 ページの流れ: 寸法計算 → 上限チェック → PdfPageRenderOptions 構築 → InMemoryRandomAccessStream へ RenderToStreamAsync → DataReader でバイト列取得 → 一時ファイル <name>.png.tmp に書き出し → std::filesystem::rename で確定(原子的置換)。
  • 1 ページの失敗で全体を中断しない。 PageResult.error に記録して次ページへ進み、 RenderReport.failed を加算する。
  • winrt::hresult_error はこのクラスの外へ出さない。すべて classifyHresult で ErrorCode に変換する。
  • transparentBackground == true のとき BackgroundColor を Colors::Transparent() にする。 既定は白。

4.5 安全ガード

  • 出力ピクセル数上限(既定 512MB 相当)を超えたら描画せず DimensionTooLarge。
  • 出力先ディレクトリが存在しなければ作成。作成失敗・書き込み不可なら開始前に OutputNotWritable で中止。
  • 既存ファイルは既定で上書きしない。UI/CLI で「上書き」「連番退避」「スキップ」を選ばせる。 既定値は「スキップ」。
  • 入力パスと出力先が同一で stem が衝突する場合は開始前に警告する。

4.6 CLI ハーネス(UtsushiCli)

utsushi-cli <input.pdf> -o <outdir>
            [--dpi 150 | --longest-edge 2000 | --scale 1.0]
            [--pages 1-3,7] [--stem NAME] [--digits N] [--start N]
            [--password PW] [--transparent] [--overwrite skip|replace|rename]
            [--json]
  • 終了コード: 0 = 全成功 / 1 = 一部失敗 / 2 = 開始前エラー / 130 = キャンセル。
  • --json で RenderReport を JSON で標準出力(CI 検証で使う)。
  • 標準出力は UTF-8(SetConsoleOutputCP(CP_UTF8))。

4.7 WinUI 3 の UI 仕様

単一ウィンドウ・単一ページ。上から下へ一直線に並べる(複数タブ・複数ペインにしない)。

  1. 入力: 「PDF を選択」ボタン + ファイルパス表示 + ドラッグ&ドロップ受け入れ。 読み込み後に「全 N ページ / 1 ページ目の寸法」を表示。
  2. 出力先: 「フォルダを選択」ボタン + パス表示。既定は入力 PDF と同じフォルダ。
  3. 解像度: RadioButtons で 3 モード。選択中モードの数値入力だけを有効化する。 直下に「1 ページ目の出力サイズ: 1240 × 1754 px(約 8.2 MB)」を即時プレビュー表示。
  4. ページ範囲: テキストボックス。空欄 = 全ページ。不正入力はその場で赤字+理由表示。
  5. ファイル名: stem / 桁数 / 開始番号。直下に「例: doc_001.png 〜 doc_012.png」を表示。
  6. 実行: 「変換」ボタン。実行中は ProgressBar(determinate)+「12 / 40 ページ」+ 「キャンセル」。
  7. 結果: 成功数・失敗数のサマリ + 失敗ページの一覧(ページ番号と日本語の理由)+ 「出力フォルダを開く」ボタン。

アクセシビリティ要件(受け入れ基準に含める)

  • 全操作がキーボードのみで完結する。Tab 順序が視覚順と一致する。
  • すべての入力コントロールに AutomationProperties.Name(日本語)を設定する。
  • 進捗更新は AutomationProperties.LiveSetting = Polite を使い、1 ページごとに読み上げさせない。 進捗の読み上げは 10% 刻みまたは 2 秒に 1 回へ間引く。
  • エラーは色だけで伝えない。アイコン+テキストを併用する。
  • OS の「アニメーションを減らす」設定(UISettings::AnimationsEnabled)が false のとき、 遷移アニメーションを無効化する。
  • ハイコントラストテーマで文字・枠線が消えないこと(ThemeResource のみ使用、色のハードコード禁止)。
  • 既定フォントサイズを 100% として、OS のテキストサイズ 200% でもレイアウトが破綻しないこと。

4.8 パッケージング

  • 既定はアンパッケージ(unpackaged)+ 自己完結(self-contained) 配布。ZIP 展開で動く形にする。
  • MSIX は Phase 6 の任意課題。
  • インストーラは作らない。

5. TDD

5.1 進め方

1 タスク = 「失敗するテストを書く → 通す最小実装 → リファクタ → コミット」。 テストを書かずに実装を進めない。実装後にテストを後付けしない。

5.2 テスト対象の分類

層 対象 方法
純粋関数 computePixelSize / makeOutputFileName / parsePageRange / classifyHresult / exceedsLimits Catch2 単体テスト。WinRT 初期化不要
レンダラ PdfRenderer Catch2 + init_apartment(multi_threaded) + fixture PDF
CLI 引数解析・終了コード・JSON 出力 Catch2(引数解析は純粋関数に切り出す)+ CI での実行検証
UI ロジックは全て ViewModel に出す ViewModel の単体テスト。XAML の自動テストはしない

5.3 fixture PDF

外部ツールに依存しない。tools/New-FixturePdf.ps1 で非圧縮の生 PDF を手書き生成する。

必要な fixture:

  • p1.pdf — 1 ページ、A4 縦(595×842pt)
  • p12.pdf — 12 ページ(桁数の境界確認用に 9 ページ版 p9.pdf も作る)
  • mixed.pdf — A4 縦・A4 横・A3 縦が混在(ページごとに寸法が変わる)
  • tiny.pdf — 1×1pt(最小サイズのクランプ確認)
  • huge.pdf — 200×200 インチ相当(DimensionTooLarge の発火確認)
  • locked.pdf — パスワード test1234(生成が困難ならこの 1 つだけコミット済みバイナリで可。 生成方法を tests/fixtures/README.md に記録する)

5.4 最低限のテストケース(これを満たさないと Phase 完了と認めない)

computePixelSize

  • A4(595.28 × 841.89 DIP 相当)@ 150dpi → 期待値を明示的に固定
  • 96dpi でちょうど等倍になる
  • 横長ページで LongestEdge が幅に効く / 縦長で高さに効く
  • scale = 0.001 で 1px にクランプされる
  • maxEdgePx 境界のちょうど上・ちょうど下

makeOutputFileName

  • 9 ページ→1 桁、10 ページ→2 桁、100 ページ→3 桁
  • minDigits = 5 が総ページ数より優先される
  • startIndex = 0 / startIndex = 100
  • stem に a/b:c → a_b_c
  • stem が CON → CON_

parsePageRange

  • 空文字 → 全ページ
  • "3" / "1-3" / "1-3,7" / "7,1-3"(ソートされる)/ "1-3,2-4"(重複除去)
  • "5-" / "-3"
  • " 1 - 3 "(空白許容)
  • "3-1" / "0" / "abc" / pageCount 超過 → PageOutOfRange

PdfRenderer(fixture 使用)

  • p12.pdf 全ページ → 12 個の PNG が生成され、ファイル名が p12_01.png 〜 p12_12.png
  • 生成された PNG のデコード後の寸法が computePixelSize の計算値と一致する
  • mixed.pdf でページごとに寸法が変わる
  • --pages 2,5 で 2 ファイルだけ生成される
  • locked.pdf を空パスワードで開くと PasswordRequired、誤パスワードで WrongPassword
  • huge.pdf は DimensionTooLarge で失敗し、他ページの処理は継続する
  • キャンセル: 12 ページの途中でキャンセルすると canceled == true、生成済み PNG は破損していない、 .tmp ファイルが残らない
  • 上書きモード skip で既存ファイルが変更されないこと

5.5 コンパイラ設定(Directory.Build.props)

/std:c++20 /permissive- /W4 /WX /EHsc /utf-8 /Zc:__cplusplus /MP

警告はエラー。抑制する場合は該当箇所に #pragma warning(push/disable/pop) と理由コメントを付ける。


6. 制約

6.1 コーディング規約

  • 副作用の局在: I/O とグローバル状態を扱うのは PdfRenderer と CLI/UI の境界だけ。 それ以外は入力→出力の関数にする。
  • 可変状態を持たない: メンバ変数は極力 const。クラスよりフリー関数と値型構造体を優先する。
  • 例外: winrt::hresult_error は PdfRenderer の内側で捕捉し ErrorCode に変換する。 コア API は例外を投げない(std::bad_alloc を除く)。std::expected で失敗を返す。
  • 文字列: 内部は std::wstring / std::filesystem::path。winrt::hstring との変換は WinRT 境界のみ。 UTF-8 変換は CLI の標準出力のみ。
  • 命名: 識別子・型名は英語。コメント・UI 文言・エラーメッセージは日本語。 型は PascalCase、関数は camelCase、メンバは m_ 接頭辞なし・末尾 _。
  • サイズ上限: 1 関数 50 行、1 ファイル 400 行を目安とする。超えたら分割を検討し、 分割しない場合は理由をコメントに書く。
  • using namespace はヘッダで禁止。.cpp 内の関数スコープのみ許可。
  • 生ポインタでの所有権表現を禁止。winrt::com_ptr / std::unique_ptr を使う。

6.2 依存関係

  • NuGet / vcpkg への依存追加は事前承認制。 追加したい場合は実装を止めて理由と代替案を提示する。
  • 現時点で許可されている依存: Microsoft.WindowsAppSDK, Microsoft.Windows.CppWinRT, Catch2。
  • JSON 出力は依存を足さず手書きする(出力する構造が小さいため)。

6.3 作業プロトコル

  • 1 タスク = 1 コミット。 コミットメッセージは Conventional Commits(日本語本文可)。
  • 1 応答で複数フェーズを進めない。 フェーズの受け入れ基準を満たしたら必ず一度止めて報告する。
  • 報告フォーマット: 「やったこと / テスト結果(件数)/ 未解決 / 次にやること」。
  • 設計判断が必要な箇所は実装せず 要設計判断 として提示する。 勝手に決めない。 判断が確定したら docs/decisions/NNN-*.md に ADR として記録する。
  • レビューは Blocker / Major / Minor で severity タグを付ける。Blocker と Major が 0 になるまで次に進まない。
  • 既存の通っているテストを、実装の都合で書き換えない。仕様変更が必要なら先に提案する。

6.4 現時点の「要設計判断」項目(勝手に決めないこと)

  1. 並列描画の有無(PdfDocument を複数開くか、直列のままか)— Phase 5 で判断。既定は直列。
  2. 設定の永続化先(%LOCALAPPDATA% の JSON か、ApplicationData か)— Phase 4 で判断。
  3. MSIX パッケージ化の要否と署名方針 — Phase 6 で判断。
  4. アプリ名/リポジトリ名の確定(暫定 Utsushi.Win)— Phase 0 冒頭で確認する。

7. フェーズと受け入れ基準

Phase 0 — 雛形

  • Utsushi.sln に 4 プロジェクト(Core / Cli / Tests / App)が並び、Debug|x64 と Release|x64 でビルドが通る。
  • Directory.Build.props / Directory.Packages.props / vcpkg.json / .gitignore / LICENSE(MIT) がある。
  • Catch2 のダミーテスト 1 件が green。
  • tools/New-FixturePdf.ps1 が動き、p1.pdf / p9.pdf / p12.pdf / mixed.pdf / tiny.pdf / huge.pdf を生成する。
  • 受け入れ: msbuild Utsushi.sln /p:Configuration=Release /p:Platform=x64 が警告 0・エラー 0 で完了し、 テスト実行ファイルが exit 0 を返す。

Phase 1 — 純粋関数

  • §4.3 の全関数を §5.4 のテストケースで TDD 実装。
  • WinRT ヘッダを一切 include しない(テストで init_apartment を呼ばずに通ることで確認)。
  • 受け入れ: テスト 40 件以上が green。UtsushiCore が WinRT 型に依存していないことを include グラフで確認。

Phase 2 — レンダラ + CLI

  • PdfRenderer 実装、UtsushiCli 実装。
  • §5.4 のレンダラテストが全て green。
  • 受け入れ: utsushi-cli tests/fixtures/p12.pdf -o out --dpi 150 で 12 個の PNG が生成され、 寸法が計算値と一致する。--pages 2,5 / --password / キャンセル(Ctrl+C)が仕様どおり動く。 .tmp ファイルが 1 つも残らない。

Phase 3 — WinUI 3 UI

  • §4.7 の 1〜7 を実装。ロジックは全て ViewModel に置き、ViewModel の単体テストを書く。
  • UI スレッドをブロックしない(40 ページ変換中もウィンドウが応答する)。
  • 受け入れ: 起動 → PDF 選択 → 変換 → 結果表示が GUI だけで完結する。 変換中にウィンドウのリサイズ・移動ができる。キャンセルが 1 ページ以内で効く。

Phase 4 — アクセシビリティと堅牢性

  • §4.7 のアクセシビリティ要件を全て満たす。
  • 設定の永続化(要設計判断 #2 の決定後)。
  • 異常系: 破損 PDF、読み取り専用フォルダ、変換中の PDF 削除、ディスク満杯。
  • 受け入れ: キーボードのみで全機能を操作できる。Windows のナレーターで全コントロールが 日本語で読み上げられる。ハイコントラスト(黒・白)両テーマで表示が破綻しない。 テキストサイズ 200% でレイアウトが崩れない。上記異常系でクラッシュせず日本語メッセージが出る。

Phase 5 — CI と配布

  • GitHub Actions: ビルド → 単体テスト → CLI による fixture E2E → 成果物 ZIP をアップロード。
  • self-contained unpackaged ビルドが、Windows App SDK ランタイム未インストールのマシンで動く。
  • README(日本語・英語)、CHANGELOG。
  • 受け入れ: CI が green。クリーンな Windows 11 環境で ZIP を展開して起動・変換できる。

Phase 6 — 任意課題

  • 並列描画(要設計判断 #1)、MSIX(#3)、複数 PDF 一括処理、JPEG/WebP 出力。

8. 最初の応答でやること

  1. 上記の「要設計判断 #4(アプリ名)」を確認する。
  2. Phase 0 のタスクを 5〜8 個の粒度に分解し、リストだけ提示して止まる。実装は開始しない。
  3. 環境確認結果(VS のバージョン、Windows SDK、Windows App SDK NuGet の解決可能な最新安定版)を報告する。 §2.1 に書いた版数と食い違う場合は、実際に解決できた版数を正とし、Directory.Packages.props の 値を提案する。