【.NET】ラベルプリンター印刷をExcelファイルからPDF変換して行う

# はじめに
現在、サーバーリプレース作業をしている、その中でDelphi 5で作成されたラベルプリンター印刷用アプリケーションがある。※ちなみにDelphi 5は1999年に発売された。
数ヶ月前には、使用している別のラベルプリンターを使用したいとのことで久しぶりにDelphi 5を起動して、新しいラベルフォーマットを追加した。

サーバーリプレースに伴いDelphi 5で開発されたアプリケーションは.NET系に変更していく予定である。

# Spire.Office
ラベルフォーマットは、開発者以外でも変更や追加ができることを念頭にExcelで作成し、プレースホルダー文字列(実際の内容を後から挿入するために、とりあえず仮に確保した文字列)を埋め込む仕組みに変更した。

Excelの編集は以前、[EPPlus](https://github.com/JanKallman/EPPlus)というExcel操作ライブラリーを使用したことがあったので、これを使えばいいと当初は思った。
しかし、EPPlusおよびClosedXMLでは印刷およびPDF出力が出来ないことが分かり、他のライブラリーを探したところ、中国の[E-iceblue社](https://www.e-iceblue.com/)が提供するofficeの.NETコンポーネントのSpire.Office があり、印刷およびPDF出力をサポートしていることが分かった。
さらに機能制限がある無償版FreeSpire.Officeも提供している。

Spire.officeには、下記コンポーネントが含まれている。個別にダウンロードすることも出来る。

|コンポーネント|内容|無償版制限|
|:--|:--|:--|
|Spire.Doc|Word ファイルの作成・読み・書き・変換・印刷・エクスポート|Wordファイルの編集は、500段落と25テーブルに制限されています。 \\ PDFファイルの変換は最初の3ページのみ。|
|Spire.DocViewer|Word ファイルの表示・変換| |
|Spire.XLS|Excel ファイルの新規作成・編集・変換|Excelファイルの編集は、ブックごとに5シート、1シートあたり200行に制限されています。|
|Spire.Presentation|PowerPont ファイルの新規作成・編集・変換|プレゼンテーションスライドが10枚に制限されています。|
|Spire.PDF|PDFドキュメントの作成・読み・書き・編集|PDFファイルの編集は、10ページに制限されています。|
|Spire.PDFViewer|PDFドキュメントをストリーム、ファイル、バイト配列から読み込み| |
|Spire.DataExport|Word/Excel/Access、html、PDF、DBF、SQLスクリプト、csv等の形式にデータをエクスポート| |
|Spire.Barcode|バーコードの生成| |

ラベルプリンター印刷用フォーマットに使用する分には機能制限は問題ない。
NuGetからFreeSpire.officeを取得して使用する。

# ExcelファイルからPDF変換
Spire.XLSを使用してExcelファイルを読み込み、セルに埋め込まれたプレースホルダー「@」文字列を書き換えてPDF出力する。

```csharp
Workbook workbook = new Workbook();
CellRange cell = sheet.FindString("@PARTNO", false, false);
if (cell != null) cell.Text = "1234567890"; 
workbook.LoadFromFile("LBL00001.xlsx");
workbook.SaveToFile("LBL00001.pdf", Spire.Xls.FileFormat.PDF);
```

## 用紙サイズ
ラベルプリンターの用紙サイズ(例 55 x 40mm)とする場合、そのままだとExcelファイルのページレイアウトに用紙サイズが出てこない。よって、当初はB5サイズを指定して作成していた。
しかし、B5サイズを縮小した状態でラベルプリンターで印刷されるので、文字が綺麗(現状のものと同レベル)に表示されないということを同僚に指摘され、見直しすることになった。

Excelファイルのページレイアウトに対象の用紙サイズを出すには、対象のラベルプリンター用ドライバーをインストールして、コントロール パネルのデバイスとプリンターにて対象のラベルプリンターを「通常使うプリンターに設定」する。
これで位置や大きさのフォーマットし直して、PDF出力するとA4サイズのままとなってしまう。

ネットで調べて、「ConverterSetting.SheetFitToPage = true」を指定することで対象の用紙サイズでPDF出力されるようになった。

* [How to shrink Excel content to fit PDF page?](https://www.e-iceblue.com/forum/how-to-shrink-excel-content-to-fit-pdf-page-t6441.html)

```csharp
workbook.ConverterSetting.SheetFitToPage = true;
workbook.SaveToFile("LBL00001.pdf", Spire.Xls.FileFormat.PDF);
```

正しいExcelファイルが出来てしまえば、ラベルプリンターを「通常使うプリンターに設定」から外してもよい。修正する場合、ラベルプリンターを「通常使うプリンターに設定」にする必要がある。

### PDFサイズ変更
もう一つの案として、Spire.PDFを使用してPDFの用紙サイズ(A4)をカスタムサイズ(例 55 x 40mm)にリサイズする。
この方法ならラベルプリンターのドライバーを入れてない環境でも問題ない。

* [Need Help with Creating PDF file with Custom Page Size](https://www.e-iceblue.com/forum/need-help-with-creating-pdf-file-with-custom-page-size-t6798.html)
[Resize and Merge a PDF Document](https://www.e-iceblue.com/forum/resize-and-merge-a-pdf-document-t5538.html)
* [Set Graphic Overlay in PDF File](https://www.e-iceblue.com/Tutorials/Spire.PDF/Spire.PDF-Program-Guide/Print/How-to-print-PDF-document-in-C.html)

```csharp
PdfDocument doc = new PdfDocument();
doc.LoadFromFile("LBL00001.pdf");

PdfDocument newPdf = new PdfDocument();
SizeF size = new SizeF(55*3, 40*3);
PdfPageBase newPage = newPdf.Pages.Add(size, new PdfMargins(0));
PdfTemplate template = doc.Pages[0].CreateTemplate();
newPage.Canvas.DrawTemplate(template, PointF.Empty);
newPdf.SaveToFile("result.pdf");
```
※印刷は正常に出来る。

# PDFファイルを印刷
PDFファイルを印刷する。

* [How to print PDF document in C#](https://www.e-iceblue.com/Tutorials/Spire.PDF/Spire.PDF-Program-Guide/Print/How-to-print-PDF-document-in-C.html)
* [How to print PDF file number of times](https://www.e-iceblue.com/forum/how-to-print-pdf-file-number-of-times-t6201.html)

```csharp
PdfDocument pdf = new PdfDocument();
pdf.LoadFromFile("LBL00001.pdf");
pdf.PrintSettings.PrinterName = printerName; //ラベルプリンター名
pdf.PrintDocument.PrinterSettings.Copies = 1; //部数
pdf.Print();  
```

【2024/07/12追記】
FreeSpire.PDF の ver.10.2.0 から印刷メソッドがなくなりました。ver.8.6.0 は問題ない。

https://www.reddit.com/r/csharp/comments/1bl6y3q/so_what_are_we_using_to_print_pdfs_silently_now/

【2018/05/24追記】
使用しているラベルプリンターの標準サイズは68 x 58mm でプリンターのプロパティで用紙登録して55 x 40mm の印刷するように設定していた。
この状態で印刷すると、横向き指定していないのに勝手に横向きになる上にラベル2枚分のサイズで表示されてしまう。
Spire.PDFで、カスタムサイズを印刷する場合は「CustomHandleLandscape = true」を指定することで、横向きから縦向きにすることが出来る。※横向きの横向きは縦向きになる。

* [Landscape is useless](https://www.e-iceblue.com/forum/landscape-is-useless-t5502.html)

これで向きの問題は解決したが、次はラベル2枚分のサイズになってしまう問題がある。プリンターのプロパティの用紙のコンボボックスには標準サイズが先頭に登録されていて、次に今回用の用紙が表示されている。もしかしたら、先頭の用紙サイズしか対応できないのかもと用紙編集で標準サイズを68 x 58mm から 55 x 40mm に変更してみたところ、ラベル1枚分のサイズで印刷されるようになった。

【2019/06/08追記】
別のプリンターに対応することになりライブラリーのバージョンを上げたところ、「CustomHandleLandscape = true」とすると横向きに印刷されるようになったので、バグっぽいのが解消されたようです。

## PDFサイズの大きさ
Spire.XLSでPDF変換するとラベルプリンターの標準サイズ(68 x 58mm)でPDFが作成されてしまう。用紙編集でサイズ変更しても変わらないので、内部の標準サイズが適用されてしまうのだろう。
この状態で印刷すると、用紙サイズに合わせる設定だと少し小さめに印刷されてしまう、かといって実際のサイズの設定だと枠が消えて印刷されてしまう。

ExcelでPDFを生成すると正しいPDFサイズの大きさ(55 x 40mm)で作成される。そこで思いついたのが、下記サイトのようにPDFを重ね合わせる方法である。

* [Set Graphic Overlay in PDF File](https://www.e-iceblue.com/Tutorials/Spire.PDF/Spire.PDF-Program-Guide/Set-Graphic-Overlay-in-PDF-File.html)

Excelで枠とラベルのみのPDFを生成、動的な値とQRコードはSpire.XLSのPDF生成し、透明度無しで重ね合わせると、正しいPDFサイズの大きさ(55 x 40mm)の最終結果のPDFが生成できる。

```csharp
//PDFのオーバーレイ処理
PdfDocument doc1 = new PdfDocument();
doc1.LoadFromFile("LBL0001.pdf");

PdfDocument doc2 = new PdfDocument();
doc2.LoadFromFile("LBL0001_Base.pdf"));

PdfTemplate template = doc1.Pages[0].CreateTemplate();
foreach (PdfPageBase page in doc2.Pages)
{
    page.Canvas.DrawTemplate(template, PointF.Empty);
}

doc2.SaveToFile("result.pdf");
doc1.Close();
doc2.Close();
```

これにより、想定したラベルが印刷できるようになった。

```csharp
PdfDocument pdf = new PdfDocument();
pdf.LoadFromFile("result.pdf");
pdf.PrintSettings.PrinterName = printerName; //ラベルプリンター名
pdf.CustomHandleLandscape = true; //印刷向き 横向き
pdf.PageScaling = PdfPrintPageScaling.ActualSize; //実際の大きさ
pdf.PrintDocument.PrinterSettings.Copies = 1; //部数
pdf.Print();  
```

## 印字サイズ
【2019/06/07追記】
ブラザー P-touchで長さを変更したにもかかわらず、反映されないという現象が発生しました。テスト用にWinフォームにボタンのみ追加して試す分には、変更した長さでカットされるのですが、ASP.NETからだと初期の長さのまま。

PaperSizeを指定することで対応した。PaperSizeの指定はインチとなっている。
最新版にSpire.PDFにしたら、これまでの記述が古い形式になってしまっていた。

```csharp
PdfDocument pdf = new PdfDocument();
pdf.LoadFromFile(resultPath);
pdf.PrintSettings.PrinterName = printerName; //ラベルプリンター名
pdf.CustomHandleLandscape = isLandscape; //印刷向き 横向き

if(paperSize != null)
{
    //1インチ 25.4mm  例 24.0mm x 100/25.4 = 94  74.0mm x 100/25.4 = 291
    PaperSize paper = new PaperSize("Custom", (int)(paperSize.Width * 100 / 25.4), (int)(paperSize.Height * 100 / 25.4));
    paper.RawKind = (int)PaperKind.Custom;
    pdf.PrintSettings.PaperSize = paper;
}

pdf.PrintSettings.SelectSinglePageLayout(PdfSinglePageScalingMode.ActualSize, false); //実際の大きさ
pdf.PrintDocument.PrinterSettings.Copies = 1; //部数
pdf.Print();
```

## 小さい文字の印字
MSゴシック 6ptの文字を印字すると、Acrobat Readerでは印字した場合は細い線を保ったまま綺麗な文字で印字されるが、Spire.PDFだと細い線が汚く印字される。もちろんその状態でも問題なく読めるので運用上は困ることはない。綺麗な状態を知っていると残念なところである。

![品番.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14053/2cb7dc8f-98b0-db07-e0ec-3ec38e72da56.png)
上がAcrobat Readerによる印字、下がSpire.PDFによる印字

かといって、PDFの印刷部分をAcrobat Readerの呼び出しに切り替えるとしてもコマンドラインには部数の指定が無いわけです。
それ以外のPDFの印刷として、「[C# と DICOM](http://csdicom.blogspot.jp/2017/02/c-itextsharp-ghostscript.html)」の記事を参考に [ghostscript.net](https://github.com/jhabjan/Ghostscript.NET)も試してみましたが、Spire.PDFと同じ印字状態でした。運用上は困ることはないので、ここは妥協します。

【2019/06/07追記】
Acrobat ReaderはPDFの本家だけに小さいフォントでも印字はきれいです。何か仕組みが違うんですかね。
そのため、PDFの印刷部分をAcrobat Readerに切り替えることも考えたのですが、ASP.NETからだとProcess.Startメソッドを呼べないんですね。権限を変更すれば実行可能かも知れませんが。

>Acrobat Reader/Adobe Reader をサーバーにインストールし、ネットワーク経由で使用する方法は原則的に禁止されています。
[Acrobat Reader/Adobe Reader のサーバー利用について](https://helpx.adobe.com/jp/x-productkb/policy-pricing/cpsid_87204.html)

それよりも原則禁止されているってことで諦めました。
フォントをMEIRYOなどに変更することで多少は改善されますので、それで対応します。

## 部数
PrintDocument.PrinterSettings.Copiesプロパティで部数を指定できるとあるので、3をセットして実行しても、1部しか印刷されない。

* [How to print PDF file number of times](https://www.e-iceblue.com/forum/how-to-print-pdf-file-number-of-times-t6201.html)

よって、部数分繰り返して印刷する。

```csharp
for(int i=0; i<3; i++){
    PdfDocument pdf = new PdfDocument();
    pdf.LoadFromFile("result.pdf");
    pdf.PrintSettings.PrinterName = printerName; //ラベルプリンター名
    pdf.CustomHandleLandscape = true; //印刷向き 横向き
    pdf.PageScaling = PdfPrintPageScaling.ActualSize; //実際の大きさ
    pdf.Print();
}  
```

他の方法として、部数分のページ分複製したPDFを生成してから印刷する。
この方法では無償版の制限でPDFの編集は10ページまでになっている。

* [How to Duplicate a Page within a PDF Document in C#, VB.NET](https://www.e-iceblue.com/Tutorials/Spire.PDF/Spire.PDF-Program-Guide/Page-Setting/How-to-Duplicate-a-Page-within-a-PDF-Document-in-C-VB.NET.html)


# QRコードのセット
今回のラベルにはQRコードをセットする必要がある。
Spire.officeには、Spire.BarcodeというQRコードを生成するライブラリーが提供されているのだが、[QRコードのバージョン](https://www.keyence.co.jp/ss/products/autoid/codereader/basic2d_qr.jsp)指定はサポートしていない。
Spire.Barcodeでは、3つのパラメータ（QRCodeDataMode、QRCodeECL、テキストの長さ）に応じてカウントされるので、パラメータを変化させながらバージョンが異なるようになっている。

* [QR Code Settings](https://www.e-iceblue.com/forum/qr-code-settings-t6170.html)


今回、移植元QRコード生成ライブラリーでバージョンを指定してQRコードを生成しているため、バージョンを設定できる[QRCoder](https://github.com/codebude/QRCoder)をNuGetから取得して使用した。

下記コードでバージョン5の誤り訂正レベルQを使用した場合、qrCode.GetGraphicの第一引数は、pixelsPerModuleで、 1 なら 45 x 45 pixel、20 なら 900 x 900 pixel で生成される。

```csharp
CreateQRCode("qrcode.png", "1234567890123", "Q", 5);

private void CreateQRCode(string path, string content, string eccLevel, int version)
{
    QRCodeGenerator qrGenerator = new QRCodeGenerator();

    QRCodeGenerator.ECCLevel level = (QRCodeGenerator.ECCLevel)Enum.Parse(typeof(QRCodeGenerator.ECCLevel), eccLevel);
    QRCodeData qrCodeData = qrGenerator.CreateQrCode(content, level, requestedVersion: version);
    QRCode qrCode = new QRCode(qrCodeData);
    using (Bitmap bitmap = qrCode.GetGraphic(1))
    {
        bitmap.Save(path, ImageFormat.Png);
    }
}
```

これもプレースホルダー文字列を使用して、QRコードを埋め込むようにしている。

* [How to Align a Picture within a Cell in C#, VB.NET](https://www.e-iceblue.com/Tutorials/Spire.XLS/Spire.XLS-Program-Guide/Image/How-to-Align-a-Picture-within-a-Cell-in-C-VB.NET.html)

```csharp
string picPath = "qrcode.png"";
CellRange cell = sheet.FindString("@QRCODE", false, false);
if (range != null) ExcelPicture picture = sheet.Pictures.Add(cell.Row, cell.Column, picPath);

picture.LeftColumnOffset = 5;
picture.TopRowOffset = 5;
```

# ASP.NETでバックグラウンド印刷
今回のラベルプリンターへの印刷処理は、ASP.NETのWebAPIで行っている。
WebAPIを呼ぶ際にタイムアウトを4秒としており、印刷までの一連処理そのままだと4秒超えでタイムアウトしてしまう。

その解消方法をネットで「ASP.NET 重い処理」で検索したところ、下記サイトを見つけた。

* [ASP.NET MVC5 とかでバックグラウンド処理（ QueueBackgroundWorkItem 編）](http://beachside.hatenablog.com/entry/2016/01/30/163500)
* [ASP.NET 4.5.2 で追加されたバックグラウンド処理 API を使って待つ必要のない非同期処理を丸投げする](http://blog.shibayan.jp/entry/20140902/1409618887)

QueueBackgroundWorkItem はASP.NET 4.5.2 で追加されたバックグラウンド処理 APIで待つ必要のない非同期処理を行える。

印刷は待つ必要が無い処理なので、処理を呼び出すと印刷結果に関わらず処理を戻すので、これによりタイムアウトにならなくて済むようになった。

## 嵌ったこと
印刷処理が終わった後に、WebAPIを再度呼ぶと処理が遅くなる現象が発生した。当初、BINフォルダ内にPDFファイルを出力していたのだが、これが原因であった。

ASP.NETは仕組み上、BINフォルダ内のファイルの更新を検知すると、アプリケーションを再起動するようになっており、アプリケーションが再起動すると、初期状態になりASP.NETの呼出しに時間がかかってしまうのです。
本来、BINフォルダにはDLLなど滅多に更新されないファイルのみを配置し、頻繁に更新される出力ファイル(PDF)は、別フォルダに作成する必要がありました。

また、QRコードの画像(qrcode.png)を削除しようとしたら、ASP.NETのw3wp.exeが使用中で削除できなかった(上書きも駄目)ため、出力フォルダ内のQRコードの画像やPDF出力のファイル名には日時を追加するようにして回避した。

【2018/05/24追記】
印刷が終わったタイミングで、1日経過した出力したファイルを削除する仕組みを用意した。1日1回はIIS Resetする運用なので使用中状態は遅くてもそこで解除される。

```csharp
DeleteByDays(path, 1);

private void DeleteByDays(string path, int days)
{
    var target = DateTime.Now.AddDays(-days);

    try
    {
        Directory.GetFiles(path)
            .Where(f => File.GetCreationTime(f) < target)
            .ToList()
            .ForEach(f => File.Delete(f));
    }
    catch
    {
        //何もしない
    }
}
```

# 最後に
最近、技術関連でネット検索していると中国ブログが引っかかる。
今回のSpire.Officeも中国のE-iceblue社で素晴らしいライブラリーを提供してもらい助かっている。中国のOSSライブラリーをサポートすると漢字圏なので、日本語が文字化けするということが発生しない。