しばやん雑記

Azure Serverless とメイドさんが大好きなフリーランスのプログラマーのブログ

PDFium を Windows on ARM (ARM64) 向けにビルドする

作っているアプリケーションに ARM64 対応を将来的に入れるにあたって、依存しているライブラリで PDFium だけが x86 / x64 だけの対応だったので、ARM64 向けビルドを試しておきました。

既に Chromium は Microsoft からのコントリビューションが行われているので、ARM64 向けビルドは多少はまりましたが比較的すんなりと行えました。

手順をメモしておかないと絶対に忘れるので残しておきます。

事前準備

ビルドに必要なものは Chromium と共通なので、以下のドキュメントを読みつつ準備します。

手順に従い ARM64 向けのツールをインストールする必要がありますが、実際のコンパイルには Clang が使われているようだったので、ここでインストールしたコンパイラは使われていない気がします。

中でも "Debugging Tools For Windows" は Visual Studio から Windows SDK をインストールしただけでは入らないので、アプリケーションの変更からコンポーネントを選択する必要があります。

depot_tools を展開しつつパスを通して、gclient で PDFium のソースと依存関係をダウンロードすれば大体完了です。set DEPOT_TOOLS_WIN_TOOLCHAIN=0 は忘れがちなので注意です。

PDFium をビルドする

GN と Ninja を使ってビルドしていきますが、必要な args.gn は以下のように用意しました。target_cpu = arm64 以外は x86 / x64 と共通です。

pdf_is_standalone = true
pdf_enable_v8 = false
pdf_enable_xfa = false
pdf_use_win32_gdi = false

is_component_build = false
is_debug = false

target_cpu = "arm64"

今回は必要なかったので v8 と XFA はオフにしてビルドします。GDI も使わないのでオフにしました。

普通にこのままビルドすると DLL が生成されないので、多少パッチを当てて DLL を生成しつつ呼び出し規約を一応 stdcall にしておきました。*1

diff --git a/BUILD.gn b/BUILD.gn
index 8bfe0ca55..1abb71741 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -42,6 +42,7 @@ config("pdfium_common_config") {
   if (is_win) {
     # Assume UTF-8 by default to avoid code page dependencies.
     cflags += [ "/utf-8" ]
+    defines += [ "FPDFSDK_EXPORTS" ]
   }
 }
 
@@ -139,7 +140,7 @@ group("pdfium_public_headers") {
   ]
 }
 
-component("pdfium") {
+shared_library("pdfium") {
   libs = []
   configs += [ ":pdfium_core_config" ]
   public_configs = [ ":pdfium_public_config" ]
diff --git a/public/fpdfview.h b/public/fpdfview.h
index debe083be..a228cdc50 100644
--- a/public/fpdfview.h
+++ b/public/fpdfview.h
@@ -175,7 +175,7 @@ typedef int FPDF_ANNOT_APPEARANCEMODE;
 // Dictionary value types.
 typedef int FPDF_OBJECT_TYPE;
 
-#if defined(COMPONENT_BUILD)
+#if defined(COMPONENT_BUILD) || defined(FPDFSDK_EXPORTS)
 // FPDF_EXPORT should be consistent with |export| in the pdfium_fuzzer
 // template in testing/fuzzers/BUILD.gn.
 #if defined(WIN32)
@@ -193,7 +193,7 @@ typedef int FPDF_OBJECT_TYPE;
 #endif  // defined(WIN32)
 #else
 #define FPDF_EXPORT
-#endif  // defined(COMPONENT_BUILD)
+#endif  // defined(COMPONENT_BUILD) || defined(FPDFSDK_EXPORTS)
 
 #if defined(WIN32) && defined(FPDFSDK_EXPORTS)
 #define FPDF_CALLCONV __stdcall

これで DLL が生成されるようになります。ビルド自体は gn gen を実行して Ninja の定義を作成した後に ninja -C directory pdfium を実行すると行われます。

今回は out\arm64 以下に args.gn を置いてあるので、以下のようなコマンドを実行します。

gn gen out\arm64
ninja -C out\arm64 pdfium

これで x86 と x64 の場合は問題なく DLL が生成されますが、ARM64 の場合は以下のようなエラーが出るケースがあるようです。CRT のディレクトリ構成が微妙に異なっているのが原因のようです。

f:id:shiba-yan:20200630181202p:plain

ソート関数が int と str で比較できないのが原因なので、今回は適当に数値に変換できない場合は 0 を返して選ばれないようにしました。

f:id:shiba-yan:20200630181210p:plain

修正後は ARM64 向けでも問題なく Ninja でビルドが行えるようになりました。

f:id:shiba-yan:20200630181651p:plain

ビルド後の DLL のヘッダーを調べると AA64 になっているので、ARM64 向けの DLL であることが分かります。エクスポート関数も一応調べましたが、問題なく定義されていました。

f:id:shiba-yan:20200630181705p:plain

後は実際に ARM64 マシン上で動作するか確認するだけです。もちろん Surface Pro X を使います。

Surface Pro X で実際に試す

PDFium ではテストコードもビルド出来るのでそれを使っても良いのですが、それだとつまらないので .NET 5 Preview 6 で追加された Win Forms の ARM64 版で試しました。

サンプル自体は以下のようなコードを書きました。GDI サポートを使えば HDC に対して直接レンダリング出来るようですが、GDI も今更感あるので Bitmap に対してレンダリングします。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        var dialog = new OpenFileDialog();

        if (dialog.ShowDialog() != DialogResult.OK)
        {
            return;
        }

        var filePath = dialog.FileName;

        NativeMethods.FPDF_InitLibrary();

        var document = NativeMethods.FPDF_LoadDocument(filePath, null);
        var page = NativeMethods.FPDF_LoadPage(document, 0);

        var width = (int)NativeMethods.FPDF_GetPageWidth(page);
        var height = (int)NativeMethods.FPDF_GetPageHeight(page);

        var bitmap = new Bitmap(width, height, PixelFormat.Format24bppRgb);
        var bitmapData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);

        var pdfBitmap = NativeMethods.FPDFBitmap_CreateEx(width, height, 2, bitmapData.Scan0, bitmapData.Stride);

        NativeMethods.FPDFBitmap_FillRect(pdfBitmap, 0, 0, width, height, 0xffffffff);
        NativeMethods.FPDF_RenderPageBitmap(pdfBitmap, page, 0, 0, width, height, 0, 0);

        NativeMethods.FPDFBitmap_Destroy(pdfBitmap);

        bitmap.UnlockBits(bitmapData);

        NativeMethods.FPDF_ClosePage(page);
        NativeMethods.FPDF_CloseDocument(document);

        NativeMethods.FPDF_DestroyLibrary();

        pictureBox1.Image = bitmap;
    }
}

ARM64 向けビルドは以下のコマンドを使って Self-contained 形式でビルドしました。

これまでもコンソールアプリの場合は RID に win-arm64 を指定できましたが、Preview 6 では Win Forms に対応したのでビルドが通ります。

dotnet publish -c Release -o ./publish -r win-arm64

WPF の ARM64 サポートもひっそり入っているのを期待しましたが、ビルドエラーになりました。

ビルドしたファイルを Surface Pro X にコピーして実行すると、ちゃんと 64bit で動作しているのが確認できます。PDF 自体もちゃんとレンダリングされて表示できています。

f:id:shiba-yan:20200630190243p:plain

既に Chromium Edge が ARM64 に対応しているからか、ビルドから実行まで問題なく行えました。

割と簡単に PDF のレンダリングまで行えましたが、これを実際に PDF Viewer まで仕上げるのはちょっと面倒な感じです。時間を見つけつつ WPF で書いていこうかなという気持ちです。

*1:x64 / ARM64 だと特に意味はないと分かってはいる