PrintOnWindows.cs
// 
// このコードは、DioDocs for PDF のサンプルの一部として提供されています。
// © MESCIUS inc. All rights reserved.
// 
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Drawing;
using System.Drawing.Printing;
using System.ComponentModel;
using System.Numerics;
using System.IO;

using GrapeCity.Documents.Common;
using GrapeCity.Documents.Pdf;
using GrapeCity.Documents.Pdf.Renderer;
using GrapeCity.Documents.Drawing;
using GrapeCity.Documents.Text;
using GrapeCity.Documents.Imaging.Windows;

using STG = GrapeCity.Documents.DX.Storage;
using D2D = GrapeCity.Documents.DX.Direct2D;
using D3D = GrapeCity.Documents.DX.Direct3D11;
using DW = GrapeCity.Documents.DX.DirectWrite;
using DXGI = GrapeCity.Documents.DX.DXGI;
using WIC = GrapeCity.Documents.DX.WIC;
using DX = GrapeCity.Documents.DX;


namespace GcPdfWeb.Samples
{
    // このサンプルでは、Direct2D を使用して、Windows で GcPdfDocument を印刷する方法を示しています。
    // 印刷を可能にする GcD2DBitmap クラスと GcD2DGraphics クラスは、
    // GrapeCity.Documents.Imaging.Windows パッケージ内にあります。
    //
    // このサンプルには、印刷機能を実装した実用的なコードがいくつか含まれており、
    // アプリケーションでそのまま使用することができます。
    // - GcPdfDocumentPrintExt クラスは、GcPdfDocument を印刷するための便利な拡張メソッドを提供します。
    // - GcPdfPrintManager クラスは、GcPdfDocumentPrintExt で使用される印刷サービスを実装しています。
    // - PageScaling 列挙体は、印刷時にページを拡大縮小する方法を指定します。
    //
    // オンラインのデモブラウザでは、このサンプルは単純な1ページの PDF ドキュメントを生成して
    // それを表示するだけです。生成された PDF をローカルシステムのプリンターで印刷する実際のコードは
    // サンプルに含まれていますが、オンラインデモ用に一部の処理を実行しないようにしています。
    //
    // 実際に PDF を印刷するには、サンプルをダウンロードし、GcPdfPrintManager.Print() メソッドを
    // 呼び出すようにサンプルコード内の条件を編集して、サンプルを実行してください。デフォルトのプリンターで
    // サンプル PDF が印刷されるはずです。必要に応じて、GcPdfPrintManager クラスと PrinterSettings クラスの
    // プロパティを設定または調整します。
    public class PrintOnWindows
    {
        public void CreatePDF(Stream stream)
        {
            GcPdfDocument doc = new GcPdfDocument();

            Common.Util.AddNote(
                "このサンプルには、Direct2D を使用して Windows で PDF を印刷する機能を実装した GcPdfPrintManager クラスと、" +
                "GcPdfDocument クラスに便利な Print() メソッドを追加して拡張した GcPdfDocumentPrintExt クラスという、" +
                "すぐに使用可能なクラスが含まれています。完全なソースコードが含まれているので、コピーして、" +
                "印刷のためのアプリケーションにそのまま使用することができます。試してみるには、このサンプルをダウンロードし、" +
                "Print() メソッドを呼び出すコードのブロック('if (false)' 条件で除外)を有効にして、Windows マシンで" +
                "ビルドの上実行してください。PDF がデフォルトのプリンターで印刷されます。",
                doc.NewPage());

            // Windows で PDF を印刷する場合は、このブロックを有効にしてください。
#pragma warning disable CS0162
            if (false)
            {
                GcPdfPrintManager pm = new GcPdfPrintManager();
                pm.Doc = doc;
                pm.PrinterSettings = new PrinterSettings();
                // デフォルトの設定を使用しない場合は、プリンター名やその他の設定を行います。
                // pm.PrinterSettings.PrinterName = "my printer";
                pm.PageScaling = PageScaling.FitToPrintableArea;
                pm.Print();
            }
#pragma warning restore CS0162

            // PDF を保存します。
            doc.Save(stream);
        }
    }

    /// <summary>
    /// 印刷時のページの拡大縮小を指定します。
    /// </summary>
    public enum PageScaling
    {
        /// <summary>
        /// 用紙に合わせるために、必要に応じてページを拡大または縮小します。
        /// </summary>
        FitToPaper,
        /// <summary>
        /// 印刷可能なページ数に合わせて、必要に応じてページを拡大または縮小します。
        /// </summary>
        FitToPrintableArea,
    }

    /// <summary>
    /// Direct2D を使用して、Windows で <see cref="GcPdfDocument"/> を印刷するための
    /// 拡張メソッドを提供します。
    /// </summary>
    public static class GcPdfDocumentPrintExt
    {
        /// <summary>
        /// PDF をデフォルトのプリンターで印刷します。
        /// </summary>
        /// <param name="doc">印刷する <see cref="GcPdfDocument"/></param>
        /// <param name="outputRange">印刷するページの範囲(<see langword="null"/> は全ページ)</param>
        public static void Print(this GcPdfDocument doc, OutputRange outputRange = null)
        {
            Print(doc, null, outputRange);
        }

        /// <summary>
        /// 指定したプリンター設定で PDF を印刷します。
        /// </summary>
        /// <param name="doc">印刷する <see cref="GcPdfDocument"/></param>
        /// <param name="printerSettings">対象のプリンターやその他設定などを指定する <see cref="PrinterSettings"/>(<see langword="null"/> の場合、デフォルトのプリンターを使用)</param>
        /// <param name="outputRange">印刷するページの範囲(<see langword="null"/> の場合、<paramref name="printerSettings"/> で指定された範囲を使用)</param>
        public static void Print(this GcPdfDocument doc, PrinterSettings printerSettings, OutputRange outputRange = null)
        {
            GcPdfPrintManager pm = new GcPdfPrintManager();
            pm.Doc = doc;
            pm.PrinterSettings = printerSettings;
            pm.OutputRange = outputRange;
            pm.Print();
        }
    }

    /// <summary>
    /// Direct2D を使用して、Windows で <see cref="GcPdfDocument"/> オブジェクトを印刷できるようにする
    /// プロパティとメソッドを提供します。
    /// </summary>
    public class GcPdfPrintManager
    {
        #region Data members
        private const PageScaling c_DefPageScaling = PageScaling.FitToPaper;

        private PrinterSettings _printerSettings;
        private PageSettings _pageSettings;
        private OutputRange _outputRange;
        private string _printJobName;
        private PageScaling _pageScaling = PageScaling.FitToPaper;
        private bool _autoRotate = true;
        private RenderingCache _renderingCache;
        private bool? _autoCollate;
        private GcPdfDocument _doc;
        #endregion

        #region Protected
        protected bool OnLongOperation(double complete, bool canCancel)
        {
            if (LongOperation != null)
                return LongOperation.Invoke(complete, canCancel);
            return true;
        }
        #endregion

        #region Public properties
        /// <summary>
        /// PrinterSettings.Collate プロパティが <see cref="GcPdfPrintManager"/> またはプリンタードライバーの
        /// どちらで処理されるべきかを示す値を取得または設定します。
        /// デフォルトではこのプロパティは null で、GcPdfPrintManager はプリンタードライバーが
        /// 部単位での印刷をサポートしているかどうかを判断しようとします。
        /// </summary>
        public bool? AutoCollate
        {
            get { return _autoCollate; }
            set { _autoCollate = value; }
        }

        /// <summary>
        /// 印刷する <see cref="GcPdfDocument"/> オブジェクトを取得または設定します。
        /// </summary>
        public GcPdfDocument Doc
        {
            get { return _doc; }
            set { _doc = value; }
        }

        /// <summary>
        /// 印刷時に用紙に合うようにページを自動で回転させるかどうかを示す値を取得または設定します。
        /// <para>デフォルトは <see langword="true"/> です。</para>
        /// </summary>
        public bool AutoRotate
        {
            get { return _autoRotate; }
            set { _autoRotate = value; }
        }

        /// <summary>
        /// 印刷パラメータを定義する <see cref="System.Drawing.Printing.PrinterSettings"/> オブジェクトを取得または設定します。
        /// <see langword="null"/> の場合、デフォルトのプリンター設定が使用されます。
        /// </summary>
        public PrinterSettings PrinterSettings
        {
            get { return _printerSettings; }
            set { _printerSettings = value; }
        }

        /// <summary>
        /// ページ設定(ページサイズ、向きなど)を定義する <see cref="System.Drawing.Printing.PageSettings"/> オブジェクトを取得または設定します。
        /// <see langword="null"/> の場合、デフォルトのページ設定が使用されます。
        /// </summary>
        public PageSettings PageSettings
        {
            get { return _pageSettings; }
            set { _pageSettings = value; }
        }

        /// <summary>
        /// 印刷時にページをどのように拡大縮小するかを示す値を取得または設定します。
        /// <para>
        /// デフォルトは <see cref="PageScaling.FitToPaper"/> です。
        /// </para>
        /// </summary>
        [DefaultValue(c_DefPageScaling)]
        public PageScaling PageScaling
        {
            get { return _pageScaling; }
            set { _pageScaling = value; }
        }

        /// <summary>
        /// 印刷するページの範囲を指定する <see cref="Common.OutputRange"/> オブジェクトを取得または設定します。
        /// <see langword="null"/> の場合、<see cref="PrinterSettings"/> で指定された範囲が使用されます。
        /// </summary>
        public OutputRange OutputRange
        {
            get { return _outputRange; }
            set { _outputRange = value; }
        }

        /// <summary>
        /// 印刷ジョブの名前を取得または設定します。
        /// </summary>
        public string PrintJobName
        {
            get { return _printJobName; }
            set { _printJobName = value; }
        }

        /// <summary>
        /// レンダリング中に使用する <see cref="Pdf.RenderingCache"/> オブジェクトを取得または設定します。
        /// <see langword="null"/> も指定可能です。
        /// </summary>
        public RenderingCache RenderingCache
        {
            get { return _renderingCache; }
            set { _renderingCache = value; }
        }
        #endregion

        #region Public static
        /// <summary>
        /// プリンターの機種によっては、印刷のサブシステムで正しく扱えないものがあります。
        /// このようなプリンターで <see cref="GcPdfPrintManager"/>を使用しようとすると、
        /// GcPdfPrintManager が制御できないという理由により失敗します。
        /// このメソッドを使用して、特定のプリンターが使用可能かどうかを確認します。
        /// なお、もし失敗しても、それは GcPdfPrintManager によるものではありません。
        /// </summary>
        /// <param name="printerName">確認するプリンター</param>
        /// <returns>プリンターが使用可能な場合は <see langword="true"/>、それ以外の場合は <see langword="false"/></returns>
        public static bool TestPrinter(string printerName)
        {
            try
            {
                var ps = new PrinterSettings() { PrinterName = printerName };
                IntPtr hDevMode = ps.GetHdevmode();
                return true;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        /// 2つの値を入れ替えます。
        /// </summary>
        /// <typeparam name="T">値のタイプ</typeparam>
        public static void Swap<T>(ref T x, ref T y)
        {
            T temp = x;
            x = y;
            y = temp;
        }

        /// <summary>
        /// 用紙の回転角度を返します。
        /// </summary>
        /// <param name="pageRotationAngle">ページの回転角度(単位:度)</param>
        /// <returns>用紙の回転角度(単位:度)</returns>
        public static int PaperRotationAngle(int pageRotationAngle)
        {
            if (pageRotationAngle == 90)
                return 270;
            else if (pageRotationAngle == 270)
                return 90;
            else if (pageRotationAngle == 0)
                return 0;
            else
                throw new ArgumentOutOfRangeException("有効な値は 90 と 270 です。");
        }

        /// <summary>
        /// 用紙に合うようにページを回転させる必要があるかどうかをテストします。
        /// </summary>
        /// <param name="paperSize">用紙のサイズ</param>
        /// <param name="pageSize">ページサイズ</param>
        /// <returns>ページを回転させる必要がある場合は <b>true</b>、それ以外の場合は <b>false</b></returns>
        public static bool ShouldRotate(SizeF paperSize, SizeF pageSize)
        {
            return (paperSize.Width > paperSize.Height) != (pageSize.Width > pageSize.Height);
        }

        /// <summary>
        /// <see cref="Size"/> 構造体の幅と高さを入れ替えます(90 度回転させます)。
        /// </summary>
        /// <param name="s">回転させる <see cref="Size"/></param>
        /// <returns>幅と高さを入れ替えて新しく作成された <see cref="Size"/></returns>
        public static SizeF RotateSize(SizeF s)
        {
            float t = s.Width;
            s.Width = s.Height;
            s.Height = t;
            return s;
        }

        /// <summary>
        /// 用紙サイズとその中の印刷可能領域を指定された角度だけ回転させます。
        /// </summary>
        /// <param name="angle">反時計回りの回転角度(有効な値は <b>90</b> と <b>270</b>)</param>
        /// <param name="paperSize">用紙サイズ</param>
        /// <param name="printableArea">印刷可能領域</param>
        public static void RotatePaper(int angle,
            ref SizeF paperSize, ref RectangleF printableArea)
        {
            if (angle == 90)
            {
                printableArea = new RectangleF(
                    printableArea.Top,
                    paperSize.Width - printableArea.Right,
                    printableArea.Height, printableArea.Width);
                paperSize = RotateSize(paperSize);
            }
            else if (angle == 270)
            {
                printableArea = new RectangleF(
                    paperSize.Height - printableArea.Bottom,
                    printableArea.Left,
                    printableArea.Height, printableArea.Width);
                paperSize = RotateSize(paperSize);
            }
            else if (angle != 0)
                throw new ArgumentOutOfRangeException("有効な値は 90 と 270 です。");
        }
        #endregion

        #region Public
        /// <summary>
        /// ドキュメントを印刷します。
        /// </summary>
        public void Print()
        {
            PrinterSettings printerSettings = _printerSettings;
            if (printerSettings == null)
                printerSettings = new PrinterSettings();
            bool autoCollate;
            if (_autoCollate.HasValue)
                autoCollate = _autoCollate.Value;
            else
            {
                // プリンターが DM_COLLATE プロパティと DM_COPIES プロパティをサポートしているかどうかを確認します。
                IntPtr hdm = printerSettings.GetHdevmode();
                IntPtr lhdm = GlobalLock(hdm);
                DEVMODE dm = (DEVMODE)Marshal.PtrToStructure(lhdm, typeof(DEVMODE));
                if ((dm.dmFields & DM.DM_COLLATE) != 0 && (dm.dmFields & DM.DM_COPIES) != 0)
                    autoCollate = false;
                else
                    autoCollate = true;
                GlobalUnlock(hdm);
                GlobalFree(hdm);
            }

            int printCopies = printerSettings.Copies;
            if (autoCollate)
            {
                printCopies = printerSettings.Copies;
                printerSettings.Copies = 1;
            }
            else
            {
                printCopies = 1;
            }
            PageSettings pageSettings;
            IntPtr hDevMode = printerSettings.GetHdevmode();
            if (_pageSettings != null)
            {
                pageSettings = _pageSettings;
                pageSettings.CopyToHdevmode(hDevMode);
                printerSettings.SetHdevmode(hDevMode);
            }
            else
            {
                pageSettings = printerSettings.DefaultPageSettings;
            }
            // * 0.96 - DIPに変換するために使用します。
            RectangleF printableArea = new RectangleF(
                pageSettings.PrintableArea.X * 0.96f,
                pageSettings.PrintableArea.Y * 0.96f,
                pageSettings.PrintableArea.Width * 0.96f,
                pageSettings.PrintableArea.Height * 0.96f);
            PaperSize paperSize = pageSettings.PaperSize;
            float printerPageWidthPx = paperSize.Width * 0.96f;
            float printerPageHeightPx = paperSize.Height * 0.96f;
            if (pageSettings.Landscape)
            {
                Swap(ref printerPageWidthPx, ref printerPageHeightPx);
                printableArea = new RectangleF(printableArea.Y, printableArea.X, printableArea.Height, printableArea.Width);
            }
            //
            OutputRange outputRange = _outputRange;
            if (outputRange == null)
            {
                int pMin, pMax;
                switch (printerSettings.PrintRange)
                {
                    case PrintRange.SomePages:
                        pMin = Math.Min(Math.Max(printerSettings.FromPage, 1), _doc.Pages.Count);
                        pMax = Math.Min(Math.Max(printerSettings.ToPage, 1), _doc.Pages.Count);
                        break;
                    default:
                        pMin = 1;
                        pMax = _doc.Pages.Count;
                        break;
                }
                outputRange = new OutputRange(pMin, pMax);
            }
            //
            IntPtr lockedDevMode = GlobalLock(hDevMode);
            DEVMODE devMode = (DEVMODE)Marshal.PtrToStructure(lockedDevMode, typeof(DEVMODE));
            int dpi = 150;
            if ((devMode.dmFields & DM.DM_PRINTQUALITY) != 0 && devMode.dmPrintQuality > 0)
            {
                dpi = devMode.dmPrintQuality;
            }
            else if ((devMode.dmFields & DM.DM_YRESOLUTION) != 0)
            {
                dpi = devMode.dmYResolution;
            }

            STG.ComStream jobPrintTicketStream = null;
            try
            {
                jobPrintTicketStream = CreatePrintTicketFromDevMode(printerSettings.PrinterName, lockedDevMode, devMode.dmSize + devMode.dmDriverExtra);

                Print(printerSettings.PrinterName,
                    jobPrintTicketStream,
                    printerPageWidthPx,
                    printerPageHeightPx,
                    printableArea,
                    dpi,
                    autoCollate,
                    printCopies,
                    printerSettings.Collate,
                    outputRange);
            }
            finally
            {
                if (jobPrintTicketStream != null)
                    jobPrintTicketStream.Dispose();
                GlobalUnlock(hDevMode);
                GlobalFree(hDevMode);
            }
        }
        #endregion

        #region Private
        private STG.ComStream CreatePrintTicketFromDevMode(string printerName, IntPtr lockedDevMode, int devModeSize)
        {
            IntPtr istream = IntPtr.Zero;
            DX.HResult hr = CreateStreamOnHGlobal(IntPtr.Zero, true, ref istream);
            hr.CheckError();
            if (istream == IntPtr.Zero)
            {
                // 以下を参照しました。
                // https://msdn.microsoft.com/ru-ru/library/windows/desktop/aa378980(v=vs.85).aspx
                // 安全のため、istream を null にすることはできないようです。
                throw new InvalidOperationException();
            }

            STG.ComStream result = new STG.ComStream(istream);
            IntPtr hProvider = IntPtr.Zero;
            try
            {
                hr = PTOpenProvider(printerName, 1, ref hProvider);
                hr.CheckError();

                hr = PTConvertDevModeToPrintTicket(hProvider, devModeSize, lockedDevMode, 2 /* kPTJobScope */, istream);
                hr.CheckError();

                return result;
            }
            catch
            {
                result.Dispose();
                throw;
            }
            finally
            {
                if (hProvider != IntPtr.Zero)
                    PTCloseProvider(hProvider);
            }
        }

        private void AlignInRect(RectangleF rect, float width, float height,
            out float offsX, out float offsY, out float scaleX, out float scaleY)
        {
            float k = width / height;
            PointF offset = new PointF();
            if (rect.Width / rect.Height > k)
            {
                float contentWidth = k * rect.Height;
                k = rect.Height / height;
                offset.X = (rect.Width - contentWidth) / 2;
            }
            else
            {
                float contentHeight = rect.Width / k;
                k = rect.Width / width;
                offset.Y = (rect.Height - contentHeight) / 2;
            }

            scaleX = k;
            scaleY = k;
            offsX = offset.X + rect.X;
            offsY = offset.Y + rect.Y;
        }

        private void DrawContent(
            GcDXGraphics graphics,
            D2D.Device device,
            ref GcD2DBitmap bitmap,
            int pageIndex,
            int landscapeAngle,
            float printerDpi,
            SizeF paperSize,
            RectangleF printableArea,
            RenderingCache renderingCache,
            FontCache fontCache)
        {
            Page page = _doc.Pages[pageIndex];

            SizeF pageSize = page.GetRenderSize(graphics.Resolution, graphics.Resolution);

            if (PageScaling == PageScaling.FitToPaper)
                printableArea = new RectangleF(0, 0, paperSize.Width, paperSize.Height);

            RectangleF alignRect;
            int rotationAngle;
            if (AutoRotate && ShouldRotate(printableArea.Size, pageSize))
            {
                rotationAngle = landscapeAngle;
                alignRect = new RectangleF(0, 0, printableArea.Height, printableArea.Width);
            }
            else
            {
                rotationAngle = 0;
                alignRect = new RectangleF(0, 0, printableArea.Width, printableArea.Height);
            }
            AlignInRect(alignRect,
                pageSize.Width,
                pageSize.Height,
                out float offsX,
                out float offsY,
                out float scaleX,
                out float scaleY);
            Matrix3x2 m;
            switch (rotationAngle)
            {
                case 0:
                    m = new Matrix3x2(scaleX, 0, 0, scaleY, printableArea.X + offsX, printableArea.Y + offsY);
                    break;
                case 90:
                    m = Matrix3x2.Multiply(Matrix3x2.CreateScale(scaleX, scaleY),
                        Matrix3x2.CreateRotation((float)(90 * Math.PI / 180)));
                    m.M31 = offsY + pageSize.Height * scaleY + printableArea.X;
                    m.M32 = printableArea.Y;
                    break;
                case 270:
                    m = Matrix3x2.Multiply(Matrix3x2.CreateScale(scaleX, scaleY),
                        Matrix3x2.CreateRotation((float)(270 * Math.PI / 180)));
                    m.M31 = printableArea.X;
                    m.M32 = offsX + pageSize.Width * scaleX + printableArea.Y;
                    break;
                default:
                    throw new ArgumentOutOfRangeException("有効な値は 90 と 270 です。");
            }

            TransparencyFeatures tf = page.GetTransparencyFeatures();
            if (tf == TransparencyFeatures.None)
            {
                // ページのコンテンツストリームが透明度機能を使用していない場合、graphics に直接レンダリングします。
                graphics.Transform = m;
                page.Draw(graphics, new RectangleF(0, 0, pageSize.Width, pageSize.Height), true, true, renderingCache, true);
            }
            else
            {
                // GcD2DGraphics によるレンダリングを使用します。
                if (bitmap == null)
                {
                    bitmap = new GcD2DBitmap(device, graphics.Factory);
                    bitmap.SetFontCache(fontCache);
                }
                SizeF pageSizeScaled = new SizeF(
                    pageSize.Width * scaleX * printerDpi / 96f,
                    pageSize.Height * scaleY * printerDpi / 96f);
                Size bitmapSize = new Size(
                    (int)(pageSizeScaled.Width + 0.5f),
                    (int)(pageSizeScaled.Height + 0.5f));
                if (bitmap.PixelWidth != bitmapSize.Width || bitmap.PixelHeight != bitmapSize.Height)
                    bitmap.CreateImage(bitmapSize.Width, bitmapSize.Height, printerDpi, printerDpi);
                using (GcD2DBitmapGraphics bg = bitmap.CreateGraphics(Color.FromArgb(0)))
                {
                    page.Draw(bg, new RectangleF(0, 0, pageSizeScaled.Width, pageSizeScaled.Height), true, true, renderingCache, true);
                }
                // ビットマップをオフセット付きで graphics に描画します。
                graphics.Transform = m;
                graphics.RenderTarget.DrawBitmap(bitmap.Bitmap, new DX.RectF(offsX, offsY, pageSize.Width, pageSize.Height));
            }
        }

        private bool PrintPage(
            string printerName,
            D2D.DeviceContext rt,
            D2D.PrintControl printControl,
            D2D.Device device,
            GcDXGraphics graphics,
            int pageIndex,
            int landscapeAngle,
            float printerDpi,
            float printerPageWidthPx,
            float printerPageHeightPx,
            RectangleF printableArea,
            int pageCount,
            RenderingCache renderingCache,
            FontCache fontCache,
            ref int pageNo,
            ref GcD2DBitmap bitmap)
        {
            D2D.CommandList printCommandList = D2D.CommandList.Create(rt);
            rt.SetTarget(printCommandList);
            rt.BeginDraw();

            DrawContent(
                graphics,
                device,
                ref bitmap,
                pageIndex,
                landscapeAngle,
                printerDpi,
                new SizeF(printerPageWidthPx, printerPageHeightPx),
                printableArea,
                renderingCache,
                fontCache);

            bool res = rt.EndDraw(true);
            if (res)
            {
                printCommandList.Close();
                printControl.AddPage(printCommandList, new DX.Size2F(printerPageWidthPx, printerPageHeightPx));
            }
            printCommandList.Dispose();

            //
            pageNo++;
            if (!OnLongOperation(0.2 + ((double)pageNo / (double)pageCount) * 0.8, true))
                return false;

            if (!res)
                throw new Exception($"プリンター[{printerName}]で印刷中にエラーが発生しました。");

            return true;
        }

        private unsafe void Print(
            string printerName,
            STG.ComStream jobPrintTicketStream,
            float printerPageWidthPx,
            float printerPageHeightPx,
            RectangleF printableArea,
            int dpi,
            bool autoCollate,
            int copies,
            bool collate,
            OutputRange outputRange)
        {
            DXGI.PrintDocumentPackageTargetFactory documentTargetFactory = null;
            DXGI.PrintDocumentPackageTarget documentTarget = null;
            D2D.Factory1 d2dFactory = null;
            D3D.DeviceContext d3dContext = null;
            D3D.Device d3dDevice = null;
            DXGI.Device1 dxgiDevice = null;
            D2D.Device d2dDevice = null;
            D2D.PrintControl printControl = null;
            WIC.ImagingFactory2 wicFactory = null;
            D2D.DeviceContext rt = null;
            DW.Factory1 dwFactory = null;
            GcD2DBitmap bitmap = null;
            RenderingCache renderingCache = _renderingCache;
            if (renderingCache == null)
                renderingCache = new RenderingCache();
            try
            {
#if DEBUG && false
                jobPrintTicketStream.Seek(0, System.IO.SeekOrigin.Begin);
                var tfile = Path.GetTempFileName();
                using (var fs = new FileStream(tfile, System.IO.FileMode.Create))
                {
                    byte[] data = new byte[1024 * 16];
                    fixed (byte* d = data)
                    {
                        while (true)
                        {
                            var read = jobPrintTicketStream.Read((IntPtr)d, data.Length);
                            if (read <= 0)
                                break;
                            fs.Write(data, 0, data.Length);
                        }
                    }
                }
#endif
                //
                string printJobName = _printJobName;
                if (string.IsNullOrEmpty(printJobName))
                    printJobName = "GcPdf 印刷ジョブ";

                //
                documentTargetFactory = DXGI.PrintDocumentPackageTargetFactory.Create();
                try
                {
                    documentTarget = documentTargetFactory.CreateDocumentPackageTargetForPrintJob(
                        printerName,
                        printJobName,
                        jobPrintTicketStream);
                }
                catch (Exception ex)
                {
                    throw new Exception(string.Format("プリンター[{0}]用の印刷ジョブを作成できません。\r\n例外:\r\n{1}", printerName, ex.Message));
                }
                finally
                {
                    documentTargetFactory.Dispose();
                    documentTargetFactory = null;
                }
                if (documentTarget == null)
                    // ユーザが印刷をキャンセルした場合のみ null が返されます。
                    throw new OperationCanceledException();

                // デバイスのリソースを初期化します。
                d2dFactory = D2D.Factory1.Create(D2D.FactoryType.SingleThreaded);
                wicFactory = WIC.ImagingFactory2.Create();
                dwFactory = DW.Factory1.Create(DW.FactoryType.Shared);
                D3D.FeatureLevel[] featureLevels = new D3D.FeatureLevel[]
                {
                    D3D.FeatureLevel.Level_11_1,
                    D3D.FeatureLevel.Level_11_0,
                    D3D.FeatureLevel.Level_10_1,
                    D3D.FeatureLevel.Level_10_0
                };
                D3D.FeatureLevel actualLevel;
                d3dContext = null;
                d3dDevice = new D3D.Device(IntPtr.Zero);

                DX.HResult result = DX.HResult.Ok;
                for (int i = 0; i <= 1; i++)
                {
                    // ハードウェアが使用できない場合は WARP を使用します。
                    D3D.DriverType driverType = i == 0 ? D3D.DriverType.Hardware : D3D.DriverType.Warp;
                    result = D3D.D3D11.CreateDevice(null, driverType, IntPtr.Zero, D3D.DeviceCreationFlags.BgraSupport | D3D.DeviceCreationFlags.SingleThreaded,
                        featureLevels, featureLevels.Length, D3D.D3D11.SdkVersion, d3dDevice, out actualLevel, out d3dContext);
                    if (result.Code != unchecked((int)0x887A0004)) // DXGI_ERROR_UNSUPPORTED
                    {
                        break;
                    }
                }
                result.CheckError();
                //
                dxgiDevice = d3dDevice.QueryInterface<DXGI.Device1>();
                d3dContext.Dispose();
                d3dContext = null;
                //
                d2dDevice = d2dFactory.CreateDevice(dxgiDevice);
                //
                D2D.PrintControlProperties printControlProperties = new D2D.PrintControlProperties
                {
                    FontSubset = D2D.PrintFontSubsetMode.Default,
                    RasterDPI = (float)dpi,
                    ColorSpace = D2D.ColorSpace.SRgb
                };
                //
                if (!OnLongOperation(0.2, true))
                    throw new OperationCanceledException();
                //
                printControl = D2D.PrintControl.Create(d2dDevice, wicFactory, documentTarget.NativePointer, printControlProperties);
                if (printControl == null)
                    throw new OperationCanceledException();
                rt = D2D.DeviceContext.Create(d2dDevice, D2D.DeviceContextOptions.None);
                //
                int totalPageCount = _doc.Pages.Count;
                int pageNo = 0;
                int pageCount = outputRange.GetPageCount(1, totalPageCount) * copies;
                int landscapeAngle = PrinterSettings.LandscapeAngle;
                using (FontCache fontCache = new FontCache(dwFactory))
                using (GlyphPathCache glyphPathCache = new GlyphPathCache())
                using (D2D.SolidColorBrush brush = rt.CreateSolidColorBrush(DX.ColorF.Black, null))
                using (GcDXGraphics graphics = new GcDXGraphics(rt, d2dFactory, null, fontCache, brush, glyphPathCache, false))
                {
                    IEnumerator<int> pages = outputRange.GetEnumerator(1, totalPageCount);
                    if (autoCollate)
                    {
                        if (collate)
                        {
                            for (int i = 0; i < copies; i++)
                            {
                                pages.Reset();
                                while (pages.MoveNext())
                                {
                                    PrintPage(
                                        printerName,
                                        rt,
                                        printControl,
                                        d2dDevice,
                                        graphics,
                                        pages.Current - 1,
                                        landscapeAngle,
                                        dpi,
                                        printerPageWidthPx,
                                        printerPageHeightPx,
                                        printableArea,
                                        pageCount,
                                        renderingCache,
                                        fontCache,
                                        ref pageNo,
                                        ref bitmap);
                                }
                            }
                        }
                        else
                        {
                            while (pages.MoveNext())
                            {
                                for (int i = 0; i < copies; i++)
                                {
                                    PrintPage(
                                        printerName,
                                        rt,
                                        printControl,
                                        d2dDevice,
                                        graphics,
                                        pages.Current - 1,
                                        landscapeAngle,
                                        dpi,
                                        printerPageWidthPx,
                                        printerPageHeightPx,
                                        printableArea,
                                        pageCount,
                                        renderingCache,
                                        fontCache,
                                        ref pageNo,
                                        ref bitmap);
                                }
                            }
                        }
                    }
                    else
                    {
                        while (pages.MoveNext())
                        {
                            PrintPage(
                                printerName,
                                rt,
                                printControl,
                                d2dDevice,
                                graphics,
                                pages.Current - 1,
                                landscapeAngle,
                                dpi,
                                printerPageWidthPx,
                                printerPageHeightPx,
                                printableArea,
                                pageCount,
                                renderingCache,
                                fontCache,
                                ref pageNo,
                                ref bitmap);
                        }
                    }
                }
                rt.Dispose();
                rt = null;
                printControl.Close();
            }
            finally
            {
                if (bitmap != null)
                    bitmap.Dispose();
                if (rt != null)
                    rt.Dispose();
                if (printControl != null)
                    printControl.Dispose();
                if (d2dDevice != null)
                    d2dDevice.Dispose();
                if (dxgiDevice != null)
                    dxgiDevice.Dispose();
                if (d3dContext != null)
                    d3dContext.Dispose();
                if (d3dDevice != null)
                    d3dDevice.Dispose();
                if (dwFactory != null)
                    dwFactory.Dispose();
                if (wicFactory != null)
                    wicFactory.Dispose();
                if (d2dFactory != null)
                    d2dFactory.Dispose();
                if (documentTarget != null)
                    documentTarget.Dispose();
                if (documentTargetFactory != null)
                    documentTargetFactory.Dispose();
                if (_renderingCache == null)
                    renderingCache.Dispose();
            }
        }
        #endregion

        #region Events
        public event Func<double, bool, bool> LongOperation;
        #endregion

        #region pinvoke
        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        private class DEVMODE
        {
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
            public string dmDeviceName;
            public short dmSpecVersion;
            public short dmDriverVersion;
            public short dmSize;
            public short dmDriverExtra;
            public int dmFields;
            public short dmOrientation;
            public short dmPaperSize;
            public short dmPaperLength;
            public short dmPaperWidth;
            public short dmScale;
            public short dmCopies;
            public short dmDefaultSource;
            public short dmPrintQuality;
            public short dmColor;
            public short dmDuplex;
            public short dmYResolution;
            public short dmTTOption;
            public short dmCollate;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
            public string dmFormName;
            public short dmLogPixels;
            public int dmBitsPerPel;
            public int dmPelsWidth;
            public int dmPelsHeight;
            public int dmDisplayFlags;
            public int dmDisplayFrequency;
            public int dmICMMethod;
            public int dmICMIntent;
            public int dmMediaType;
            public int dmDitherType;
            public int dmICCManufacturer;
            public int dmICCModel;
            public int dmPanningWidth;
            public int dmPanningHeight;
        }

        /// <summary>
        /// DEVMODE 構造体のフィールド
        /// </summary>
        private class DM
        {
            public const int
                DM_ORIENTATION = 0x00001,
                DM_PAPERSIZE = 2,
                DM_PAPERLENGTH = 4,
                DM_PAPERWIDTH = 8,
                DM_COPIES = 0x100,
                DM_DEFAULTSOURCE = 0x200,
                DM_PRINTQUALITY = 0x400,
                DM_COLOR = 0x800,
                DM_YRESOLUTION = 0x2000,
                DM_COLLATE = 0x00008000,
                DM_BITSPERPEL = 0x40000,
                DM_PELSWIDTH = 0x80000,
                DM_PELSHEIGHT = 0x100000,
                DM_DISPLAYFREQUENCY = 0x400000;
        }

        [DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
        private static extern IntPtr GlobalLock(IntPtr handle);

        [DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
        private static extern bool GlobalUnlock(IntPtr handle);

        [DllImport("kernel32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
        private static extern IntPtr GlobalFree(IntPtr hMem);

        [DllImport("ole32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
        private static extern int CreateStreamOnHGlobal(IntPtr hGlobal, bool fDeleteOnRelease, ref IntPtr istream);

        [DllImport("prntvpt.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
        private static extern int PTConvertDevModeToPrintTicket(IntPtr hProvider, int cbDevmode, IntPtr devMode, int scope, IntPtr printTicket);

        [DllImport("prntvpt.dll", ExactSpelling = true, CharSet = CharSet.Unicode)]
        private static extern int PTOpenProvider(string pszPrinterName, int dwVersion, ref IntPtr phProvider);

        [DllImport("prntvpt.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
        private static extern int PTCloseProvider(IntPtr hProvider);
        #endregion
    }
}