C#でiText7を使ってPDFファイルのパスワード設定・解除する

C#でiText7を使う
--
C#で`iText7`を使ってPDFファイルのパスワード設定と解除をやってみました。
巷では`iTextSharp`の情報は見かけるのですが、JavaではなくC#の`iText7`の記事が少ないと思い書いてみました。
`iText7`は`iTextSharp`の後継版で、.NET6と組み合わせればMacでも使えます。
と言うことで、今回の動作確認は以下の環境で行いました。
* macOS Ventura 13.2
* .NET6
* Visual Studio for Mac 17.6 Preview
* iText7 7.2.5

`iText7`は「NuGetパッケージの管理」でインストールしておきます。
![スクリーンショット 2023-04-03 22.52.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/ba8fdfd5-4476-4858-4ff6-53c625ff91e3.png)

パスワードを設定する
--
まずは、パスワードがかかっていないPDFファイルを用意します。そして、それをホームディレクトリに置きます。以下、Macで試したのでファイルパスはMac用ですが、Windowsでもファイルパスを適切に書き換えれば問題なく動作します。
設定するパスワードは、文字列からバイト配列に変換する必要があります。

```C#:パスワードを設定する
using iText.Kernel.Pdf;

public void ProtectPdf()
{
    // 以下は各自のホームディレクトリのパスに変えてください
    string targetPdfPath = "/Users/mackohei/hoge.pdf";
    // 以下で作業用一時ファイルのパスを生成
    string tmpPdfPath = GenerateRandomPath(targetPdfPath);

    using (FileStream stream = new(tmpPdfPath, FileMode.Create, FileAccess.Write, FileShare.None))
    using (PdfReader pdfReader = new(targetPdfPath))
    using (PdfWriter pdfWriter = new(stream, new WriterProperties()
        // 以下でパスワードとパーミッションの設定をする
        // パスワードは文字列からバイト配列に変換する
        .SetStandardEncryption(Encoding.Default.GetBytes("password"), Encoding.Default.GetBytes("password"),
            EncryptionConstants.ALLOW_PRINTING | EncryptionConstants.ALLOW_COPY,
            EncryptionConstants.STANDARD_ENCRYPTION_128)))
    {
        // 以下でパスワード付きPDFファイルを生成
        var pdfDoc = new PdfDocument(pdfReader, pdfWriter);
        pdfDoc.Close();
    }
    // 元ファイルを一時ファイルに置き換える
    File.Delete(targetPdfPath);
    File.Move(tmpPdfPath, targetPdfPath);
}

```
パスワード設定した作業用の一時ファイルを作成し、それを元ファイルと置き換えます。
一時ファイルの完成は`PdfDocument`生成時に行われます。元ファイルの内容を読み込んだ`PdfReader`の内容を一時ファイルの`PdfWriter`に書き込む感じです。

一時ファイルのパスは以下の関数で作成しました。（これは一時ファイルのファイル名の重複を避けるためのもので、今回の記事のテーマとは直接関係ありません）
```C#:作業用一時ファイルのパスを生成
public static string GenerateRandomPath(string originalPath)
{
    string dirName = Path.GetDirectoryName(originalPath) ?? "/";
    string extension = Path.GetExtension(originalPath);
    string randomPath;
    Random random = new();
    do
    {
        randomPath = random.NextInt64(1, 9999999999).ToString("D10");
        randomPath = dirName + "/" + randomPath + extension;
    }
    while (File.Exists(randomPath));
    return randomPath;
}
```
以下に`SetStandardEncryption`メソッドのリファレンスを貼っておきます。

https://api.itextpdf.com/iText7/dotnet/7.2.5/classi_text_1_1_kernel_1_1_pdf_1_1_writer_properties.html#a38ea303765616b85be0ad72f47944e87

パスワードを解除する
--
パスワード解除も、基本的にパスワード設定と同様の流れです。
ポイントは、`PdfReader`生成時にパスワード解除が必要なところです。バイト配列に変換したパスワードを渡すことでエラー無く読み込めます。

```C#:パスワードを解除する
public void UnprotectPdf()
{
    // 以下は各自のホームディレクトリのパスに変えてください
    string targetPdfPath = "/Users/mackohei/hoge.pdf";
    // 以下で作業用一時ファイルのパスを生成
    string tmpPdfPath = GenerateRandomPath(targetPdfPath);
    // 以下で文字列のパスワードをバイト配列にエンコード
    byte[] bytePassword = System.Text.Encoding.Default.GetBytes("password");

    using (FileStream stream = new(tmpPdfPath, FileMode.Create, FileAccess.Write, FileShare.None))
    // 以下で読み込み用PDFリーダーにバイト配列を渡す
    using (PdfReader pdfReader = new(targetPdfPath, new ReaderProperties().SetPassword(bytePassword)))
    using (PdfWriter pdfWriter = new(stream))
    {
        // 以下でパスワードを除いたPDFファイルを生成
        PdfDocument pdfDoc = new(pdfReader, pdfWriter);
        pdfDoc.Close();
    }
    // 元ファイルを一時ファイルに置き換える
    File.Delete(targetPdfPath);
    File.Move(tmpPdfPath, targetPdfPath);
}
```
当然ですが、パスワードが対象のPDFファイルに設定されているものと一致しなければエラーとなります。

https://api.itextpdf.com/iText7/dotnet/7.2.5/classi_text_1_1_kernel_1_1_pdf_1_1_reader_properties.html#a87bc45ccbf01f62bc67c1ba33dcd683c

チュートリアルやリファレンスなど
--
以上、`iText7`は色々と簡単にPDF加工ができて便利なライブラリだと思います:rocket:
皆さんも機会があったら、以下などを参考に試してみてください:laughing:

https://kb.itextpdf.com/home/it7kb/ebooks/itext-7-jump-start-tutorial-for-net

https://api.itextpdf.com/iText7/dotnet/7.2.5/index.html