C# - 文書洗浄 - タイムスタンプ更新とプロパティ削除

## はじめに

公開文書、外部送付ファイルは、スッキリ洗浄してから提供することがありました。
サンプルソースは、処理毎で２つに分離しましたが、ひとつにまとめてツール化していました。

### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8
- WPF - .NET Framework 4.8
- WPF - .NET 8

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、基本的に C# 7.3 機能範囲で記述しています。
:::


## タイムスタンプ更新

複数ファイルの場合、タイムスタンプがそれぞれ異なる状態よりも、揃っていたほうが良いと思い、全て同一日時のタイムスタンプとしていました。

::: note
休日、もしくは、定時勤務時間以外のタイムスタンプは、それ自体が勤務情報として認知されることもあります。
:::

### サンプルコード

```CSharp
var folder = "FOLDER_PATH";                // TODO
var today = DateTime.Today.AddHours(10);   // 本日の 10:00:00
SetTimeStamp(folder, today);
```
```CSharp
// フォルダ下のファイルについて、タイムスタンプを更新
private void SetTimeStamp(string target, DateTime today)
{
  // サブフォルダ下も処理する場合はコメントを外す
  // var folders = Directory.GetDirectories(target);
  // if (folders != null)
  // {
  //   foreach(var folder in folders)
  //   {
  //     SetTimeStamp(folder, today);
  //   }
  // }

  // フォルダ内のファイル取得
  var files = Directory.GetFiles(target);
  if (files != null)
  {
    foreach (var file in files)
    {
      // タイムスタンプ更新
      File.SetCreationTime(file, today);
      File.SetLastWriteTime(file, today);
    }
  }
}
```

## プロパティ削除

文書プロパティに個人情報などが格納されている場合、クリアしたほうが良いと思います。
本記事では、下記文書を対象とします。

- Microsoft Office 文書（Word, Excel, Power Point）
- PDF 文書

Microsoft Office 文書は、[NuGet Gallery | DocumentFormat.OpenXml](https://www.nuget.org/packages/documentformat.openxml) を利用します。
PDF 文書は、AGPL は避けて、[NuGet Gallery | PDFsharp](https://www.nuget.org/packages/pdfsharp/) を利用します。
PDFSharp は Version 1.50.5147 までが .NET Framework 用で、Version 6.0.0 以降が .NET 用となっています。.NET Framework で利用する場合は、バージョン指定して取得しましょう。

- .NET
  - `PM> NuGet\Install-Package DocumentFormat.OpenXml`
  - `PM> NuGet\Install-Package PDFsharp`
- .NET Framework
  - `PM> NuGet\Install-Package DocumentFormat.OpenXml`
  - `PM> NuGet\Install-Package PdfSharp -Version 1.50.5147`


### Microsoft Office 文書

エクスプローラ、プロパティで参照できる下記情報の削除を行います。

![office.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/4bf449f4-2882-46b2-98ac-bfaecf2b4d39.png)

Word、Excel、Power Point で利用するクラスが異なりますが、それ以外のコードは同一です。

| 文書           | 利用するクラス           |
| -------------- | ---------------------- |
| Word           | WordprocessingDocument |
| Excel          | SpreadsheetDocument    |
| Power Poinet   | PresentationDocument   |

Word サンプルコードを直接掲載して、それ以外はアコーディオンで掲載します。
サンプルコードで、削除不要なプロパティがありましたら、コメントアウトしてください。


```CSharp
using DocumentFormat.OpenXml.Packaging;
```
```CSharp
// Word 文書プロパティ削除
private void CleanupWordProperties(string file)
{
  // Word ファイルを開く
  using (var document = WordprocessingDocument.Open(file, true))
  {
    // ビルトインプロパティ
    var coreProperties = document.PackageProperties;
    if (coreProperties != null)
    {
      // プロパティを null に設定して削除
      coreProperties.Title = null;           // タイトル
      coreProperties.Subject = null;         // 件名
      coreProperties.Keywords = null;        // タグ
      coreProperties.Category = null;        // 分類項目
      coreProperties.Description = null;     // コメント
      coreProperties.Creator = null;         // 作成者
      coreProperties.LastModifiedBy = null;  // 最終保存者
      coreProperties.Revision = null;        // 改定番号 
      coreProperties.Modified = null;        // 最終更新日時
      coreProperties.LastModifiedBy = null;  // 前回保存者
      coreProperties.Created = null;         // コンテンツの作成日時
      coreProperties.Modified = null;        // 前回保存日時
      coreProperties.LastPrinted = null;     // 前回印刷日
    }
    // 拡張プロパティ
    var extendedProperties = document.ExtendedFilePropertiesPart?.Properties;
    if (extendedProperties != null)
    {
      // プロパティを null に設定して削除
      extendedProperties.Company = null;      // 会社
      extendedProperties.Manager = null;      // マネージャ
      extendedProperties.TotalTime = null;    // 編集総時間
      extendedProperties.Save();
    }
  }
}
```

::: note
コンテンツの作成日時、前回保存日時を削除した場合、それぞれ、対象ファイルのタイムスタンプ CreationTime, LastWriteTime を参照するみたいです。
前述、タイムスタンプ更新とあわせて対処することで、表示情報のリセットは可能です。
:::


<details><summary>Excel 文書プロパティ削除</summary>

```CSharp
// Excel 文書プロパティ削除
private void CleanupExcelProperties(string file)
{
  // Excel ファイルを開く
  using (var document = SpreadsheetDocument.Open(file, true))
  {
    // ビルトインプロパティ
    var coreProperties = document.PackageProperties;
    if (coreProperties != null)
    {
      // プロパティを null に設定して削除
      coreProperties.Title = null;           // タイトル
      coreProperties.Subject = null;         // 件名
      coreProperties.Keywords = null;        // タグ
      coreProperties.Category = null;        // 分類項目
      coreProperties.Description = null;     // コメント
      coreProperties.Creator = null;         // 作成者
      coreProperties.LastModifiedBy = null;  // 最終保存者
      coreProperties.Revision = null;        // 改定番号 
      coreProperties.Modified = null;        // 最終更新日時
      coreProperties.LastModifiedBy = null;  // 前回保存者
      coreProperties.Created = null;         // コンテンツの作成日時
      coreProperties.Modified = null;        // 前回保存日時
      coreProperties.LastPrinted = null;     // 前回印刷日
    }
    // 拡張プロパティ
    var extendedProperties = document.ExtendedFilePropertiesPart?.Properties;
    if (extendedProperties != null)
    {
      // プロパティを null に設定して削除
      extendedProperties.Company = null;      // 会社
      extendedProperties.Manager = null;      // マネージャ
      extendedProperties.TotalTime = null;    // 編集総時間
      extendedProperties.Save();
    }
  }
}
```

</details>


<details><summary>Power Point 文書プロパティ削除</summary>

```CSharp
// Power Point 文書プロパティ削除
private void CleanupPowerPointProperties(string file)
{
  // Power Point ファイルを開く
  using (var document = PresentationDocument.Open(file, true))
  {
    // ビルトインプロパティ
    var coreProperties = document.PackageProperties;
    if (coreProperties != null)
    {
      // プロパティを null に設定して削除
      coreProperties.Title = null;           // タイトル
      coreProperties.Subject = null;         // 件名
      coreProperties.Keywords = null;        // タグ
      coreProperties.Category = null;        // 分類項目
      coreProperties.Description = null;     // コメント
      coreProperties.Creator = null;         // 作成者
      coreProperties.LastModifiedBy = null;  // 最終保存者
      coreProperties.Revision = null;        // 改定番号 
      coreProperties.Modified = null;        // 最終更新日時
      coreProperties.LastModifiedBy = null;  // 前回保存者
      coreProperties.Created = null;         // コンテンツの作成日時
      coreProperties.Modified = null;        // 前回保存日時
      coreProperties.LastPrinted = null;     // 前回印刷日
    }
    // 拡張プロパティ
    var extendedProperties = document.ExtendedFilePropertiesPart?.Properties;
    if (extendedProperties != null)
    {
      // プロパティを null に設定して削除
      extendedProperties.Company = null;      // 会社
      extendedProperties.Manager = null;      // マネージャ
      extendedProperties.TotalTime = null;    // 編集総時間
      extendedProperties.Save();
    }
  }
}
```

</details>

### PDF 文書

PDF は提供時に PDF 化していたこともあり、文書プロパティをあまり気にしていませんでしたが、Microsoft Office 文書と同様に対処すべきだったと思い、本記事では掲載することにしました。
PDF編集ソフト（本来は Acrobat Pro で確認すべきですがライセンスを持っていないので、CubePDF Utility を利用）で参照できる下記情報の削除を行います。

![pdf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/82a48cad-79e4-489c-bab5-c5c5726b965b.png)

```CSharp
using PdfSharp.Pdf.IO;
```
```CSharp
// PDF 文書プロパティ削除
private void CleanupPdfProperties(string target)
{
  // PDF ファイルを開く
  using (var document = PdfReader.Open(target, PdfDocumentOpenMode.Modify))
  {
    // プロパティを string.Empty に設定して削除
    document.Info.Title = string.Empty;       // タイトル
    document.Info.Author = string.Empty;      // 作成者
    document.Info.Subject = string.Empty;     // サブタイトル
    document.Info.Keywords = string.Empty;    // キーワード

    // PDF編集ソフトによっては、下記も参照できるので、必要に応じてコメントアウトを外す
    // document.Info.Comment = string.Empty;
    // document.Info.Creator = string.Empty;
    // document.Info.CreationDate = DateTime.Now;
    // document.Info.ModificationDate = DateTime.Now;

    document.Save(target);
  }
}
```


### 全体処理
フォルダ下の対象文書を処理する全体処理のソースコードを掲載します。

```CSharp
var folder = "FOLDER_PATH";                // TODO
ClearDocumentProperties(folder);
```
```CSharp
// フォルダ下の文書について、文書プロパティを削除
private void ClearDocumentProperties(string target)
{
  // サブフォルダ下も処理する場合はコメントを外す
  // var folders = Directory.GetDirectories(target);
  // if (folders != null)
  // {
  //   foreach(var folder in folders)
  //   {
  //     ClearDocumentProperties(folder);
  //   }
  // }

  // フォルダ内のファイル取得
  var files = Directory.GetFiles(target);
  if (files != null)
  {
    foreach (var file in files)
    {
      // 拡張子を利用した分岐
      var fileType = Path.GetExtension(file)?.ToLower();
      switch (fileType)
      {
        case ".docx":
          CleanupWordProperties(file);
          break;
        case ".xlsx":
          CleanupExcelProperties(file);
          break;
        case ".pptx":
          CleanupPowerPointProperties(file);
          break;
        case ".pdf":
          CleanupPdfProperties(file);
          break;
      }
    }
  }
}
```

.NET 8 で利用可能な switch 式を用いると、こんな感じですかね。

```CSharp
// 拡張子を利用した分岐 - switch 式
var fileType = Path.GetExtension(file)?.ToLower();
Action<string>? action = fileType switch
{
  ".docx" => CleanupWordProperties,
  ".xlsx" => CleanupExcelProperties,
  ".pptx" => CleanupPowerPointProperties,
  ".pdf" => CleanupPdfProperties,
  _ => null
};
// 拡張子に基づく action を実行
action?.Invoke(file);
```


::: note
.NET Framework でも、[Visual Studio と C#、および、フレームワーク（プラットフォーム）の関係](https://qiita.com/chai0917/items/16d65b899c734e7417b7) で記載した C# バージョン設定で 8.0 以降にすれは、上記 switch 式を利用できます。
※null 許容参照型の明示は不要なので、下記修正は必要です。
```CSharp
// Action<string>? を Action<string> に修正
Action<string> action = fileType switch
```
:::