{*******************************************************}
{                                                       }
{       Tocsg.Printer                                   }
{                                                       }
{       Copyright (C) 2019 kku                          }
{                                                       }
{*******************************************************}

unit Tocsg.Printer;

interface

uses
  Tocsg.Obj, System.SysUtils, Winapi.Windows, Winapi.WinSpool,
  System.Classes, Tocsg.Thread, System.Generics.Collections;

const
  MAX_JOB = 1024;

  SIG_PJL: array[0..8] of Byte = ($1B, $25, $2D, $31, $32, $33, $34, $35, $58);

  REG_KEY_PRINTERSX = 'SYSTEM\CurrentControlSet\Control\Print\Printers';

  DMCOLOR_MONOCHROME = 1;
  {$EXTERNALSYM DMCOLOR_MONOCHROME}
  DMCOLOR_COLOR      = 2;
  {$EXTERNALSYM DMCOLOR_COLOR}

  DMPAPER_LETTER      =        1; // Letter 8 1/2 x 11 in
  DMPAPER_LETTERSMALL =        2; // Letter Small 8 1/2 x 11 in
  DMPAPER_TABLOID     =        3; // Tabloid 11 x 17 in
  DMPAPER_LEDGER      =        4; // Ledger 17 x 11 in
  DMPAPER_LEGAL       =        5; // Legal 8 1/2 x 14 in
  DMPAPER_STATEMENT   =        6; // Statement 5 1/2 x 8 1/2 in
  DMPAPER_EXECUTIVE   =        7; // Executive 7 1/4 x 10 1/2 in
  DMPAPER_A3          =        8; // A3 297 x 420 mm
  DMPAPER_A4          =        9; // A4 210 x 297 mm
  DMPAPER_A4SMALL     =       10; // A4 Small 210 x 297 mm
  DMPAPER_A5          =       11; // A5 148 x 210 mm
  DMPAPER_B4          =       12; // B4 (JIS) 250 x 354
  DMPAPER_B5          =       13; // B5 (JIS) 182 x 257 mm
  DMPAPER_FOLIO       =       14; // Folio 8 1/2 x 13 in
  DMPAPER_QUARTO      =       15; // Quarto 215 x 275 mm
  DMPAPER_10X14       =       16; // 10x14 in
  DMPAPER_11X17       =       17; // 11x17 in
  DMPAPER_NOTE        =       18; // Note 8 1/2 x 11 in
  DMPAPER_ENV_9       =       19; // Envelope #9 3 7/8 x 8 7/8
  DMPAPER_ENV_10      =       20; // Envelope #10 4 1/8 x 9 1/2
  DMPAPER_ENV_11      =       21; // Envelope #11 4 1/2 x 10 3/8
  DMPAPER_ENV_12      =       22; // Envelope #12 4 \276 x 11
  DMPAPER_ENV_14      =       23; // Envelope #14 5 x 11 1/2
  DMPAPER_CSHEET      =       24; // C size sheet
  DMPAPER_DSHEET      =       25; // D size sheet
  DMPAPER_ESHEET      =      26; // E size sheet
  DMPAPER_ENV_DL      =        27; // Envelope DL 110 x 220mm
  DMPAPER_ENV_C5      =        28; // Envelope C5 162 x 229 mm
  DMPAPER_ENV_C3      =        29; // Envelope C3  324 x 458 mm
  DMPAPER_ENV_C4      =        30; // Envelope C4  229 x 324 mm
  DMPAPER_ENV_C6      =        31; // Envelope C6  114 x 162 mm
  DMPAPER_ENV_C65     =        32; // Envelope C65 114 x 229 mm
  DMPAPER_ENV_B4      =        33; // Envelope B4  250 x 353 mm
  DMPAPER_ENV_B5      =        34; // Envelope B5  176 x 250 mm
  DMPAPER_ENV_B6      =        35; // Envelope B6  176 x 125 mm
  DMPAPER_ENV_ITALY   =        36; // Envelope 110 x 230 mm
  DMPAPER_ENV_MONARCH =        37; // Envelope Monarch 3.875 x 7.5 in
  DMPAPER_ENV_PERSONAL =       38; // 6 3/4 Envelope 3 5/8 x 6 1/2 in
  DMPAPER_FANFOLD_US   =       39; // US Std Fanfold 14 7/8 x 11 in
  DMPAPER_FANFOLD_STD_GERMAN = 40; // German Std Fanfold 8 1/2 x 12 in
  DMPAPER_FANFOLD_LGL_GERMAN = 41; // German Legal Fanfold 8 1/2 x 13 in
  DMPAPER_ISO_B4             = 42; // B4 (ISO) 250 x 353 mm
  DMPAPER_JAPANESE_POSTCARD  = 43; // Japanese Postcard 100 x 148 mm
  DMPAPER_9X11               = 44; // 9 x 11 in
  DMPAPER_10X11       =        45; // 10 x 11 in
  DMPAPER_15X11       =        46; // 15 x 11 in
  DMPAPER_ENV_INVITE  =        47; // Envelope Invite 220 x 220 mm
  DMPAPER_RESERVED_48 =        48; // RESERVED--DO NOT USE
  DMPAPER_RESERVED_49 =        49; // RESERVED--DO NOT USE
  DMPAPER_LETTER_EXTRA =       50; // Letter Extra 9 \275 x 12 in
  DMPAPER_LEGAL_EXTRA  =       51; // Legal Extra 9 \275 x 15 in
  DMPAPER_TABLOID_EXTRA =      52; // Tabloid Extra 11.69 x 18 in
  DMPAPER_A4_EXTRA      =      53; // A4 Extra 9.27 x 12.69 in
  DMPAPER_LETTER_TRANSVERSE =  54; // Letter Transverse 8 \275 x 11 in
  DMPAPER_A4_TRANSVERSE  =     55; // A4 Transverse 210 x 297 mm
  DMPAPER_LETTER_EXTRA_TRANSVERSE = 56; // Letter Extra Transverse 9\275 x 12 in
  DMPAPER_A_PLUS            =  57; // SuperA/SuperA/A4 227 x 356 mm
  DMPAPER_B_PLUS            =  58; // SuperB/SuperB/A3 305 x 487 mm
  DMPAPER_LETTER_PLUS       =  59; // Letter Plus 8.5 x 12.69 in
  DMPAPER_A4_PLUS           =  60; // A4 Plus 210 x 330 mm
  DMPAPER_A5_TRANSVERSE     =  61; // A5 Transverse 148 x 210 mm
  DMPAPER_B5_TRANSVERSE     =  62; // B5 (JIS) Transverse 182 x 257 mm
  DMPAPER_A3_EXTRA          =  63; // A3 Extra 322 x 445 mm
  DMPAPER_A5_EXTRA          =  64; // A5 Extra 174 x 235 mm
  DMPAPER_B5_EXTRA          =  65; // B5 (ISO) Extra 201 x 276 mm
  DMPAPER_A2                =  66; // A2 420 x 594 mm
  DMPAPER_A3_TRANSVERSE     =  67; // A3 Transverse 297 x 420 mm
  DMPAPER_A3_EXTRA_TRANSVERSE = 68; // A3 Extra Transverse 322 x 445 mm

  DMPAPER_DBL_JAPANESE_POSTCARD = 69; // Japanese Double Postcard 200 x 148 mm
  DMPAPER_A6                =  70; // A6 105 x 148 mm
  DMPAPER_JENV_KAKU2        =  71; // Japanese Envelope Kaku #2
  DMPAPER_JENV_KAKU3        =  72; // Japanese Envelope Kaku #3
  DMPAPER_JENV_CHOU3        =  73; // Japanese Envelope Chou #3
  DMPAPER_JENV_CHOU4        =  74; // Japanese Envelope Chou #4
  DMPAPER_LETTER_ROTATED    =  75; // Letter Rotated 11 x 8 1/2 11 in
  DMPAPER_A3_ROTATED        =  76; // A3 Rotated 420 x 297 mm
  DMPAPER_A4_ROTATED        =  77; // A4 Rotated 297 x 210 mm
  DMPAPER_A5_ROTATED        =  78; // A5 Rotated 210 x 148 mm
  DMPAPER_B4_JIS_ROTATED    =  79; // B4 (JIS) Rotated 364 x 257 mm
  DMPAPER_B5_JIS_ROTATED    =  80; // B5 (JIS) Rotated 257 x 182 mm
  DMPAPER_JAPANESE_POSTCARD_ROTATED = 81; // Japanese Postcard Rotated 148 x 100 mm
  DMPAPER_DBL_JAPANESE_POSTCARD_ROTATED = 82; // Double Japanese Postcard Rotated 148 x 200 mm
  DMPAPER_A6_ROTATED          = 83; // A6 Rotated 148 x 105 mm
  DMPAPER_JENV_KAKU2_ROTATED = 84; // Japanese Envelope Kaku #2 Rotated
  DMPAPER_JENV_KAKU3_ROTATED = 85; // Japanese Envelope Kaku #3 Rotated
  DMPAPER_JENV_CHOU3_ROTATED = 86; // Japanese Envelope Chou #3 Rotated
  DMPAPER_JENV_CHOU4_ROTATED = 87; // Japanese Envelope Chou #4 Rotated
  DMPAPER_B6_JIS             = 88; // B6 (JIS) 128 x 182 mm
  DMPAPER_B6_JIS_ROTATED     = 89; // B6 (JIS) Rotated 182 x 128 mm
  DMPAPER_12X11              = 90; // 12 x 11 in
  DMPAPER_JENV_YOU4          = 91; // Japanese Envelope You #4
  DMPAPER_JENV_YOU4_ROTATED  = 92; // Japanese Envelope You #4 Rotated
  DMPAPER_P16K               = 93; // PRC 16K 146 x 215 mm
  DMPAPER_P32K               = 94; // PRC 32K 97 x 151 mm
  DMPAPER_P32KBIG            = 95; // PRC 32K(Big) 97 x 151 mm
  DMPAPER_PENV_1             = 96; // PRC Envelope #1 102 x 165 mm
  DMPAPER_PENV_2             = 97; // PRC Envelope #2 102 x 176 mm
  DMPAPER_PENV_3             = 98; // PRC Envelope #3 125 x 176 mm
  DMPAPER_PENV_4             = 99; // PRC Envelope #4 110 x 208 mm
  DMPAPER_PENV_5             = 100; // PRC Envelope #5 110 x 220 mm
  DMPAPER_PENV_6             = 101; // PRC Envelope #6 120 x 230 mm
  DMPAPER_PENV_7             = 102; // PRC Envelope #7 160 x 230 mm
  DMPAPER_PENV_8             = 103; // PRC Envelope #8 120 x 309 mm
  DMPAPER_PENV_9             = 104; // PRC Envelope #9 229 x 324 mm
  DMPAPER_PENV_10            = 105; // PRC Envelope #10 324 x 458 mm
  DMPAPER_P16K_ROTATED       = 106; // PRC 16K Rotated
  DMPAPER_P32K_ROTATED       = 107; // PRC 32K Rotated
  DMPAPER_P32KBIG_ROTATED    = 108; // PRC 32K(Big) Rotated
  DMPAPER_PENV_1_ROTATED     = 109; // PRC Envelope #1 Rotated 165 x 102 mm
  DMPAPER_PENV_2_ROTATED     = 110; // PRC Envelope #2 Rotated 176 x 102 mm
  DMPAPER_PENV_3_ROTATED     = 111; // PRC Envelope #3 Rotated 176 x 125 mm
  DMPAPER_PENV_4_ROTATED     = 112; // PRC Envelope #4 Rotated 208 x 110 mm
  DMPAPER_PENV_5_ROTATED     = 113; // PRC Envelope #5 Rotated 220 x 110 mm
  DMPAPER_PENV_6_ROTATED     = 114; // PRC Envelope #6 Rotated 230 x 120 mm
  DMPAPER_PENV_7_ROTATED     = 115; // PRC Envelope #7 Rotated 230 x 160 mm
  DMPAPER_PENV_8_ROTATED     = 116; // PRC Envelope #8 Rotated 309 x 120 mm
  DMPAPER_PENV_9_ROTATED     = 117; // PRC Envelope #9 Rotated 324 x 229 mm
  DMPAPER_PENV_10_ROTATED    = 118; // PRC Envelope #10 Rotated 458 x 324 mm

{
 2085      DEVMODEA = record
 2086           dmDeviceName : array[0..(CCHDEVICENAME)-1] of AnsiChar;
 2087           dmSpecVersion : WORD;
 2088           dmDriverVersion : WORD;
 2089           dmSize : WORD;
 2090           dmDriverExtra : WORD;
 2091           dmFields : DWORD;
 2092           case byte of
 2093             1: (dmOrientation : SmallInt;
 2094                 dmPaperSize : SmallInt;
 2095                 dmPaperLength : SmallInt;
 2096                 dmPaperWidth : SmallInt;
 2097                 dmScale : SmallInt;
 2098                 dmCopies : SmallInt;
 2099                 dmDefaultSource : SmallInt;
 2100                 dmPrintQuality : SmallInt;
 2101                 dmColor : SmallInt;
 2102                 dmDuplex : SmallInt;
 2103                 dmYResolution : SmallInt;
 2104                 dmTTOption : SmallInt;
 2105                 dmCollate : SmallInt;
 2106                 dmFormName : array[0..(CCHFORMNAME)-1] of AnsiCHAR;
 2107                 dmLogPixels : WORD;
 2108                 dmBitsPerPel : DWORD;
 2109                 dmPelsWidth : DWORD;
 2110                 dmPelsHeight : DWORD;
 2111                 dmDisplayFlags : DWORD;
 2112                 dmDisplayFrequency : DWORD;
 2113                 dmICMMethod : DWORD;
 2114                 dmICMIntent : DWORD;
 2115                 dmMediaType : DWORD;
 2116                 dmDitherType : DWORD;
 2117                 dmICCManufacturer : DWORD;
 2118                 dmICCModel : DWORD
 2119                );
 2120             2: (dmPosition: POINTL;
 2121                 dmDisplayOrientation: DWORD;
 2122                 dmDisplayFixedOutput: DWORD;
 2123                );
 2124        end;
 2125
 2126      LPDEVMODEA = ^DEVMODEA;
 2127      _DEVMODEA  = DEVMODEA;
 2128      TDEVMODEA  = DEVMODEA;
 2129      PDEVMODEA  = LPDEVMODEA;
 2130
 2131      _devicemodeA = DEVMODEA;
 2132      devicemodeA  = DEVMODEA;
 2133      tdevicemodeA = DEVMODEA;
 2134      PDeviceModeA = LPDEVMODEA;
 2135
 }


type
  POINTL = packed record
    x : LONG;
    y : LONG;
  end;

  PDevModeW = ^TDevModeW;
  TDevModeW = packed record
    dmDeviceName    : array[0.. CCHDEVICENAME-1] of WCHAR;
    dmSpecVersion   : WORD;
    dmDriverVersion : WORD;
    dmSize          : WORD;
    dmDriverExtra   : WORD;
    dmFields        : DWORD;
    case byte of
      1:
      (
        dmOrientation  : short;
        dmPaperSize    : short;
        dmPaperLength  : short;
        dmPaperWidth   : short;
        dmScale       : short;
        dmCopies      : short;
        dmDefaultSource: short;
        dmPrintQuality : short;
        dmColor       : short;
        dmDuplex      : short;
        dmYResolution  : short;
        dmTTOption    : short;
        dmCollate     : short;
        dmFormName     : array [0..CCHFORMNAME-1] of wchar;
        dmLogPixels    : WORD;
        dmBitsPerPel   : DWORD;
        dmPelsWidth    : DWORD;
        dmPelsHeight   : DWORD;
        dmDisplayFlags : DWORD;
        dmDisplayFrequency     : DWORD;
        dmICMMethod    : DWORD;
        dmICMIntent    : DWORD;
        dmMediaType    : DWORD;
        dmDitherType   : DWORD;
        dmReserved1    : DWORD;
        dmReserved2    : DWORD;
        dmPanningWidth : DWORD;
        dmPanningHeight: DWORD;
      );
    2:
      (
        dmPosition: POINTL;
        dmDisplayOrientation: DWORD;
        dmDisplayFixedOutput: DWORD;
      );
  end;

  PJOB_INFO_1 = ^TJOB_INFO_1;
  TJOB_INFO_1 = packed record
    dwJobId        : DWORD;
    sPrinterName   : LPTSTR;
    sMachineName   : LPTSTR;
    sUserName      : LPTSTR;
    sDocument      : LPTSTR;
    sDatatype      : LPTSTR;
    sStatus        : LPTSTR;
    dwStatus       : DWORD;
    dwPriority     : DWORD;
    dwPosition     : DWORD;
    dwTotalPages   : DWORD;
    dwPagesPrinted : DWORD;
    Submitted      : SYSTEMTIME;
  end;

  PJOB_INFO_2 = ^TJOB_INFO_2;
  TJOB_INFO_2 = record
    dwJobId             : DWORD;
    pPrinterName        : LPTSTR;
    pMachineName        : LPTSTR;
    pUserName           : LPTSTR;
    pDocument           : LPTSTR;
    pNotifyName         : LPTSTR;
    pDatatype           : LPTSTR;
    pPrintProcessor     : LPTSTR;
    pParameters         : LPTSTR;
    pDriverName         : LPTSTR;
    pDevMode            : PDevModeW;
    pStatus             : LPTSTR;
    pSecurityDescriptor : PSECURITY_DESCRIPTOR;
    dwStatus            : DWORD;
    dwPriority          : DWORD;
    dwPosition          : DWORD;
    dwStartTime         : DWORD;
    dwUntilTime         : DWORD;
    dwTotalPages        : DWORD;
    dwSize              : DWORD;
    Submitted           : SYSTEMTIME;
    dwTime              : DWORD;
    dwPagesPrinted      : DWORD;
  end;

  PPrtJobDevInfo = ^TPrtJobDevInfo;
  TPrtJobDevInfo = record
    sDataType,
    sDocName,
    sPtrName,
    sDrvName,
    sPaperInfo,
    sPrintProcessor: String;
    dwScale,
    dwTotalPage,
    dwPaperSizeT,
    dwCopyCount: DWORD;
    bColor,
    bPaperV: Boolean;

    DevMode: TDeviceMode;
  end;

  TPrtJobState = (jsAdd, jsWork, jsDelete);
  TPrtJobInfo = class(TTgObject)
  private
    dwID_: DWORD;
    hPrtHandle_: THandle;
    bCustomPause_: Boolean;

    sUserName_,
    sMachine_,
    sPort_,
    sDocument_,
    sPrinterName_: String;
    dwPagesPrinted_,
    dwTotalPages_,
    dwBytesPrinted_,
    dwTotalBytes_,
    dwChange_,
    dwStatus_    : DWORD;
    PrtJobState_: TPrtJobState;
    dtSubmitted_: TDateTime;
    bWorkEnd_: Boolean; // 작업상태 체크용 22_0719 12:30:58 kku

    procedure SetJobState(aPrtJobState: TPrtJobState);
  public
  // 출력물 수행 프로세스 정보를 넣는데 사용 25_0605 15:17:45 kku
    Wnd: HWND;
//    PID: DWORD;
//    PName: String;

    Constructor Create(hPrtHandle: THandle; dwID: DWORD);

    function SetPrtJob(dwControl: DWORD): Boolean;
    function PausePrtJob: Boolean;
    function ResumePrtJob(bForce: Boolean = false): Boolean;
    procedure UpdatePrtFieldInfo(Info: TPrinterNotifyInfoData);

    function IsSpooling: Boolean;
    function IsSpooling2: Boolean;
    function GetJobDevInfo(var aInfo: TPrtJobDevInfo): Boolean;

    property ID: DWORD read dwID_;
    property UserName: String read sUserName_;
    property PrinterName: String read sPrinterName_;
    property Machine: String read sMachine_;
    property Port: String read sPort_;
    property Document: String read sDocument_;
    property JobDateTime: TDateTime read dtSubmitted_;
    property PagesPrinted: DWORD read dwPagesPrinted_;
    property TotalPages: DWORD read dwTotalPages_;
    property BytesPrinted: DWORD read dwBytesPrinted_;
    property TotalBytes: DWORD read dwTotalBytes_;
    property Status: DWORD read dwStatus_;
    property IsCustomPause: Boolean read bCustomPause_ write bCustomPause_;
    property PtrJobState: TPrtJobState read PrtJobState_ write SetJobState;
    property WorkEnd: Boolean read bWorkEnd_ write bWorkEnd_;
  end;

  TThdPrtSpoolWatch = class;

  TPrtChangeNotifyEvent = procedure(Sender: TThdPrtSpoolWatch; dwChange: DWORD) of object;
  TPrtNotifyJobInfoEvent = procedure(Sender: TThdPrtSpoolWatch; Job: TPrtJobInfo) of object;

  PPrtJobs = ^TPrtJobs;
  TPrtJobs = array [0..MAX_JOB-1] of TJobInfo1;

  TThdPrtSpoolWatch = class(TTgThread)
  private
    PrtJobInfo_: TPrtJobInfo;
    sSpoolDir_,
    sDeviceName_: String;
    hPrtChangeEvent_,
    hPrinter_: THandle;
    arrJobFields_: array [0..22] of WORD;
    PNO_: TPrinterNotifyOptions;
    bSync_: Boolean;
    arrPNOT_: array [0..0] of TPrinterNotifyOptionsType;
    JobList_: TDictionary<DWORD,TPrtJobInfo>;

    procedure OnJobValue(Sender: TObject; const Item: TPrtJobInfo;
                  Action: TCollectionNotification);
  protected
    dwChangeMod_: DWORD;
    evPrtChangeNotifyEvent_: TPrtChangeNotifyEvent;
    evPrtNotifyJobInfoEvent_: TPrtNotifyJobInfoEvent;

    procedure ProcessChangeEvent;
    procedure ProcessNotifyJobInfoEvent;

    procedure Execute; override;
  public
    Constructor Create(bSync: Boolean; sDeviceName: String; dwWatchMod: DWORD = PRINTER_CHANGE_ALL);
    Destructor Destroy; override;

    procedure StopThread; override;

    property DeviceName: String read sDeviceName_;
    property SpoolDir: String read sSpoolDir_;
    property OnPrtChangeNotifyEvent: TPrtChangeNotifyEvent write evPrtChangeNotifyEvent_;
    property OnPrtNotifyJobInfoEvent: TPrtNotifyJobInfoEvent write evPrtNotifyJobInfoEvent_;
  end;

  TTgPrtSpoolWatch = class(TTgThread)
  private
    bIsWatch_: Boolean;
    dwWatchMod_: DWORD;
    WatchThdList_: TList<TThdPrtSpoolWatch>;
    bSync_: Boolean;
    PrtList_: TStringList;
    procedure OnPrtNotify(Sender: TObject; const Item: TThdPrtSpoolWatch; Action: TCollectionNotification);
  protected
    evPrtChangeNotifyEvent_: TPrtChangeNotifyEvent;
    evPtrNotifyInfoEvent_: TPrtNotifyJobInfoEvent;
    procedure StartPrtWatch;
    procedure StopPrtWatch;
    procedure Execute; override;
  public
    Constructor Create(bSync: Boolean; dwWatchMod: DWORD = PRINTER_CHANGE_ALL);
    Destructor Destroy; override;

    property IsWatch: Boolean read bIsWatch_;
    property OnPrtChangeNotificationEvent: TPrtChangeNotifyEvent write evPrtChangeNotifyEvent_;
    property OnPrtNotificationEvent: TPrtNotifyJobInfoEvent write evPtrNotifyInfoEvent_;
  end;

  TPrinterPortType = (PTUnknown, PTLocal, PTTcpIp, PTWsd, PTShared);
  PPrinterInfo = ^TPrinterInfo;
  TPrinterInfo = record
    sIp,
    sPrtName,
    sDrvName,
    sPortName: String;
    PortType: TPrinterPortType;
    bIsPowerSaveMode: Boolean;
    dwStatus: DWORD;
  end;
  TPrtInfoList = TList<PPrinterInfo>;

  TPrintersInfo = class(TTgObject)
  private
    PrtInfoList_: TPrtInfoList;
    procedure OnPrtInfoNotify(Sender: TObject; const Item: PPrinterInfo; Action: TCollectionNotification);
  public
    Constructor Create;
    Destructor Destroy; override;

    function GetPrtInfoByPrtName(const sPrtName: String): PPrinterInfo;

    procedure RefreshList;
    procedure SaveToFile(const sPath: String);
    procedure LoadFromFile(const sPath: String);

    property PrtInfoList: TPrtInfoList read PrtInfoList_;
  end;

function GetPrinterSpoolDir(hPrinter: THandle = 0): String;
function GetLastSpoolPath(sSpoolDir: String): String;
function GetDefaultPrinterName: string;
function GetProcessNameByPrtDocName(sDocName: String): String;
function PrinterDriverToName(sDrvName: String): String;
function PrinterDriverToIP(sDrvName: String): String;

function IsPJL(const sPath: String): Boolean;
function GetQtyFromPJL(const sPath: String; var bCollate: Boolean): Integer;
function IsPJLAndLanguagePLW(const sPath: string): Boolean;

implementation

uses
  Tocsg.Registry, Tocsg.Path, Tocsg.WinInfo, Tocsg.DateTime, Tocsg.Safe,
  Vcl.Printers, Tocsg.Exception, Tocsg.Files, Tocsg.Trace, Tocsg.Strings, Winapi.WinSvc, Tocsg.Service, System.Win.Registry, superobject, Tocsg.Json;

function GetPrinterSpoolDir(hPrinter: THandle = 0): String;
var
  dwLen,
  dwType: DWORD;
  sDir: PWideChar;
begin
  Result := '';
  if hPrinter <> 0 then
  begin
    dwLen := 0;
    dwType := REG_SZ;
    GetPrinterData(hPrinter, SPLREG_DEFAULT_SPOOL_DIRECTORY, @dwType, nil, 0, dwLen);
    if dwLen > 0 then
    begin
      sDir := AllocMem(dwLen);
      try
        if GetPrinterData(hPrinter,
                          SPLREG_DEFAULT_SPOOL_DIRECTORY,
                          @dwType, sDir,
                          dwLen, dwLen) = ERROR_SUCCESS then Result := sDir;
      finally
        FreeMem(sDir, dwLen);
      end;
    end;
  end;

  if Result = '' then
    Result := GetRegValueAsString(HKEY_LOCAL_MACHINE, REG_KEY_PRINTERSX, 'DefaultSpoolDirectory');

  if Result = '' then
  begin
    Result := GetWindowsDir + 'System32\spool\PRINTERS\';
    if not DirectoryExists(Result) then
      Result := '';
  end;

  if Result <> '' then
  begin
  // os가 64bit 이고 실행되는 프로그램이 32bit 용이면 system 폴더를 아래처럼 바꿔준다.
  // 그래야 syswow64 여기로 인식안한다. 2012-06-20 kku 
    if IsWow64 and (SizeOf(NativeInt) = 4) then
      Result := StringReplace(Result, 'system32', 'sysnative', [rfIgnoreCase]);

    Result := IncludeTrailingBackslash(Result);
  end;
end;

function GetLastSpoolPath(sSpoolDir: String): String;
var
  wfd: TWin32FindData;
  hSc: THandle;
  sDir,
  sPath: String;
  SplList: TStringList;
  i: Integer;
begin
  TTgTrace.T('GetLastSpoolPath() ..', 9);
  Result := '';
  try
    sDir := ExtractFilePath(sSpoolDir);
    if not ForceDirectories(sDir) then
      exit;

    sPath := sDir + '*.*';

    hSc := FindFirstFile(PChar(sPath), wfd);
    if hSc = INVALID_HANDLE_VALUE then
      exit;

    Guard(SplList, TStringList.Create);
    try
      Repeat
        if (String(wfd.cFileName) <> '.') and (String(wfd.cFileName) <> '..') then
          if ((wfd.dwFileAttributes and FILE_ATTRIBUTE_DIRECTORY) = 0) then
          begin
            TTgTrace.T('GetLastSpoolPath() .. %s', [wfd.cFileName], 9);
            if (GetFileExt(wfd.cFileName).ToUpper = 'SPL') then
//               (GetFileSizeHiLow(wfd.nFileSizeHigh, wfd.nFileSizeLow) > 0) then
              SplList.Add(sDir + wfd.cFileName);
          end;
      Until not FindNextFile(hSc, wfd);
    finally
      FindClose(hSc);
    end;

    TTgTrace.T('GetLastSpoolPath() .. Cnt=%d', [SplList.Count], 9);
    if SplList.Count > 0 then
    begin
      SplList.CustomSort(StringListCompareFileModifyDate);
      Result := SplList[SplList.Count - 1];
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetLastSpoolPath()');
  end;
end;

{ TPrtJobInfo }

Constructor TPrtJobInfo.Create(hPrtHandle: THandle; dwID: DWORD);
begin
  Inherited Create;

  sUserName_      := '';
  sMachine_       := '';
  sPort_          := '';
  sDocument_      := '';
  dwPagesPrinted_ := 0;
  dwTotalPages_   := 0;
  dwBytesPrinted_ := 0;
  dwTotalBytes_   := 0;
  dwChange_       := 0;
  dwStatus_       := 0;
  dtSubmitted_    := 0;
  bWorkEnd_ := false;
  bCustomPause_ := false;

  hPrtHandle_ := hPrtHandle;
  dwID_ := dwID;

  PrtJobState_ := jsAdd;
//  PausePrtJob;
end;

procedure TPrtJobInfo.SetJobState(aPrtJobState: TPrtJobState);
begin
  if PrtJobState_ <> aPrtJobState then
    PrtJobState_ := aPrtJobState;
end;

function TPrtJobInfo.SetPrtJob(dwControl: DWORD): Boolean;
begin
  try
    Result := SetJob(hPrtHandle_, dwID_, 0, nil, dwControl);
    if not Result then
      _Trace('Fail .. Code=%d', [dwControl], 1);
  except
    on E: Exception do
      ETgException.TraceException(Self, E, 'Fail .. SetPrtJob()');
  end;
end;

function TPrtJobInfo.PausePrtJob: Boolean;
begin
  if not bCustomPause_ then
  begin
    try
    // 원격 프린터의 경우.. SetJob 호출 시 ERROR_ACCESS_DENIED 날 수 있다.
    // 많은걸 테스트 해보지 않아서 원인은 파악안됨 2012-06-21 kku
      {bCustomPause_ := }Result := SetPrtJob(JOB_CONTROL_PAUSE);
      if not Result then
      begin
        _Trace('Fail .. JOB_CONTROL_PAUSE');
        exit;
      end;
      bCustomPause_ := true;
    except
      on E: Exception do
        ETgException.TraceException(Self, E, 'Fail .. PausePrtJob()');
    end;
  end;
end;

function TPrtJobInfo.ResumePrtJob(bForce: Boolean = false): Boolean;
begin
  if bCustomPause_ or bForce then
  begin
    {bCustomPause_ := not }Result := SetPrtJob(JOB_CONTROL_RESUME);
    bCustomPause_ := false;
  end;
end;

procedure TPrtJobInfo.UpdatePrtFieldInfo(Info: TPrinterNotifyInfoData);
begin
  case Info.Field of
    JOB_NOTIFY_FIELD_PRINTER_NAME  : sPrinterName_ := PChar(Info.NotifyData.Data.pBuf);
    JOB_NOTIFY_FIELD_USER_NAME     : sUserName_ := PChar(Info.NotifyData.Data.pBuf);
    JOB_NOTIFY_FIELD_MACHINE_NAME  : sMachine_ := PChar(Info.NotifyData.Data.pBuf);
    JOB_NOTIFY_FIELD_PORT_NAME     : sPort_ := PChar(Info.NotifyData.Data.pBuf);
    JOB_NOTIFY_FIELD_DOCUMENT      : sDocument_ := PChar(Info.NotifyData.Data.pBuf);
    JOB_NOTIFY_FIELD_SUBMITTED     : dtSubmitted_ := ConvSystemTimeToDateTime_Local(TSystemTime(Info.NotifyData.Data.pBuf^));
    JOB_NOTIFY_FIELD_PAGES_PRINTED : dwPagesPrinted_ := Info.NotifyData.adwData[0];
    JOB_NOTIFY_FIELD_TOTAL_PAGES   : dwTotalPages_ := Info.NotifyData.adwData[0];
    JOB_NOTIFY_FIELD_BYTES_PRINTED : dwBytesPrinted_ := Info.NotifyData.adwData[0];
    JOB_NOTIFY_FIELD_TOTAL_BYTES   : dwTotalBytes_ := Info.NotifyData.adwData[0];
    JOB_NOTIFY_FIELD_STATUS        :
    begin
      dwStatus_ := Info.NotifyData.adwData[0];
//      if (dwStatus_ and JOB_STATUS_SPOOLING) <> 0 then
//      begin
//        SetPrtJob(JOB_CONTROL_PAUSE);
//        dwStatus_ := dwStatus_ + 0;
//      end;
    end;
//    else
//    begin
//      _Trace('%X = %s', [Info.Field, PChar(Info.NotifyData.Data.pBuf)]);
//    end;
  end;
end;

function TPrtJobInfo.IsSpooling: Boolean;
begin
  Result := (dwStatus_ and JOB_STATUS_SPOOLING) <> 0;
end;

function TPrtJobInfo.IsSpooling2: Boolean;
var
  dwNeed: DWORD;
  pBuf: TBytes;
begin
  Result := false;

  try
    if hPrtHandle_ = 0 then
      exit;

    dwNeed := 0;
    GetJob(hPrtHandle_, dwID_, 2, nil, 0, @dwNeed);
  //  if not GetJob(hPrtHandle_, dwID_, 1, pBuf, 0, @dwNeed) then
  //    exit;
    if dwNeed = 0 then
      exit;

    SetLength(pBuf, dwNeed);
    if GetJob(hPrtHandle_, dwID_, 2, pBuf, dwNeed, @dwNeed) then
    begin
      Result := (PJOB_INFO_2(pBuf).dwStatus and JOB_STATUS_SPOOLING) <> 0;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. IsSpooling2()');
  end;
end;

function TPrtJobInfo.GetJobDevInfo(var aInfo: TPrtJobDevInfo): Boolean;
var
  dwNeed: DWORD;
  pBuf: TBytes;
begin
  Result := false;
  Finalize(aInfo);
  ZeroMemory(@aInfo, SizeOf(aInfo));

  try
    if hPrtHandle_ = 0 then
      exit;

    dwNeed := 0;
    GetJob(hPrtHandle_, dwID_, 2, nil, 0, @dwNeed);
    if dwNeed = 0 then
      exit;

    SetLength(pBuf, dwNeed);
    if GetJob(hPrtHandle_, dwID_, 2, pBuf, dwNeed, @dwNeed) then
    begin
      aInfo.sDocName := PJOB_INFO_2(pBuf).pDocument;
      aInfo.sPtrName := PJOB_INFO_2(pBuf).pPrinterName;
      aInfo.sDrvName := PJOB_INFO_2(pBuf).pDriverName;
      aInfo.bColor := PJOB_INFO_2(pBuf).pDevMode.dmColor = DMCOLOR_COLOR;
      aInfo.dwTotalPage := PJOB_INFO_2(pBuf).dwTotalPages;
      aInfo.dwCopyCount := PJOB_INFO_2(pBuf).pDevMode.dmCopies;
      aInfo.dwScale := PJOB_INFO_2(pBuf).pDevMode.dmScale;
      if aInfo.dwScale = 0 then
        aInfo.dwScale := 100;
      aInfo.bPaperV := PJOB_INFO_2(pBuf).pDevMode.dmOrientation = 1;
      aInfo.sPrintProcessor := PJOB_INFO_2(pBuf).pPrintProcessor;
      aInfo.sDataType := PJOB_INFO_2(pBuf).pDatatype;
      aInfo.dwPaperSizeT := PJOB_INFO_2(pBuf).pDevMode.dmPaperSize;

      case aInfo.dwPaperSizeT of
        DMPAPER_LETTER        : aInfo.sPaperInfo := 'Letter 8 1/2 x 11 in';
        DMPAPER_LETTERSMALL   : aInfo.sPaperInfo := 'Letter Small 8 1/2 x 11 in';
        DMPAPER_TABLOID       : aInfo.sPaperInfo := 'Tabloid 11 x 17 in';
        DMPAPER_LEDGER        : aInfo.sPaperInfo := 'Ledger 17 x 11 in';
        DMPAPER_LEGAL         : aInfo.sPaperInfo := 'Legal 8 1/2 x 14 in';
        DMPAPER_A3            : aInfo.sPaperInfo := 'A3 297 x 420 mm';
        DMPAPER_A4            : aInfo.sPaperInfo := 'A4 210 x 297 mm';
        DMPAPER_A4SMALL       : aInfo.sPaperInfo := 'A4 Small 210 x 297 mm';
        DMPAPER_A5            : aInfo.sPaperInfo := 'A5 148 x 210 mm';
        DMPAPER_B4            : aInfo.sPaperInfo := 'B4 (JIS) 250 x 354';
        DMPAPER_B5            : aInfo.sPaperInfo := 'B5 (JIS) 182 x 257 mm';
        DMPAPER_FOLIO         : aInfo.sPaperInfo := 'Folio 8 1/2 x 13 in';
        else aInfo.sPaperInfo := Format('ETC (%d) %d x %d',
          [aInfo.dwPaperSizeT, PJOB_INFO_2(pBuf).pDevMode.dmPaperWidth, PJOB_INFO_2(pBuf).pDevMode.dmPaperLength]);
      end;

  //    aInfo.sPaperInfo
      try
        CopyMemory(@aInfo.DevMode,  PJOB_INFO_2(pBuf).pDevMode, SizeOf(aInfo.DevMode));
      except
      // ..
      end;
      Result := true;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetJobDevInfo()');
  end;
end;

{ TThdPrtSpoolWatch }

Constructor TThdPrtSpoolWatch.Create(bSync: Boolean; sDeviceName: String; dwWatchMod: DWORD = PRINTER_CHANGE_ALL);

  procedure InitJobFields;
  begin
    hPrinter_ := 0;
    nLastError_ := ERROR_SUCCESS;
    evPrtChangeNotifyEvent_ := nil;
    evPrtNotifyJobInfoEvent_ := nil;

    arrJobFields_[0] := JOB_NOTIFY_FIELD_PRINTER_NAME;
    arrJobFields_[1] := JOB_NOTIFY_FIELD_MACHINE_NAME;
    arrJobFields_[2] := JOB_NOTIFY_FIELD_PORT_NAME;
    arrJobFields_[3] := JOB_NOTIFY_FIELD_USER_NAME;
    arrJobFields_[4] := JOB_NOTIFY_FIELD_NOTIFY_NAME;
    arrJobFields_[5] := JOB_NOTIFY_FIELD_DATATYPE;
    arrJobFields_[6] := JOB_NOTIFY_FIELD_PRINT_PROCESSOR;
    arrJobFields_[7] := JOB_NOTIFY_FIELD_PARAMETERS;
    arrJobFields_[8] := JOB_NOTIFY_FIELD_DRIVER_NAME;
    arrJobFields_[9] := JOB_NOTIFY_FIELD_DEVMODE;
    arrJobFields_[10] := JOB_NOTIFY_FIELD_STATUS;
    arrJobFields_[11] := JOB_NOTIFY_FIELD_STATUS_STRING;
    arrJobFields_[12] := JOB_NOTIFY_FIELD_DOCUMENT;
    arrJobFields_[13] := JOB_NOTIFY_FIELD_PRIORITY;
    arrJobFields_[14] := JOB_NOTIFY_FIELD_POSITION;
    arrJobFields_[15] := JOB_NOTIFY_FIELD_SUBMITTED;
    arrJobFields_[16] := JOB_NOTIFY_FIELD_START_TIME;
    arrJobFields_[17] := JOB_NOTIFY_FIELD_UNTIL_TIME;
    arrJobFields_[18] := JOB_NOTIFY_FIELD_TIME;
    arrJobFields_[19] := JOB_NOTIFY_FIELD_TOTAL_PAGES;
    arrJobFields_[20] := JOB_NOTIFY_FIELD_PAGES_PRINTED;
    arrJobFields_[21] := JOB_NOTIFY_FIELD_TOTAL_BYTES;
    arrJobFields_[22] := JOB_NOTIFY_FIELD_BYTES_PRINTED;

    arrPNOT_[0].wType := JOB_NOTIFY_TYPE;
    arrPNOT_[0].Reserved0 := 0;
    arrPNOT_[0].Reserved1 := 0;
    arrPNOT_[0].Reserved2 := 0;
    arrPNOT_[0].Count := SizeOf(arrJobFields_) div SizeOf(arrJobFields_[0]);
    arrPNOT_[0].pFields := @arrJobFields_;

    ZeroMemory(@PNO_, SizeOf(PNO_));
    PNO_.Version := 2;
    PNO_.Flags := PRINTER_NOTIFY_OPTIONS_REFRESH;
    PNO_.Count := SizeOf(arrPNOT_) div SizeOf(arrPNOT_[0]);
    PNO_.pTypes := @arrPNOT_;
  end;

//var
//  PD: PRINTER_DEFAULTS;

begin
  Inherited Create;

  bSync_ := bSync;
  FreeOnTerminate := true;

  JobList_ := TDictionary<DWORD,TPrtJobInfo>.Create;
  JobList_.OnValueNotify := OnJobValue;

  dwChangeMod_ := dwWatchMod;
  sDeviceName_ := sDeviceName;

  InitJobFields;

  hPrtChangeEvent_ := INVALID_HANDLE_VALUE;
  hPrinter_ := 0;

//  PD.pDatatype := nil;
//  PD.pDevMode := nil;
//  PD.DesiredAccess := PRINTER_ALL_ACCESS;

  if OpenPrinter(PChar(sDeviceName_), hPrinter_, nil{@PD}) then
  begin
    hPrtChangeEvent_ := FindFirstPrinterChangeNotification(hPrinter_,
                                                           dwChangeMod_,
                                                           0,
                                                           @PNO_);
    if hPrtChangeEvent_ = INVALID_HANDLE_VALUE then
    begin
      nLastError_ := 2;
      exit;
    end;

    sSpoolDir_ := GetPrinterSpoolDir(hPrinter_);
    if sSpoolDir_ = '' then
      nLastError_ := 3;
  end else
    nLastError_ := 1;
end;

Destructor TThdPrtSpoolWatch.Destroy;
begin
  if hPrtChangeEvent_ <> INVALID_HANDLE_VALUE then
    FindClosePrinterChangeNotification(hPrtChangeEvent_);

  if hPrinter_ <> 0 then
    ClosePrinter(hPrinter_);

  FreeAndNil(JobList_);

  Inherited;
end;

procedure TThdPrtSpoolWatch.StopThread;
begin
  Inherited;

  if hPrtChangeEvent_ <> 0 then  
    SetEvent(hPrtChangeEvent_);
end;

procedure TThdPrtSpoolWatch.OnJobValue(Sender: TObject; const Item: TPrtJobInfo;
              Action: TCollectionNotification);
begin
  case Action of
    cnAdded: ;
    cnRemoved: Item.Free;
    cnExtracted: ;
  end;
end;

procedure TThdPrtSpoolWatch.ProcessChangeEvent;
begin
  if Assigned(evPrtChangeNotifyEvent_) then
    evPrtChangeNotifyEvent_(Self, dwChangeMod_);
end;

procedure TThdPrtSpoolWatch.ProcessNotifyJobInfoEvent;
begin
  if Assigned(evPrtNotifyJobInfoEvent_) then
    evPrtNotifyJobInfoEvent_(Self, PrtJobInfo_);
end;

procedure TThdPrtSpoolWatch.Execute;

  procedure DeletePrtJob;
  var
    dwNumJobs,
    dwByteNeeded: DWORD;
    pJobs: PPrtJobs;
    pJob: TJobInfo1;
    p: TPrtJobInfo;
    n, c: Integer;
    bFind: Boolean;
    enum: TEnumerator<TPrtJobInfo>;
  begin
    EnumJobs(hPrinter_, 0, MAX_JOB, 1, nil, 0, dwByteNeeded, dwNumJobs);
    pJobs := AllocMem(dwByteNeeded);
    try
      dwNumJobs := 0;
      if EnumJobs(hPrinter_, 0, MAX_JOB, 1, pJobs, 0, dwByteNeeded, dwNumJobs) then
      begin
        if dwNumJobs > 0 then
        begin
          bFind := false;

          Guard(enum, JobList_.Values.GetEnumerator);
          while enum.MoveNext do
          begin
            p := enum.Current;

            for c := 0 to dwNumJobs - 1 do
            begin
              pJob := pJobs[c];
              if pJob.JobId = p.ID then
              begin
                bFind := true;
                break;
              end;
            end;

            if not bFind then
            begin
              JobList_.Remove(p.dwID_);
              exit;
            end;
          end;
        end else
          JobList_.Clear;
      end;

    finally
      FreeMem(pJobs, dwByteNeeded);
    end;
  end;

var
  i: Integer;
  dwJobID,
  dwOldFlags: DWORD;
  PNI: PPrinterNotifyInfo;
  pData: PPrinterNotifyInfoData;
begin
  while not Terminated and not bWorkStop_ and (hPrtChangeEvent_ <> INVALID_HANDLE_VALUE) do
  begin
    PNI := nil;
    dwChangeMod_ := 0;
    try
      case WaitForSingleObject(hPrtChangeEvent_, INFINITE) of
        WAIT_OBJECT_0 :
        begin
          if Terminated or bWorkStop_ then
            break;

          if not FindNextPrinterChangeNotification(hPrtChangeEvent_,
                                                   dwChangeMod_,
                                                   @PNO_,
                                                   Pointer(PNI)) then
          begin
            _Trace('Fail .. FindNextPrinterChangeNotification(), PrtName=%s, Error=%d', [sDeviceName_, GetLastError]);
            continue;
          end;

          if Assigned(evPrtChangeNotifyEvent_) then
          begin
            if bSync_ then
              Synchronize(ProcessChangeEvent)
            else
              evPrtChangeNotifyEvent_(Self, dwChangeMod_);
          end;

          if PNI <> nil then
            try
              if (PNI.Flags and PRINTER_NOTIFY_INFO_DISCARDED) <> 0 then
              begin
                dwOldFlags := PNO_.Flags;
                PNO_.Flags := PRINTER_NOTIFY_OPTIONS_REFRESH;

                FreePrinterNotifyInfo(PNI);

                FindNextPrinterChangeNotification(hPrtChangeEvent_,
                                                  dwChangeMod_,
                                                  @PNO_,
                                                  Pointer(PNI));

                PNO_.Flags := dwOldFlags;
              end;

              PrtJobInfo_ := nil;

              for i := 0 to Integer(PNI.Count) - 1 do
              begin
                pData := PPrinterNotifyInfoData(ULONGLONG(@PNI.aData) + (SizeOf(TPrinterNotifyInfoData) * i));
                dwJobID := pData.Id;
              {$IFDEF DEBUG}
                ASSERT(pData.wType = JOB_NOTIFY_TYPE);
              {$ENDIF}
                if JobList_.ContainsKey(dwJobID) then
                  PrtJobInfo_ := JobList_[dwJobID]
                else
                  PrtJobInfo_ := nil;

                if not Assigned(PrtJobInfo_) then
                begin
                {$IFDEF DEBUG}
                  ASSERT(not (dwChangeMod_ and PRINTER_CHANGE_ADD_JOB) <> 0);
                {$ELSE}
                  if not ((dwChangeMod_ and PRINTER_CHANGE_ADD_JOB) <> 0) then
                    break;
                {$ENDIF}
                  PrtJobInfo_ := TPrtJobInfo.Create(hPrinter_, dwJobID);
                  JobList_.Add(dwJobID, PrtJobInfo_);
                end else begin
                  PrtJobInfo_.PtrJobState := jsWork;
                // job이 해제되는걸 이런식으로 판단하게 한다..
                // 안그러면 계속 쌓이는데 job이 종료되는 시점을 분간할 방법이 없네;; 2011-07-25 kku
                  if (dwChangeMod_ and PRINTER_CHANGE_DELETE_JOB) <> 0 then
                  begin
                    PrtJobInfo_.PtrJobState := jsDelete;
                  end;
                  PrtJobInfo_.UpdatePrtFieldInfo(pData^);

                // JOB_NOTIFY_FIELD_SUBMITTED 이거 올때까지 값을 채워준다. 22_0719 13:18:58 kku
                  if pData.Field <> JOB_NOTIFY_FIELD_SUBMITTED then
                    continue;

                  if Assigned(evPrtNotifyJobInfoEvent_) then
                  begin
                    if bSync_ then
                      Synchronize(ProcessNotifyJobInfoEvent)
                    else
                      evPrtNotifyJobInfoEvent_(Self, PrtJobInfo_);
                  end;
                end;
              end;

              if (dwChangeMod_ and PRINTER_CHANGE_DELETE_JOB) <> 0 then
                DeletePrtJob;
            finally
              FreePrinterNotifyInfo(PNI);
              PNI := nil;
            end;
        end else break;
      end;
    except
      on E: Exception do
        ETgException.TraceException(Self, E, 'Fail .. Execute()');
    end;
    Sleep(50);
  end;
end;

{ TTgPrtSpoolWatch }

Constructor TTgPrtSpoolWatch.Create(bSync: Boolean; dwWatchMod: DWORD = PRINTER_CHANGE_ALL);
begin
  Inherited Create;
  bIsWatch_ := false;
  bSync_ := bSync;
  dwWatchMod_ := dwWatchMod;
  PrtList_ := TStringList.Create;
  PrtList_.CaseSensitive := false;
  WatchThdList_ := TList<TThdPrtSpoolWatch>.Create;
  WatchThdList_.OnNotify := OnPrtNotify;
end;

Destructor TTgPrtSpoolWatch.Destroy;
begin
  StopPrtWatch;
  Inherited;
  FreeAndNil(WatchThdList_);
  FreeAndNil(PrtList_);
end;

procedure TTgPrtSpoolWatch.OnPrtNotify(Sender: TObject; const Item: TThdPrtSpoolWatch; Action: TCollectionNotification);
var
  nStep: Integer;
begin
  nStep := 0;
  try
    case Action of
      cnAdded: ;
      cnRemoved:
      begin
        nStep := 1;
        Item.StopThread;
        nStep := 2;
        Item.Terminate;
        nStep := 3;
  //      Item.Free;
      end;
      cnExtracted: ;
    end;
  except
    on E: Exception do
      ETgException.TraceException(Self, E, 'Fail .. OnPrtNotify() .. Step=%d', [nStep]);
  end;
end;

procedure TTgPrtSpoolWatch.StartPrtWatch;
var
  i: Integer;
  thd: TThdPrtSpoolWatch;
begin
  try
    if Terminated or GetWorkStop then
      exit;

    if not bIsWatch_ then
    begin
      _Trace('StartPrtWatch() ..', 5);
      PrtList_.Clear;
      PrtList_.AddStrings(Printer.Printers);

      for i := 0 to PrtList_.Count - 1 do
      begin
        thd := TThdPrtSpoolWatch.Create(bSync_, PrtList_[i], dwWatchMod_);
        if thd.LastError = ERROR_SUCCESS then
        begin
          WatchThdList_.Add(thd);

          thd.OnPrtChangeNotifyEvent := evPrtChangeNotifyEvent_;
          thd.OnPrtNotifyJobInfoEvent := evPtrNotifyInfoEvent_;
          thd.StartThread;
        end else
          FreeAndNil(thd);
      end;

      bIsWatch_ := true;
      _Trace('StartPrtWatch() .. OK', 5);
    end;
  except
    on E: Exception do
      ETgException.TraceException(Self, E, 'Fail .. StartWatch()');
  end;
end;

procedure TTgPrtSpoolWatch.StopPrtWatch;
begin
  try
    if Terminated or GetWorkStop then
      exit;

    if bIsWatch_ then
    begin
      _Trace('StopPrtWatch() ..', 5);
      bIsWatch_ := false;
      WatchThdList_.Clear;
      _Trace('StopPrtWatch() .. OK', 5);
    end;
  except
    on E: Exception do
      ETgException.TraceException(Self, E, 'Fail .. StopWatch()');
  end;
end;

procedure TTgPrtSpoolWatch.Execute;
var
  llTick,
  llChkTick: ULONGLONG;
  ChkPrtList: TStringList;
  i: Integer;
begin
  llChkTick := GetTickCount64;
  Guard(ChkPrtList, TStringList.Create);
  ChkPrtList.CaseSensitive := false;
  while not Terminated and not GetWorkStop do
  begin
    try
      case GetServiceStatus('Spooler') of
        SERVICE_RUNNING :
        begin
          if bIsWatch_ then
          begin
            llTick := GetTickCount64;
            if (llTick - llChkTick) >= 3000 then // 3초에 한번 프린터 변동사항 체크 25_0827 10:31:17 kku
            begin
              llChkTick := llTick;

              var Prt: TPrinter;
              Guard(Prt, TPrinter.Create);
              ChkPrtList.Clear;
              ChkPrtList.AddStrings(Prt.Printers);

              if PrtList_.Count <> ChkPrtList.Count then
              begin
              // 추가된 프린터 체크
                for i := ChkPrtList.Count - 1 downto 0 do
                begin
                  if PrtList_.IndexOf(ChkPrtList[i]) = -1 then
                  begin
                    _Trace('Printer 추가됨 .. Name=%s', [ChkPrtList[i]], 1);
                    var thd: TThdPrtSpoolWatch := TThdPrtSpoolWatch.Create(bSync_, ChkPrtList[i], dwWatchMod_);
                    if thd.LastError = ERROR_SUCCESS then
                    begin
                      WatchThdList_.Add(thd);

                      thd.OnPrtChangeNotifyEvent := evPrtChangeNotifyEvent_;
                      thd.OnPrtNotifyJobInfoEvent := evPtrNotifyInfoEvent_;
                      thd.StartThread;
                    end else begin
                      _Trace('Fail .. Printer 감시 활성화 실패 .. Name=%s', [ChkPrtList[i]], 1);
                      ChkPrtList.Delete(i);
                      FreeAndNil(thd);
                    end;
                  end;
                end;

              // 제거된 프린터 체크
                for i := 0 to PrtList_.Count - 1 do
                begin
                  if ChkPrtList.IndexOf(PrtList_[i]) = -1 then
                  begin
                    var c: Integer;
                    for c := 0 to WatchThdList_.Count - 1 do
                    begin
                      if CompareText(WatchThdList_[c].sDeviceName_, PrtList_[i]) = 0 then
                      begin
                        _Trace('Printer 제거됨 .. Name=%s', [PrtList_[i]], 1);
                        WatchThdList_.Delete(c);
                        break;
                      end;
                    end;
                  end;
                end;

                PrtList_.Clear;
                PrtList_.AddStrings(ChkPrtList);
              end;
            end;
          end else begin
            _Trace('Print Spooler 서비스 감지됨.', 1);
            StartPrtWatch;
          end;
        end;
        SERVICE_STOPPED :
        begin
          if bIsWatch_ then
          begin
            _Trace('Print Spooler 서비스 중지됨.', 1);
            StopPrtWatch;
          end;
        end;
      end;

      Sleep(1000);
    except
      on E: Exception do
        ETgException.TraceException(Self, E, 'Fail .. Execute()');
    end;
  end;
end;

//------------------------------------------------------------------------------

{ TPrintersInfo }

Constructor TPrintersInfo.Create;
begin
  Inherited Create;
  PrtInfoList_ := TPrtInfoList.Create;
  PrtInfoList_.OnNotify := OnPrtInfoNotify;
end;

Destructor TPrintersInfo.Destroy;
begin
  FreeAndNil(PrtInfoList_);
  Inherited;
end;

procedure TPrintersInfo.OnPrtInfoNotify(Sender: TObject; const Item: PPrinterInfo; Action: TCollectionNotification);
begin
  if Action = cnRemoved then
    Dispose(Item);
end;

function TPrintersInfo.GetPrtInfoByPrtName(const sPrtName: String): PPrinterInfo;
var
  i: Integer;
begin
  Result := nil;
  try
    for i := 0 to PrtInfoList_.Count - 1 do
    begin
      if CompareText(sPrtName, PrtInfoList_[i].sPrtName) = 0 then
      begin
        Result := PrtInfoList_[i];
        exit;
      end;
    end;
  except
    on E: Exception do
      ETgException.TraceException(Self, E, 'Fail .. GetPrtInfoByPrtName()');
  end;
end;

procedure TPrintersInfo.RefreshList;
var
  pEnt: PPrinterInfo;
  Reg: TRegistry;

  procedure FillPrinterDetails;
  var
    sRegKey: string;
    printHandle: THandle;
    pPrinterInfo: PPrinterInfo2;
    bytesNeeded: DWORD;
  begin
    try
      Reg.CloseKey;
      sRegKey := 'SYSTEM\CurrentControlSet\Control\Print\Printers\' + pEnt.sPrtName;
      if Reg.OpenKeyReadOnly(sRegKey) then
      begin
        pEnt.sDrvName := Reg.ReadString('Printer Driver');
        pEnt.sPortName := Reg.ReadString('Port');
      end;

      // WSD ports
      if pEnt.sPortName <> '' then
      begin
        Reg.CloseKey;
        sRegKey := 'SYSTEM\CurrentControlSet\Control\Print\Monitors\WSD Port\Ports\' + pEnt.sPortName;
        if Reg.KeyExists(sRegKey) then
        begin
          pEnt.sIp := pEnt.sPortName;
          pEnt.PortType := PTWsd;
        end else begin
          Reg.CloseKey;
          sRegKey := 'SYSTEM\CurrentControlSet\Control\Print\Monitors\Standard TCP/IP Port\Ports\' + pEnt.sPortName;
          if Reg.OpenKeyReadOnly(sRegKey) then
          begin
            pEnt.PortType := PTTcpIp;
          end
        end;
      end;

      //절약 모드 및 공유 PC 여부 확인
      pPrinterInfo:= nil;
      try
        if not OpenPrinter(PChar(pEnt.sPrtName), printHandle, nil) then
          Exit;

        GetPrinter(printHandle, 2, nil, 0, @bytesNeeded);
        if (GetLastError <> ERROR_INSUFFICIENT_BUFFER) or (bytesNeeded = 0) then
          Exit;

        GetMem(pPrinterInfo, bytesNeeded);
        if GetPrinter(printHandle, 2, pPrinterInfo, bytesNeeded, @bytesNeeded) then
        begin
          pEnt.dwStatus := pPrinterInfo.Status;

          if ((pPrinterInfo.Attributes and PRINTER_ATTRIBUTE_NETWORK) <> 0) and
             ((pPrinterInfo.Attributes and PRINTER_ATTRIBUTE_SHARED) <> 0) and
             (pPrinterInfo.pShareName <> nil) then
          begin
            pEnt.PortType := PTShared;
            pEnt.sIp:= pPrinterInfo.pShareName;
          end;

          if (pEnt.dwStatus and PRINTER_STATUS_POWER_SAVE) <> 0 then
            pEnt.bIsPowerSaveMode:= True;
        end;

        if pEnt.PortType = ptUnknown then
        begin
          pEnt.PortType := PTLocal;
        end;
      finally
        if pPrinterInfo <> nil then
          FreeMem(pPrinterInfo);
      end;
    except
      on E: Exception do
        ETgException.TraceException(Self, E, 'Fail .. RefreshList() .. FillPrinterDetails()');
    end;
  end;

var
  pPrinters, pCurrentPrinter: PPrinterInfo2;
  dwPrinters, dwReturned: DWORD;
  i: Integer;
  portTypeStr: string;
  debug: string;
begin
  try
    PrtInfoList_.Clear;

    EnumPrinters(PRINTER_ENUM_LOCAL or PRINTER_ENUM_CONNECTIONS, nil, 2, nil, 0, dwPrinters, dwReturned);
    if dwPrinters = 0 then
    begin
      _Trace('Fail .. RefreshList() .. EnumPrinters()', 1);
      exit;
    end;

    Guard(Reg, TRegistry.Create);
    Reg.RootKey := HKEY_LOCAL_MACHINE;
    GetMem(pPrinters, dwPrinters);
    try
      if EnumPrinters(PRINTER_ENUM_LOCAL or PRINTER_ENUM_CONNECTIONS, nil, 2, pPrinters, dwPrinters, dwPrinters, dwReturned) then
      begin
        pCurrentPrinter := pPrinters;
        for i := 0 to dwReturned - 1 do
        begin
          New(pEnt);
          ZeroMemory(pEnt, SizeOf(TPrinterInfo));
          pEnt.sPrtName := pCurrentPrinter.pPrinterName;
          pEnt.sDrvName := pCurrentPrinter.pDriverName;
          pEnt.dwStatus := pCurrentPrinter.Status;
          pEnt.sPortName := pCurrentPrinter.pPortName;
          FillPrinterDetails;

          PrtInfoList_.Add(pEnt);

          //디버그 로깅
//          mmo1.Lines.Add(Format('--- Printer [%d]: %s ---', [i, SafePChar(pCurrentPrinter.pPrinterName)]));
//          mmo1.Lines.Add(Format('  pServerName: %s', [SafePChar(pCurrentPrinter.pServerName)]));
//          mmo1.Lines.Add(Format('  pShareName: %s', [SafePChar(pCurrentPrinter.pShareName)]));
//          mmo1.Lines.Add(Format('  pPortName: %s', [SafePChar(pCurrentPrinter.pPortName)]));
//          mmo1.Lines.Add(Format('  pDriverName: %s', [SafePChar(pCurrentPrinter.pDriverName)]));
//          mmo1.Lines.Add(Format('  pComment: %s', [SafePChar(pCurrentPrinter.pComment)]));
//          mmo1.Lines.Add(Format('  pLocation: %s', [SafePChar(pCurrentPrinter.pLocation)]));
//          mmo1.Lines.Add(Format('  Attributes: 0x%x', [pCurrentPrinter.Attributes]));
//          mmo1.Lines.Add(Format('  Status: 0x%x', [pCurrentPrinter.Status]));
//          mmo1.Lines.Add(Format('  cJobs: %d', [pCurrentPrinter.cJobs]));
//          mmo1.Lines.Add(Format('--------------------', []));

//          case printerDetails.portType of
//            PTLocal: portTypeStr := '로컬';
//            PTTcpIp: portTypeStr := 'TCP/IP';
//            PTWsd: portTypeStr := 'WSD PORT';
//            PTShared: portTypeStr := '공유PC';
//          else
//            portTypeStr := '알 수 없음';
//          end;
//
//          listItem := lvPrinters.Items.Add;
//          listItem.Caption := printerDetails.deviceName;
//          listItem.SubItems.Add(portTypeStr);
//          listItem.SubItems.Add(printerDetails.ip);
//          if printerDetails.isPowerSaveMode then
//            listItem.SubItems.Add('예')
//          else
//            listItem.SubItems.Add('아니요');
//          listItem.SubItems.Add(printerDetails.driverName);
//          listItem.SubItems.Add(printerDetails.portName);
//          listItem.SubItems.Add(Format('0x%x', [printerDetails.status]));

          Inc(pCurrentPrinter);
        end;
      end;
    finally
      FreeMem(pPrinters);
    end;
  except
    on E: Exception do
      ETgException.TraceException(Self, E, 'Fail .. RefreshList()');
  end;
end;

procedure TPrintersInfo.SaveToFile(const sPath: String);
var
  O, OA: ISuperObject;
  i: Integer;
begin
  try
    OA := TSuperObject.Create(stArray);

    for i := 0 to PrtInfoList_.Count - 1 do
    begin
      OA.AsArray.Add(TTgJson.ValueToJsonObject<TPrinterInfo>(PrtInfoList_[i]^));
    end;

    O := SO;
    O.O['List'] := OA;
    SaveJsonObjToFile(O, sPath);
  except
    on E: Exception do
      ETgException.TraceException(Self, E, 'Fail .. SaveToFile()');
  end;
end;

procedure TPrintersInfo.LoadFromFile(const sPath: String);
var
  O: ISuperObject;
  i: Integer;
  pEnt: PPrinterInfo;
begin
  try
    PrtInfoList_.Clear;
    if LoadJsonObjFromFile(O, sPath) then
    begin
      if (O.O['List'] = nil) or (O.O['List'].DataType <> stArray) then
        exit;

      for i := 0 to O.A['List'].Length - 1 do
      begin
        New(pEnt);
//        ZeroMemory(pEnt, SizeOf(PPrinterInfo));
        pEnt^ := TTgJson.GetDataAsType<TPrinterInfo>(O.A['List'].O[i]);
        PrtInfoList_.Add(pEnt);
      end;
    end;
  except
    on E: Exception do
      ETgException.TraceException(Self, E, 'Fail .. SaveToFile()');
  end;
end;

//------------------------------------------------------------------------------

function GetDefaultPrinterName: string;
var
  arrBuf: array[0..255] of Char;
  dwSize: DWORD;
begin
  dwSize := SizeOf(arrBuf);
  if GetDefaultPrinter(@arrBuf, @dwSize) then
    Result := StrPas(arrBuf)
  else
    Result := 'Unknown';
end;

function GetProcessNameByPrtDocName(sDocName: String): String;
var
  sExt: String;
begin
  Result := '';
  try
    sDocName := LowerCase(sDocName);
    if sDocName.StartsWith('microsoft powerpoint -') then
    begin
      Result := 'POWERPNT.EXE';
      exit;
    end else
    if sDocName.StartsWith('microsoft word -') then
    begin
      Result := 'WINWORD.EXE';
      exit;
    end;

    sExt := GetFileExt(sDocName);
    if (sExt = 'xls') or (sExt = 'xlsx') then
    begin
      Result := 'EXCEL.EXE';
      exit;
    end else
    if (sExt = 'hwp') or (sExt = 'hwpx') then
    begin
      Result := 'hwp.exe';
      exit;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetProcessNameByPrtDocName()');
  end;
end;

function PrinterDriverToName(sDrvName: String): String;
var
  dwFlags: DWORD;
  pBuf: TBytes;
  dwNeed, dwPrtCnt: DWORD;
  pInfo: PPrinterInfo2;
  i: Integer;
begin
  Result := 'Unknown';
  try
    dwNeed := 0;
    dwPrtCnt := 0;
    dwFlags := PRINTER_ENUM_LOCAL or PRINTER_ENUM_CONNECTIONS;
    if not EnumPrinters(dwFlags, nil, 2, nil, 0, dwNeed, dwPrtCnt) then
    begin
//      LogToReg('PrinterDriverToName2222-01 .. Error', IntToStr(GetLastError));
//      exit;
    end;
    if dwNeed > 0 then
    begin
      SetLength(pBuf, dwNeed);
    end else exit;
    if not EnumPrinters(dwFlags, nil, 2, @pBuf[0], dwNeed, dwNeed, dwPrtCnt) then
    begin
      exit;
    end;

    if dwPrtCnt > 0 then
    begin
      pInfo := PPrinterInfo2(@pBuf[0]);
      for i := 0 to dwPrtCnt - 1 do
      begin
        if CompareText(pInfo.pDriverName, sDrvName) = 0 then
        begin
          Result := StrPas(pInfo.pPrinterName);
          exit;
        end;
        Inc(pInfo);
      end;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. PrinterDriverToName()');
  end;
end;

function PrinterDriverToIP(sDrvName: String): String;
var
  dwFlags: DWORD;
  pBuf: TBytes;
  dwNeed, dwPrtCnt: DWORD;
  pInfo: PPrinterInfo2;
  i: Integer;
begin
  Result := 'Unknown';
  try
    dwNeed := 0;
    dwPrtCnt := 0;
    dwFlags := PRINTER_ENUM_LOCAL or PRINTER_ENUM_CONNECTIONS;
    if not EnumPrinters(dwFlags, nil, 2, nil, 0, dwNeed, dwPrtCnt) then
    begin
//      LogToReg('PrinterDriverToName2222-01 .. Error', IntToStr(GetLastError));
//      exit;
    end;
    if dwNeed > 0 then
    begin
      SetLength(pBuf, dwNeed);
    end else exit;
    if not EnumPrinters(dwFlags, nil, 2, @pBuf[0], dwNeed, dwNeed, dwPrtCnt) then
    begin
      exit;
    end;

    if dwPrtCnt > 0 then
    begin
      pInfo := PPrinterInfo2(@pBuf[0]);
      for i := 0 to dwPrtCnt - 1 do
      begin
        if CompareText(pInfo.pDriverName, sDrvName) = 0 then
        begin
          Result := StrPas(pInfo.pPortName);
          exit;
        end;
        Inc(pInfo);
      end;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. PrinterDriverToName()');
  end;
end;

function IsPJL(const sPath: String): Boolean;
begin
  try
    if FileExists(sPath) then
      Result := CheckSign(sPath, @SIG_PJL, Length(SIG_PJL))
    else Result := false;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. IsPJL()');
  end;
end;

function GetQtyFromPJL(const sPath: String; var bCollate: Boolean): Integer;
var
  fs: TFileStream;
  pBuf: TBytes;
  sText: AnsiString;
  nRead, nCopy: Integer;
begin
  bCollate := true; // 기본 "한부씩 인쇄 설정"
  Result := 0;
  try
    if not IsPJL(sPath) then
      exit;

    Guard(fs, TFileStream.Create(sPath, fmOpenRead or fmShareDenyNone));
  // 앞부분 4KB만 읽기
    SetLength(pBuf, 4096);
    nRead := fs.Read(pBuf[0], Length(pBuf));
    if nRead = 0 then
      exit;

  // TBytes → AnsiString 변환
    SetString(sText, PAnsiChar(@pBuf[0]), nRead);

    Result := StrToIntDef(GetCapsuleStr('@PJL SET QTY=', #10, UpperCase(sText)), 0);
    nCopy := StrToIntDef(GetCapsuleStr('@PJL SET COPIES=', #10, UpperCase(sText)), 0);
    if nCopy > Result then
    begin
    // 한부씩 인쇄 안함을 사용할 경우 COPIES 값에 부수 정보가 들어가는거 같다? 25_0904 16:51:06 kku
      Result := nCopy;
      bCollate := false;
    end;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. GetQtyFromPJL()');
  end;
end;

function IsPJLAndLanguagePLW(const sPath: string): Boolean;
var
  fs: TFileStream;
  pBuf: TBytes;
  sText: AnsiString;
  nRead: Integer;
begin
  Result := false;
  try
    if not IsPJL(sPath) then
      exit;

    Guard(fs, TFileStream.Create(sPath, fmOpenRead or fmShareDenyNone));
  // 앞부분 4KB만 읽기
    SetLength(pBuf, 4096);
    nRead := fs.Read(pBuf[0], Length(pBuf));
    if nRead = 0 then
      exit;

  // TBytes → AnsiString 변환
    SetString(sText, PAnsiChar(@pBuf[0]), nRead);

  // LANGUAGE=PLW 존재 여부 확인
    if Pos('@PJL ENTER LANGUAGE=PLW', UpperCase(string(sText))) > 0 then
      Result := true;
  except
    on E: Exception do
      ETgException.TraceException(E, 'Fail .. IsPJLAndLanguagePLW()');
  end;
end;

end.
