.NET で PDF ファイルの日時を変更する

# はじめに

僕は以前 Delphi でファイルの日時を変更する Windows アプリを作りました。
[簡単エクスプローラ拡張 EzExpEx]
(http://hp.vector.co.jp/authors/VA029585/EzExpEx/)
これは残念なことにユニコードの名称のファイルを扱えないんですよね。
最新の Delphi でビルドし直せばいいのですが、僕が持っている Delphi ではだめなんです。
これを VB.NET で作り直したいとずっと思っていました。

僕のアプリが扱えるファイルの日時は
・ファイル自体の作成日時、更新日時
・Exif 情報の撮影日時、更新日時
・PDF ファイルの作成日時、更新日時
・概要情報の作成日時、更新日時

ファイル自体の日時を変更するのは .NET の標準機能で対応できますが、PDF ファイルの日時などはどうでしょう。

## PDF ファイルの日時を変更する

PDF ファイルは、Windows が管理するファイルそのものの日時と別に、ファイルの内容に作成日時、更新日時を持っています。

PDF ファイルが持っている日時は、.NET の標準機能で変更できません。
PDF ファイルを扱うライブラリが多く作られています。例えば
・[PDFsharp - A .NET library for processing PDF]
(http://www.pdfsharp.com/PDFsharp/)
・[iTextSharp]
(https://github.com/itext/itextsharp)
・[iText for .NET]
(https://github.com/itext/itext7-dotnet)
これは楽でいいのですが、この方法で PDF ファイルを変更すると、読込時点でデコードされ保存時点でエンコードし直されるようです。

僕が Delphi で作ったアプリは、メタデータだけ読取して変更するようにしていました。
これを VB.NET で書き直しました。C# のコードも用意しました。

# PDF ファイルの日時を取得する

```vb:VB.NET
        Dim Path As String = "●●●●.jpg"    '対象とするファイルのパス
        Dim CreationTime As DateTime    '作成日時を返す
        Dim CreationValid As Boolenan = False    '作成日時を取得できたか返す
        Dim ModTime As DateTime    '更新日時を返す
        Dim ModValid As Boolean = False    '更新日時を取得できたか返す

        If Not System.IO.File.Exists(Path) Then
            Exit Function
        End If
        Try
            'ファイルを読取
            Using fs As New System.IO.FileStream(Path, System.IO.FileMode.Open, System.IO.FileAccess.Read)
                'PDF ファイルか確認
                Dim PdfHeaderBuff As Byte() = {0, 0, 0, 0, 0}
                If fs.Read(PdfHeaderBuff, 0, PdfHeaderBuff.Length) <> PdfHeaderBuff.Length Then Exit Function
                If System.Text.Encoding.ASCII.GetString(PdfHeaderBuff).ToUpper() <> "%PDF-" Then
                    Exit Function
                End If
            End Using
            Dim buff As Byte() = System.IO.File.ReadAllBytes(Path)
            With Me
                '作成日時を取得
                Dim p As Long = 0
                Do While p <= buff.Length - 13
                    If System.Text.Encoding.ASCII.GetString({buff(p), buff(p + 1), buff(p + 2), buff(p + 3), buff(p + 4), buff(p + 5), buff(p + 6), buff(p + 7), buff(p + 8), buff(p + 9), buff(p + 10), buff(p + 11), buff(p + 12)}) = "/CreationDate" Then Exit Do
                    p += 1
                Loop
                Do While p <= buff.Length - 3
                    If System.Text.Encoding.ASCII.GetString({buff(p), buff(p + 1), buff(p + 2)}) = "(D:" Then Exit Do
                    p += 1
                Loop
                p += 3
                If p <= buff.Length - 14 Then
                    Dim yy As Integer = CInt(System.Text.Encoding.ASCII.GetString({buff(p + 0), buff(p + 1), buff(p + 2), buff(p + 3)}))
                    Dim mm As Integer = CInt(System.Text.Encoding.ASCII.GetString({buff(p + 4), buff(p + 5)}))
                    Dim dd As Integer = CInt(System.Text.Encoding.ASCII.GetString({buff(p + 6), buff(p + 7)}))
                    Dim hh As Integer = CInt(System.Text.Encoding.ASCII.GetString({buff(p + 8), buff(p + 9)}))
                    Dim nn As Integer = CInt(System.Text.Encoding.ASCII.GetString({buff(p + 10), buff(p + 11)}))
                    Dim ss As Integer = CInt(System.Text.Encoding.ASCII.GetString({buff(p + 12), buff(p + 13)}))
                    CreationTime = New DateTime(yy, mm, dd, hh, nn, ss)
                    CreationValid = True
                End If
                '更新日時を取得
                p = 0
                Do While p <= buff.Length - 8
                    If System.Text.Encoding.ASCII.GetString({buff(p), buff(p + 1), buff(p + 2), buff(p + 3), buff(p + 4), buff(p + 5), buff(p + 6), buff(p + 7)}) = "/ModDate" Then Exit Do
                    p += 1
                Loop
                Do While p <= buff.Length - 3
                    If System.Text.Encoding.ASCII.GetString({buff(p), buff(p + 1), buff(p + 2)}) = "(D:" Then Exit Do
                    p += 1
                Loop
                p += 3
                If p <= buff.Length - 14 Then
                    Dim yy As Integer = CInt(System.Text.Encoding.ASCII.GetString({buff(p + 0), buff(p + 1), buff(p + 2), buff(p + 3)}))
                    Dim mm As Integer = CInt(System.Text.Encoding.ASCII.GetString({buff(p + 4), buff(p + 5)}))
                    Dim dd As Integer = CInt(System.Text.Encoding.ASCII.GetString({buff(p + 6), buff(p + 7)}))
                    Dim hh As Integer = CInt(System.Text.Encoding.ASCII.GetString({buff(p + 8), buff(p + 9)}))
                    Dim nn As Integer = CInt(System.Text.Encoding.ASCII.GetString({buff(p + 10), buff(p + 11)}))
                    Dim ss As Integer = CInt(System.Text.Encoding.ASCII.GetString({buff(p + 12), buff(p + 13)}))
                    ModTime = New DateTime(yy, mm, dd, hh, nn, ss)
                    ModValid = True
                End If
            End With
        Catch ex As Exception
            Exit Function
        End Try
```
```csharp:C#
            string Path = "●●●●.jpg";    // 対象とするファイルのパス
            DateTime CreationTime;    // 作成日時を返す
            bool CreationValid = false;    // 作成日時を取得できたか返す
            DateTime ModTime;    // 更新日時を返す
            bool ModValid = false;    // 更新日時を取得できたか返す

            if (!System.IO.File.Exists(Path)) {
                return false;
            }
            try {
                // ファイルを読取
                using (System.IO.FileStream fs = new System.IO.FileStream(Path, System.IO.FileMode.Open, System.IO.FileAccess.Read)) {
                    // PDF ファイルか確認
                    byte[] PdfHeaderBuff = new byte[5];
                    if (fs.Read(PdfHeaderBuff, 0, PdfHeaderBuff.Length) != PdfHeaderBuff.Length) return false;
                    if (System.Text.Encoding.ASCII.GetString(PdfHeaderBuff).ToUpper() != "%PDF-") {
                        return false;
                    }
                }
                byte[] buff = System.IO.File.ReadAllBytes(Path);
                {
                    // 作成日時を取得
                    long p = 0;
                    while(p <= buff.Length - 13) {
                        if (System.Text.Encoding.ASCII.GetString(new byte[] {buff[p], buff[p + 1], buff[p + 2], buff[p + 3], buff[p + 4], buff[p + 5], buff[p + 6], buff[p + 7], buff[p + 8], buff[p + 9], buff[p + 10], buff[p + 11], buff[p + 12]}) == "/CreationDate") break;
                        p++;
                    }
                    while (p <= buff.Length - 3) {
                        if (System.Text.Encoding.ASCII.GetString(new byte[] {buff[p], buff[p + 1], buff[p + 2]}) == "(D:") break;
                        p++;
                    }
                    p += 3;
                    if (p <= buff.Length - 14) {
                        int yy = int.Parse(System.Text.Encoding.ASCII.GetString(new byte[] {buff[p + 0], buff[p + 1], buff[p + 2], buff[p + 3]}));
                        int mm = int.Parse(System.Text.Encoding.ASCII.GetString(new byte[] {buff[p + 4], buff[p + 5]}));
                        int dd = int.Parse(System.Text.Encoding.ASCII.GetString(new byte[] {buff[p + 6], buff[p + 7]}));
                        int hh = int.Parse(System.Text.Encoding.ASCII.GetString(new byte[] {buff[p + 8], buff[p + 9]}));
                        int nn = int.Parse(System.Text.Encoding.ASCII.GetString(new byte[] {buff[p + 10], buff[p + 11]}));
                        int ss = int.Parse(System.Text.Encoding.ASCII.GetString(new byte[] {buff[p + 12], buff[p + 13]}));
                        CreationTime = new DateTime(yy, mm, dd, hh, nn, ss);
                        CreationValid = true;
                    }
                    // 更新日時を取得
                    p = 0;
                    while (p <= buff.Length - 13) {
                        if (System.Text.Encoding.ASCII.GetString(new byte[] {buff[p], buff[p + 1], buff[p + 2], buff[p + 3], buff[p + 4], buff[p + 5], buff[p + 6], buff[p + 7]}) == "/ModDate") break;
                        p++;
                    }
                    while (p <= buff.Length - 3) {
                        if (System.Text.Encoding.ASCII.GetString(new byte[] { buff[p], buff[p + 1], buff[p + 2] }) == "(D:") break;
                        p++;
                    }
                    p += 3;
                    if (p <= buff.Length - 14) {
                        int yy = int.Parse(System.Text.Encoding.ASCII.GetString(new byte[] {buff[p + 0], buff[p + 1], buff[p + 2], buff[p + 3]}));
                        int mm = int.Parse(System.Text.Encoding.ASCII.GetString(new byte[] {buff[p + 4], buff[p + 5]}));
                        int dd = int.Parse(System.Text.Encoding.ASCII.GetString(new byte[] {buff[p + 6], buff[p + 7]}));
                        int hh = int.Parse(System.Text.Encoding.ASCII.GetString(new byte[] {buff[p + 8], buff[p + 9]}));
                        int nn = int.Parse(System.Text.Encoding.ASCII.GetString(new byte[] {buff[p + 10], buff[p + 11]}));
                        int ss = int.Parse(System.Text.Encoding.ASCII.GetString(new byte[] {buff[p + 12], buff[p + 13]}));
                        ModTime = new DateTime(yy, mm, dd, hh, nn, ss);
                        ModValid = true;
                    }
                }
            }
            catch (Exception) {
                return false;
            }
```

# PDF ファイルの日時を変更する

```vb:VB.NET
        Dim Path As String = "●●●●.jpg"    '対象とするファイルのパス
        Dim CreationTime As DateTime    '作成日時を指定する
        Dim CreationValid As Boolean = True    '作成日時を変更するか指定する
        Dim ModTime As DateTime    '更新日時を指定する
        Dim ModValid As Boolean = True    '更新日時を変更するか指定する

        If Not System.IO.File.Exists(Path) Then
            Exit Function
        End If
        Try
            'ファイルを読取
            Using fs As New System.IO.FileStream(Path, System.IO.FileMode.Open, System.IO.FileAccess.Read)
                'PDF ファイルか確認
                Dim PdfHeaderBuff As Byte() = {0, 0, 0, 0, 0}
                If fs.Read(PdfHeaderBuff, 0, PdfHeaderBuff.Length) <> PdfHeaderBuff.Length Then Exit Function
                If System.Text.Encoding.ASCII.GetString(PdfHeaderBuff).ToUpper().TrimEnd(vbNullChar) <> "%PDF-" Then
                    Exit Function
                End If
            End Using
            Dim buff As Byte() = System.IO.File.ReadAllBytes(Path)
            With Me
                '作成日時を更新
                If CreationValid Then
                    Dim p As Long = 0
                    Do While p <= buff.Length - 13
                        If System.Text.Encoding.ASCII.GetString({buff(p), buff(p + 1), buff(p + 2), buff(p + 3), buff(p + 4), buff(p + 5), buff(p + 6), buff(p + 7), buff(p + 8), buff(p + 9), buff(p + 10), buff(p + 11), buff(p + 12)}) = "/CreationDate" Then Exit Do
                        p += 1
                    Loop
                    Do While p <= buff.Length - 3
                        If System.Text.Encoding.ASCII.GetString({buff(p), buff(p + 1), buff(p + 2)}) = "(D:" Then Exit Do
                        p += 1
                    Loop
                    p += 3
                    If p <= buff.Length - 14 Then
                        Dim s As Byte() = System.Text.Encoding.ASCII.GetBytes(Format(CreationTime, "yyyyMMddHHmmss"))
                        Buffer.BlockCopy(s, 0, buff, p, s.Length)
                    End If
                    '作成日時を更新
                    p = 0
                    Do While p <= buff.Length - 16
                        If System.Text.Encoding.ASCII.GetString({buff(p), buff(p + 1), buff(p + 2), buff(p + 3), buff(p + 4), buff(p + 5), buff(p + 6), buff(p + 7), buff(p + 8), buff(p + 9), buff(p + 10), buff(p + 11), buff(p + 12), buff(p + 13), buff(p + 14), buff(p + 15)}) = "<xmp:CreateDate>" Then Exit Do
                        p += 1
                    Loop
                    p += 16
                    If p <= buff.Length - 19 Then
                        Dim s As Byte() = System.Text.Encoding.ASCII.GetBytes(Format(CreationTime, "yyyy-MM-ddTHH:mm:ss"))
                        Buffer.BlockCopy(s, 0, buff, p, s.Length)
                    End If
                End If
                '更新日時を更新
                If CreationValid Then
                    Dim p As Long = 0
                    Do While p <= buff.Length - 8
                        If System.Text.Encoding.ASCII.GetString({buff(p), buff(p + 1), buff(p + 2), buff(p + 3), buff(p + 4), buff(p + 5), buff(p + 6), buff(p + 7)}) = "/ModDate" Then Exit Do
                        p += 1
                    Loop
                    Do While p <= buff.Length - 3
                        If System.Text.Encoding.ASCII.GetString({buff(p), buff(p + 1), buff(p + 2)}) = "(D:" Then Exit Do
                        p += 1
                    Loop
                    p += 3
                    If p <= buff.Length - 14 Then
                        Dim s As Byte() = System.Text.Encoding.ASCII.GetBytes(Format(ModTime, "yyyyMMddHHmmss"))
                        Buffer.BlockCopy(s, 0, buff, p, s.Length)
                    End If
                    '更新日時を更新
                    p = 0
                    Do While p <= buff.Length - 16
                        If System.Text.Encoding.ASCII.GetString({buff(p), buff(p + 1), buff(p + 2), buff(p + 3), buff(p + 4), buff(p + 5), buff(p + 6), buff(p + 7), buff(p + 8), buff(p + 9), buff(p + 10), buff(p + 11), buff(p + 12), buff(p + 13), buff(p + 14), buff(p + 15)}) = "<xmp:ModifyDate>" Then Exit Do
                        p += 1
                    Loop
                    p += 16
                    If p <= buff.Length - 19 Then
                        Dim s As Byte() = System.Text.Encoding.ASCII.GetBytes(Format(ModTime, "yyyy-MM-ddTHH:mm:ss"))
                        Buffer.BlockCopy(s, 0, buff, p, s.Length)
                    End If
                End If
            End With
            'ファイルに書込
            System.IO.File.WriteAllBytes(Path, buff)
        Catch ex As Exception
            Exit Function
        End Try
```
```csharp:C#
            string Path = "●●●●.jpg";    // 対象とするファイルのパス
            DateTime CreationTime;    // 作成日時を指定する
            bool CreationValid;    // 作成日時を変更するか指定する
            DateTime ModTime;    // 更新日時を指定する
            bool ModValid = True;    // 更新日時を変更するか指定する

            if (!System.IO.File.Exists(Path)) {
                return false;
            }
            try {
                // ファイルを読取
                using (System.IO.FileStream fs = new System.IO.FileStream(Path, System.IO.FileMode.Open, System.IO.FileAccess.ReadWrite)) {
                    // PDF ファイルか確認
                    byte[] PdfHeaderBuff = new byte[5];
                    if (fs.Read(PdfHeaderBuff, 0, PdfHeaderBuff.Length) != PdfHeaderBuff.Length) return false;
                    if (System.Text.Encoding.ASCII.GetString(PdfHeaderBuff).ToUpper() != "%PDF-") {
                        return false;
                    }
                }
                byte[] buff = System.IO.File.ReadAllBytes(Path);
                {
                    // 作成日時を更新
                    if (CreationValid) {
                        int p = 0;
                        while (p <= buff.Length - 13) {
                            if (System.Text.Encoding.ASCII.GetString(new byte[] {buff[p], buff[p + 1], buff[p + 2], buff[p + 3], buff[p + 4], buff[p + 5], buff[p + 6], buff[p + 7], buff[p + 8], buff[p + 9], buff[p + 10], buff[p + 11], buff[p + 12]}) == "/CreationDate") break;
                            p++;
                        }
                        while (p <= buff.Length - 3) {
                            if (System.Text.Encoding.ASCII.GetString(new byte[] {buff[p], buff[p + 1], buff[p + 2]}) == "(D:") break;
                            p++;
                        }
                        p += 3;
                        if (p <= buff.Length - 14) { 
                            byte[] s = System.Text.Encoding.ASCII.GetBytes(CreationTime.ToString("yyyyMMddHHmmss"));
                            Buffer.BlockCopy(s, 0, buff, p, s.Length);
                        }
                        // 作成日時を更新
                        p = 0;
                        while (p <= buff.Length - 16) {
                            if (System.Text.Encoding.ASCII.GetString(new byte[] {buff[p], buff[p + 1], buff[p + 2], buff[p + 3], buff[p + 4], buff[p + 5], buff[p + 6], buff[p + 7], buff[p + 8], buff[p + 9], buff[p + 10], buff[p + 11], buff[p + 12], buff[p + 13], buff[p + 14], buff[p + 15]}) == "<xmp:CreateDate>") break;
                            p++;
                        }
                        p += 16;
                        if (p <= buff.Length - 19) {
                            byte[] s = System.Text.Encoding.ASCII.GetBytes(CreationTime.ToString("yyyy-MM-ddTHH:mm:ss"));
                            Buffer.BlockCopy(s, 0, buff, p, s.Length);
                        }
                    }
                    // 更新日時を更新
                    if (CreationValid) {
                        int p = 0;
                        while (p <= buff.Length - 8) {
                            if (System.Text.Encoding.ASCII.GetString(new byte[] {buff[p], buff[p + 1], buff[p + 2], buff[p + 3], buff[p + 4], buff[p + 5], buff[p + 6], buff[p + 7]}) == "/ModDate") break;
                            p++;
                        }
                        while (p <= buff.Length - 3) {
                            if (System.Text.Encoding.ASCII.GetString(new byte[] {buff[p], buff[p + 1], buff[p + 2]}) == "(D:") break;
                            p++;
                        }
                        p += 3;
                        if (p <= buff.Length - 14) {
                            byte[] s = System.Text.Encoding.ASCII.GetBytes(CreationTime.ToString("yyyyMMddHHmmss"));
                            Buffer.BlockCopy(s, 0, buff, p, s.Length);
                        }
                        // 更新日時を更新
                        p = 0;
                        while (p <= buff.Length - 16) {
                            if (System.Text.Encoding.ASCII.GetString(new byte[] {buff[p], buff[p + 1], buff[p + 2], buff[p + 3], buff[p + 4], buff[p + 5], buff[p + 6], buff[p + 7], buff[p + 8], buff[p + 9], buff[p + 10], buff[p + 11], buff[p + 12], buff[p + 13], buff[p + 14], buff[p + 15] }) == "<xmp:ModifyDate>") break;
                            p++;
                        }
                        p += 16;
                        if (p <= buff.Length - 19) {
                            byte[] s = System.Text.Encoding.ASCII.GetBytes(CreationTime.ToString("yyyy-MM-ddTHH:mm:ss"));
                            Buffer.BlockCopy(s, 0, buff, p, s.Length);
                        }
                    }
                }
                // ファイルに書込
                System.IO.File.WriteAllBytes(Path, buff);
            }
            catch (Exception) {
                return false;
            }
```