doc
curtis
2026-03-19 beac355a0f1b5f75020760c36061283e44e9636c
doc
新增35個檔案
21030 ■■■■■ 已變更過的檔案
.idea/.gitignore 8 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
.idea/codeStyles 116 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
.idea/git_toolbox_prj.xml 15 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
.idea/misc.xml 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
.idea/modules.xml 8 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
.idea/vcs.xml 7 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
CB_IMGPSScanImp.ts 694 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
CB_IMGPSScan_TLB.ts 123 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/AI可能讀不到的_TLB_介面.md 44 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/Business_Logic_Analysis.md 64 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/Delphi_套件_EnScan_webAssembly_可行性.md 77 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/Delphi_套件相依.md 115 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/Image_Processer_Analysis.md 69 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/Local_IO_抽像方法列表.md 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/Local_IO_路徑與檔案操作分析.md 233 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/OCX_溝通與_抽象層抽換.md 249 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/ScanImp_源碼分析.md 242 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/Scanner_Controller_Analysis.md 62 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/Transport_Manager_Analysis.md 69 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/UI_View_Analysis.md 86 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/delphi_conventions.md 249 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/index.md 15 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/remote_相關方法.md 修補檔 | 檢視 | 原始 | 究查 | 歷程
doc/curtis/分析_尚進行中.md 830 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
separate/scanImp/CB_IMGPSScanImp_Data.pas 5178 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
separate/scanImp/CB_IMGPSScanImp_Data.ts 819 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
separate/scanImp/CB_IMGPSScanImp_Main.pas 1089 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
separate/scanImp/CB_IMGPSScanImp_Main.ts 17 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
separate/scanImp/CB_IMGPSScanImp_Scan.pas 1050 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
separate/scanImp/CB_IMGPSScanImp_Scan.ts 124 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
separate/scanImp/CB_IMGPSScanImp_UI.pas 6592 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
separate/scanImp/CB_IMGPSScanImp_UI.ts 78 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
separate/scanImp/CB_IMGPSScanImp_Utils.pas 2545 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
separate/scanImp/CB_IMGPSScanImp_Utils.ts 59 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
split_pascal.js 98 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
.idea/.gitignore
比對新檔案
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
.idea/codeStyles
比對新檔案
@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ProjectCodeStyleConfiguration">
    <code_scheme name="Project" version="173">
      <codeStyleSettings language="XML">
        <arrangement>
          <rules>
            <section>
              <rule>
                <match>
                  <AND>
                    <NAME>xmlns:android</NAME>
                    <XML_ATTRIBUTE />
                    <XML_NAMESPACE>^$</XML_NAMESPACE>
                  </AND>
                </match>
              </rule>
            </section>
            <section>
              <rule>
                <match>
                  <AND>
                    <NAME>xmlns:.*</NAME>
                    <XML_ATTRIBUTE />
                    <XML_NAMESPACE>^$</XML_NAMESPACE>
                  </AND>
                </match>
                <order>BY_NAME</order>
              </rule>
            </section>
            <section>
              <rule>
                <match>
                  <AND>
                    <NAME>.*:id</NAME>
                    <XML_ATTRIBUTE />
                    <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                  </AND>
                </match>
              </rule>
            </section>
            <section>
              <rule>
                <match>
                  <AND>
                    <NAME>.*:name</NAME>
                    <XML_ATTRIBUTE />
                    <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                  </AND>
                </match>
              </rule>
            </section>
            <section>
              <rule>
                <match>
                  <AND>
                    <NAME>name</NAME>
                    <XML_ATTRIBUTE />
                    <XML_NAMESPACE>^$</XML_NAMESPACE>
                  </AND>
                </match>
              </rule>
            </section>
            <section>
              <rule>
                <match>
                  <AND>
                    <NAME>style</NAME>
                    <XML_ATTRIBUTE />
                    <XML_NAMESPACE>^$</XML_NAMESPACE>
                  </AND>
                </match>
              </rule>
            </section>
            <section>
              <rule>
                <match>
                  <AND>
                    <NAME>.*</NAME>
                    <XML_ATTRIBUTE />
                    <XML_NAMESPACE>^$</XML_NAMESPACE>
                  </AND>
                </match>
                <order>BY_NAME</order>
              </rule>
            </section>
            <section>
              <rule>
                <match>
                  <AND>
                    <NAME>.*</NAME>
                    <XML_ATTRIBUTE />
                    <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
                  </AND>
                </match>
                <order>ANDROID_ATTRIBUTE_ORDER</order>
              </rule>
            </section>
            <section>
              <rule>
                <match>
                  <AND>
                    <NAME>.*</NAME>
                    <XML_ATTRIBUTE />
                    <XML_NAMESPACE>.*</XML_NAMESPACE>
                  </AND>
                </match>
                <order>BY_NAME</order>
              </rule>
            </section>
          </rules>
        </arrangement>
      </codeStyleSettings>
    </code_scheme>
  </component>
</project>
.idea/git_toolbox_prj.xml
比對新檔案
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="GitToolBoxProjectSettings">
    <option name="commitMessageIssueKeyValidationOverride">
      <BoolValueOverride>
        <option name="enabled" value="true" />
      </BoolValueOverride>
    </option>
    <option name="commitMessageValidationEnabledOverride">
      <BoolValueOverride>
        <option name="enabled" value="true" />
      </BoolValueOverride>
    </option>
  </component>
</project>
.idea/misc.xml
比對新檔案
@@ -0,0 +1,6 @@
<project version="4">
  <component name="MarkdownNavigator.ProfileManager" plain-text-search-scope="Project Files" />
  <component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-11" project-jdk-type="JavaSDK">
    <output url="file://$PROJECT_DIR$/out" />
  </component>
</project>
.idea/modules.xml
比對新檔案
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="ProjectModuleManager">
    <modules>
      <module fileurl="file://$PROJECT_DIR$/.idea/CB_IMGPSScan.iml" filepath="$PROJECT_DIR$/.idea/CB_IMGPSScan.iml" />
    </modules>
  </component>
</project>
.idea/vcs.xml
比對新檔案
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="" vcs="Git" />
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
  </component>
</project>
CB_IMGPSScanImp.ts
比對新檔案
@@ -0,0 +1,694 @@
import * as fs from 'fs';
import * as path from 'path';
// ============================================================================
// 模擬 Delphi 與 VCL/硬體 相依介面 (Node.js 中不存在的實體)
// ============================================================================
export type TObject = any;
export type TMouseButton = 'mbLeft' | 'mbRight' | 'mbMiddle';
export type TShiftState = any;
export type THandle = number;
export type TDibGraphic = any;
export type TTiffGraphic = any;
export type TImageFormat = 'ifBlackWhite' | 'ifGray256' | 'ifTrueColor' | 'ifColor256';
export type TMouseMode = 'mmUser' | 'mmAmplifier' | 'mmZoom' | 'mmDrag' | 'mmR270' | 'mmR180' | 'mmR90' | 'mmDelete';
export type TxActiveFormBorderStyle = number;
export type TxPrintScale = number;
export type TxPopupMode = number;
export type OLE_COLOR = number;
export type IFontDisp = any;
export interface TTreeNode {
    Text: string;
    Level: number;
    ImageIndex: number;
    SelectedIndex: number;
    Parent?: TTreeNode;
    Count: number;
    Item: TTreeNode[];
    Expand(expand: boolean): void;
    Delete(): void;
}
export interface TImageScrollBox {
    Name: string;
    FileName: string;
    Graphic: any;
    DisplayedGraphic: any;
    MouseMode: TMouseMode;
    ZoomMode: any;
    ZoomPercent: number;
    AntiAliased: boolean;
    Parent: any;
    LoadFromFile(filename: string, index: number): void;
    SaveToFile(filename: string): void;
    Redraw(force: boolean): void;
    SetFocus(): void;
    BeginDrag(immediate: boolean): void;
}
export interface TListView {
    Items: { Count: number; Item: any[], Add(): any, Clear(): void };
    Selected: any;
    ItemIndex: number;
    ClearSelection(): void;
    Clear(): void;
}
export interface TTreeView {
    Items: { Add(node: any, text: string): TTreeNode; AddChild(node: any, text: string): TTreeNode; Clear(): void };
    Selected: TTreeNode;
}
// ============================================================================
// 模擬 Delphi TStringList 檔案字串處理功能
// ============================================================================
export class TStringList {
    private _strings: string[] = [];
    get Count(): number { return this._strings.length; }
    get Strings(): string[] { return this._strings; }
    set Strings(value: string[]) { this._strings = value; }
    get Text(): string { return this._strings.join('\n'); }
    set Text(value: string) { this._strings = value.split('\n'); }
    get CommaText(): string { return this._strings.join(','); }
    set CommaText(value: string) { this._strings = value.split(','); }
    public Add(item: string) { this._strings.push(item); }
    public Insert(index: number, item: string) { this._strings.splice(index, 0, item); }
    public Delete(index: number) { this._strings.splice(index, 1); }
    public Clear() { this._strings = []; }
    public IndexOf(item: string): number { return this._strings.indexOf(item); }
    public LoadFromFile(filepath: string) {
        if (fs.existsSync(filepath)) {
            const data = fs.readFileSync(filepath, 'utf8');
            this._strings = data.split(/\r?\n/).filter(line => line.length > 0);
        }
    }
    public SaveToFile(filepath: string) {
        fs.writeFileSync(filepath, this._strings.join('\n'), 'utf8');
    }
    public Sort() { this._strings.sort(); }
    public Assign(source: TStringList) { this._strings = [...source.Strings]; }
}
// ============================================================================
// 列舉與資料結構
// ============================================================================
export enum TTransMode { tsHttp, tsFtp, tsNone }
export enum TScanMode { smNew, smReplace, smInsert, smSample, smRTS }
export enum TFtpProtocol { fpftp, fpftps }
export interface TScanInfo {
    MultiPage: boolean;
    Graphic: TTiffGraphic;
    Stream: any;
    ImageCount: number;
}
export interface TOMRErrInfo {
    Display: boolean;
    Ignore: boolean;
    Info: string;
    Mode: string;
}
export interface TScrollRec {
    HScroll: number;
    VScroll: number;
    Rate: number;
}
export interface TPoint {
    X: number;
    Y: number;
}
export interface TMpsBarcodeinf {
    Count: number;
    Text: string[];
    r180: number[];
}
// ============================================================================
// 主要實作類別 (對應 TCB_IMGPSScanX)
// ============================================================================
export class TCB_IMGPSScanX {
    // ===== 系統全域變數 =====
    private Ch_WriteNote: boolean = false;
    private RejectCase: boolean = false;
    private ErrIndex: number = 0;
    // ===== 預設區 =====
    private Def_DeviceDelete: boolean = true;
    private Def_DeviceDeleteSize: number = 3072;
    private Def_ScannerReverse: boolean = false;
    private Def_BoardClear: boolean = false;
    private Def_ScanDpi: number = 300;
    private Def_ScanDuplex: boolean = true;
    private Def_ScanRotate: number = 0;
    private Def_ScanDeskew: boolean = false;
    private Def_ScanBright: number = 0;
    private Def_ScanContrast: number = 0;
    private Def_ScanImgShowMode: number = 2;
    private Def_ScanImgSetUse: boolean = false;
    // ===== 傳入參數 =====
    private FUrl: string = '';
    private FCaseID: string = '';
    private FMode: string = '';
    private FModeName: string = '';
    private FWork_no: string = '';
    private FUserID: string = '';
    private FUserName: string = '';
    private FUserUnit: string = '';
    private FData: string = '';
    private FVerify: string = '';
    private FReWrite: string = '';
    private FLanguage: string = 'zh_tw';
    private FLoanDoc_Value: string = '';
    private FLoanDoc_Enable: string = '';
    private FUseProxy: string = '';
    private FC_DocNoList: string = '';
    private FC_DocNameList: string = '';
    private FFixFileList: string = '';
    private FIs_In_Wh: string = '';
    private FOldCaseInfo: string = '';
    private FPrintyn: string = '';
    private FIs_OldCase: string = '';
    private FCustDocYN: string = '';
    private FImgDPI: number = 300;
    private FScanColor: number = 0;
    private FFileSizeLimit: number = 5120; // 5 * 1024
    private FCaseNoLength: number = 16;
    private FImgDelete: string = 'N';
    private FIsExternal: string = 'N';
    private FCheck_main_form: string = 'N';
    private FWH_category: string = 'N';
    // ===== 狀態變數 =====
    private TransMode: TTransMode = TTransMode.tsNone;
    private HttpErrStr: string = '';
    private ScanColor: TImageFormat = 'ifBlackWhite';
    private ServerDate: string = '';
    private ServerTime: string = '';
    private Balance: number = 0;
    private ScanDenialTime: string = '';
    private ScanDenialHint: string = '';
    // ===== 路徑區 =====
    private ScanPath: string = '';
    private ImagePath: string = '';
    private ImageSavePath: string = '';
    private ScaniniPath: string = '';
    private LngPath: string = '';
    private CheckXmlPath: string = '';
    private SitePath: string = '';
    private SamplePath: string = '';
    private TransPath: string = '';
    // ===== 清單區 =====
    private Doc_Inf_List: TStringList = new TStringList();
    private DM_FORM_INF_List: TStringList = new TStringList();
    private FORM_INF_List: TStringList = new TStringList();
    private CHECK_RULE_INF_List: TStringList = new TStringList();
    private MEMO_INF_List: TStringList = new TStringList();
    private WORK_INF_List: TStringList = new TStringList();
    private LASTEST_FORM_INF_List: TStringList = new TStringList();
    private FindResult: TStringList = new TStringList();
    private CaseList: TStringList = new TStringList();
    private Context_DocnoList: TStringList = new TStringList();
    private CaseDocNoList: TStringList = new TStringList();
    private CaseDocNo_CopiesList: TStringList = new TStringList();
    private ContextList: TStringList = new TStringList();
    private AttContextList: TStringList = new TStringList();
    private OMRFileList: TStringList = new TStringList();
    private Cust_DocNoList: TStringList = new TStringList();
    private IN_WH_DocNoList: TStringList = new TStringList();
    private FormCode_PageSize: TStringList = new TStringList();
    private DocNo_NeedDoc: TStringList = new TStringList();
    private DocNo_NoDoc: TStringList = new TStringList();
    private DocNo_VerinCase: TStringList = new TStringList();
    private NoSaveBarCodeList: TStringList = new TStringList();
    private FormID_List: TStringList = new TStringList();
    private DocNo_List: TStringList = new TStringList();
    private NowShowFileList: TStringList = new TStringList();
    private NowSelectFileList: TStringList = new TStringList();
    private GuideFormIDList: TStringList = new TStringList();
    private DivPageFormIDList: TStringList = new TStringList();
    private LastInitFormidList: TStringList = new TStringList();
    private LastAddFormidList: TStringList = new TStringList();
    private SampleFormIDList: TStringList = new TStringList();
    private ExistImgList: TStringList = new TStringList();
    private reSizeExistImgList: TStringList = new TStringList();
    // ===== 顯示區變數 =====
    private NowCaseno: string = '';
    private NowDocNo: string = '';
    private NowDocDir: string = '';
    private NowFormCode: string = '';
    private NowFormName: string = '';
    private NowPage: number = 0;
    private DisplayPath: string = '';
    private Case_loandoc: string = '';
    // ===== 其他狀態 =====
    private ScanModeState: TScanMode = TScanMode.smNew;
    private InitialOk: boolean = false;
    private ShowText: string = '';
    private SafePixel: number = 20;
    private OMRErrInfo: TOMRErrInfo[] = []; // length 11
    // UI Mocks
    private TreeView1: TTreeView = {} as TTreeView;
    private PageLV: TListView = {} as TListView;
    private ISB1: TImageScrollBox = {} as TImageScrollBox;
    private DisplayISB: TImageScrollBox = {} as TImageScrollBox;
    private SelectISB: TImageScrollBox = {} as TImageScrollBox;
    private NewTreeNode: TTreeNode = {} as TTreeNode;
    private MyTreeNode1: TTreeNode = {} as TTreeNode;
    private MyTreeNode2: TTreeNode = {} as TTreeNode;
    private MyTreeNode3: TTreeNode = {} as TTreeNode;
    constructor() {
        // Constructor initialization
    }
    // ============================================================================
    // COM 屬性 (Getters & Setters)
    // ============================================================================
    get url(): string { return this.FUrl; }
    set url(value: string) { this.FUrl = value; }
    get caseid(): string { return this.FCaseID; }
    set caseid(value: string) { this.FCaseID = value; }
    get mode(): string { return this.FMode; }
    set mode(value: string) { this.FMode = value.toUpperCase(); }
    get work_no(): string { return this.FWork_no; }
    set work_no(value: string) { this.FWork_no = value; }
    get userid(): string { return this.FUserID; }
    set userid(value: string) { this.FUserID = value; }
    get username(): string { return this.FUserName; }
    set username(value: string) { this.FUserName = value; }
    get userunit(): string { return this.FUserUnit; }
    set userunit(value: string) { this.FUserUnit = value; }
    get data(): string { return this.FData; }
    set data(value: string) { this.FData = value; }
    get verify(): string { return this.FVerify; }
    set verify(value: string) { this.FVerify = value; }
    get rewrite(): string { return this.FReWrite; }
    set rewrite(value: string) { this.FReWrite = value; }
    get modename(): string { return this.FModeName; }
    set modename(value: string) { this.FModeName = value; }
    get language(): string { return this.FLanguage; }
    set language(value: string) { this.FLanguage = value.toLowerCase(); }
    get loandoc_value(): string { return this.FLoanDoc_Value; }
    set loandoc_value(value: string) { this.FLoanDoc_Value = value; }
    get loandoc_enable(): string { return this.FLoanDoc_Enable; }
    set loandoc_enable(value: string) { this.FLoanDoc_Enable = value; }
    get c_docnolist(): string { return this.FC_DocNoList; }
    set c_docnolist(value: string) { this.FC_DocNoList = value; }
    get c_docnamelist(): string { return this.FC_DocNameList; }
    set c_docnamelist(value: string) { this.FC_DocNameList = value; }
    get is_in_wh(): string { return this.FIs_In_Wh; }
    set is_in_wh(value: string) { this.FIs_In_Wh = value.toUpperCase(); }
    get oldcaseinfo(): string { return this.FOldCaseInfo; }
    set oldcaseinfo(value: string) { this.FOldCaseInfo = value; }
    get scancolor(): string { return this.FScanColor.toString(); }
    set scancolor(value: string) {
        this.FScanColor = value ? parseInt(value) : 0;
        this.ScanColor = this.FScanColor === 2 ? 'ifTrueColor' : (this.FScanColor === 1 ? 'ifGray256' : 'ifBlackWhite');
    }
    get imgdpi(): string { return this.FImgDPI.toString(); }
    set imgdpi(value: string) {
        this.FImgDPI = value ? parseInt(value) : 300;
        this.Def_ScanDpi = this.FImgDPI;
    }
    get filesizelimit(): string { return this.FFileSizeLimit.toString(); }
    set filesizelimit(value: string) { this.FFileSizeLimit = value ? parseInt(value) : 5120; }
    get casenolength(): string { return this.FCaseNoLength.toString(); }
    set casenolength(value: string) { this.FCaseNoLength = value ? parseInt(value) : 0; }
    get WH_CATEGORY(): string { return this.FWH_category; }
    set WH_CATEGORY(value: string) { this.FWH_category = value; }
    // ============================================================================
    // 核心資料庫 / 字串解析方法 (移植自 Pascal)
    // ============================================================================
    /**
     * 依欄位及索引取值
     */
    private GetSQLData(TableList: TStringList, ColName: string, colNo: number): string {
        if (TableList.Count === 0 || colNo >= TableList.Count) return '';
        const ColStr = TableList.Strings[0].split(',');
        const TmpStr = TableList.Strings[colNo];
        const DataStr = TmpStr.split('!@!'); // Delphi 用 '!@!' 分隔
        const idx = ColStr.indexOf(ColName);
        if (idx !== -1 && idx < DataStr.length) {
            return DataStr[idx];
        }
        return '';
    }
    /**
     * 找指定的資料
     */
    private FindSQLData(TableList: TStringList, ColumeStr: string, KeyColumeStr: string, KeyStr: string, ColNo: number, ResultList: TStringList): boolean {
        ResultList.Clear();
        if (!KeyStr || TableList.Count <= 1) return false;
        const ColList = ColumeStr.split(',');
        const KeyColList = KeyColumeStr.split(',');
        const KeyList = KeyStr.split(',');
        let findIndex = -1;
        let found = false;
        if (ColNo === 0) {
            for (let i = 1; i < TableList.Count; i++) {
                let match = true;
                for (let n = 0; n < KeyColList.length; n++) {
                    const keyCol = KeyColList[n];
                    const keyVal = KeyList[n];
                    if (this.GetSQLData(TableList, keyCol, i) !== keyVal) {
                        match = false;
                        break;
                    }
                }
                if (match) {
                    found = true;
                    findIndex = i;
                    break;
                }
            }
        } else {
            // Specific Row Check
            let match = true;
            for (let n = 0; n < KeyColList.length; n++) {
                const keyCol = KeyColList[n];
                const keyVal = KeyList[n];
                const rowData = TableList.Strings[ColNo];
                if (rowData.indexOf(`!@!${keyVal}!@!`) === -1) {
                    match = false;
                    break;
                }
            }
            if (match) {
                found = true;
                findIndex = ColNo;
            }
        }
        if (found) {
            for (let n = 0; n < ColList.length; n++) {
                const col = ColList[n];
                ResultList.Add(`${col},${this.GetSQLData(TableList, col, findIndex)}`);
            }
        }
        return found;
    }
    private GetFindResult(Col: string): string {
        for (let i = 0; i < this.FindResult.Count; i++) {
            const s = this.FindResult.Strings[i];
            const idx = s.indexOf(',');
            if (idx > -1) {
                const rCol = s.substring(0, idx);
                const rValue = s.substring(idx + 1);
                if (Col === rCol) return rValue;
            }
        }
        return '';
    }
    // ============================================================================
    // 文件、表單、名稱轉換方法
    // ============================================================================
    private FormCode2DocNo(FormCode: string): string {
        let result = '';
        for (let i = 0; i < this.FormID_List.Count; i++) {
            if (this.FormID_List.Strings[i] === FormCode) {
                result = this.DocNo_List.Strings[i];
                break;
            }
        }
        if (FormCode && !result) {
            result = FormCode.substring(0, 8); // 自訂文件
        }
        return result;
    }
    private FormCode2Version(FormCode: string): string {
        return FormCode.substring(10, 15); // Delphi is 1-based (11,5) -> JS is 0-based (10, 15)
    }
    private FormCode2Page(FormCode: string): string {
        return FormCode.substring(8, 10); // Delphi is (9,2)
    }
    private FileName2FormCode(FileName: string): string {
        const fname = path.basename(FileName);
        const v = fname.indexOf('_');
        const v1 = fname.lastIndexOf('.');
        if (v > -1 && v1 > v) {
            return fname.substring(v + 1, v1);
        }
        return ''; // 附件
    }
    private DocNo2DocNoDir(savePath: string, DocNo: string): string {
        if (DocNo) {
            let i = 0;
            let iDocNo = '';
            do {
                i++;
                iDocNo = `${DocNo}(${i})`;
            } while (fs.existsSync(path.join(savePath, iDocNo)));
            return iDocNo;
        }
        return 'Attach';
    }
    private DocNoDir2DocNo(DocNoDir: string): string {
        if (DocNoDir !== 'Attach' && DocNoDir !== 'S_Attach') {
            const v = DocNoDir.indexOf('(');
            if (v > 0) return DocNoDir.substring(0, v);
            return DocNoDir;
        }
        return DocNoDir;
    }
    // ============================================================================
    // 系統初始化與清單載入 (模擬 Delphi 的 Timer1Timer)
    // ============================================================================
    public async InitSystem() {
        this.InitialOk = false;
        this.FMaxUploadSize = '10';
        if (!this.FUrl) {
            console.error('URL cannot be empty, please contact system administrator');
            return;
        }
        if (!this.FUrl.endsWith('/')) this.FUrl += '/';
        // 在 Node.js 中通常依賴環境變數或 config 檔案,這裡模擬建立目錄
        this.ImagePath = path.join(__dirname, 'Scantemp', this.FWork_no, this.FUserUnit, this.FMode);
        this.ImageSavePath = this.ImagePath;
        if (!fs.existsSync(this.ImagePath)) fs.mkdirSync(this.ImagePath, { recursive: true });
        this.DataLoading(true, true);
        // TODO: 模擬 HTTP API 請求 (GetServerDate, GetSetInf1~7 等)
        // await this.GetServerDate();
        // await this.GetSetInf1();
        this.InitialOk = true;
        this.DataLoading(false, false);
    }
    // ============================================================================
    // 按鈕動作邏輯:上傳 (TransBtnClick)
    // ============================================================================
    public async TransBtnClick() {
        if (!this.InitialOk) {
            console.log('資訊尚未下載完成,請稍候或重新進入');
            return;
        }
        this.RejectCase = false;
        if (this.NewTreeNode.Count === 0) {
            console.log('無影像需傳送');
            return;
        }
        let SuccessCount = 0;
        let ReCasecount = 0;
        let CheckErrCount = 0;
        this.DataLoading(true, true);
        // 巡覽所有案件節點
        for (let i = 0; i < this.NewTreeNode.Count; i++) {
            const nodeText = this.NewTreeNode.Item[i].Text;
            const v = nodeText.indexOf('-');
            const CaseID = nodeText.substring(0, v);
            this.CreateIn_WH(CaseID);
            this.Case2upload(CaseID);
            this.TransPath = path.join(this.ImageSavePath, CaseID, 'Upload');
            // 是否可以上傳?
            const CaseTrans = this.CaseAsk(CaseID); // 0: 可以, 1: 重複, -1: 失敗
            if (CaseTrans === -1) {
                console.error(this.HttpErrStr);
                this.DataLoading(false, false);
                return;
            }
            if (CaseTrans === 1) {
                ReCasecount++;
                continue;
            }
            if (CaseTrans === 0) {
                // 進行 OMR 檢核
                if (this.FMode !== 'FSCAN') {
                    if (!this.OMRCheckCase(CaseID)) {
                        CheckErrCount++;
                        continue;
                    }
                }
                // 執行傳送 (ZIP 壓縮 + HTTP/FTP Upload)
                const isTransOk = await this.TransCaseID(this.TransPath, CaseID, true);
                if (!isTransOk) {
                    this.DataLoading(false, false);
                    return;
                }
                SuccessCount++;
            }
        }
        this.DataLoading(false, false);
        console.log(`傳送完成。成功件【${SuccessCount}】件,失敗件【${ReCasecount}】,檢核失敗【${CheckErrCount}】`);
    }
    // ============================================================================
    // OMR 檢核邏輯 (OMRCheckCase)
    // ============================================================================
    private OMRCheckCase(CaseID: string): boolean {
        let CaseOk = true;
        const errIniPath = path.join(this.ImageSavePath, CaseID, 'upload', 'Checkerr.ini');
        if (fs.existsSync(errIniPath)) fs.unlinkSync(errIniPath);
        const MainFormID = this.GetCaseFormID(path.join(this.ImageSavePath, CaseID, 'upload'));
        // 這裡會實作根據 MainFormID 取得 OMR 檢核 XML 設定的邏輯
        // 並比對掃描圖檔的影像黑點數量,決定是否缺件或缺漏必填欄位。
        // TODO: 完整的 OMR Check (XML Parsing and Image Pixel counting)
        return CaseOk;
    }
    // ============================================================================
    // 壓縮與傳送封裝 (TransCaseID)
    // ============================================================================
    private async TransCaseID(TransPath: string, CaseID: string, MainCase: boolean): Promise<boolean> {
        // 1. 產生必要的 Data 檔案 (FormCode_Name.dat, DocNo_Name.dat 等)
        this.CreateFormID_FormName(TransPath, CaseID);
        this.CreateDocNo_DocName(TransPath, CaseID);
        // 2. 壓縮檔案為 Img.zip (模擬)
        const zipPath = path.join(TransPath, 'Img.zip');
        console.log(`[ZIP] Zipping files in ${TransPath} to ${zipPath}`);
        // 3. 檢查檔案大小是否超過限制 FMaxUploadSize
        // const stat = fs.statSync(zipPath);
        // if (stat.size > parseInt(this.FMaxUploadSize) * 1048576) { ... }
        // 4. 根據 TransMode 進行 Http POST 或 FTP 上傳
        if (this.TransMode === TTransMode.tsHttp) {
            console.log(`[HTTP] Uploading file to ${this.FUrl}...`);
            // TODO: Axios or fetch POST request
        } else if (this.TransMode === TTransMode.tsFtp) {
            console.log(`[FTP] Uploading file to FTP server...`);
            // TODO: FTP Client put
        }
        // 5. 刪除本機暫存
        const caseDir = path.join(this.ImageSavePath, CaseID);
        if (fs.existsSync(caseDir)) {
            fs.rmSync(caseDir, { recursive: true, force: true });
        }
        return true;
    }
    // ============================================================================
    // UI 與工具抽象 (Stubbing)
    // ============================================================================
    private DataLoading(loading: boolean, useTimer: boolean) {
        if (loading) {
            console.log(`Loading... ${this.ShowText}`);
        } else {
            console.log('Loading complete.');
        }
    }
    private CreateIn_WH(CaseID: string) {
        // 模擬產生 In_Wh.dat
    }
    private Case2upload(CaseID: string) {
        // 模擬搬移/複製檔案結構準備上傳
    }
    private CaseAsk(CaseID: string): number {
        // 模擬詢問 Server 案件是否允許上傳
        return 0; // 0: OK
    }
    private GetCaseFormID(path: string): string {
        return '11A000000000000'; // Mock FormID
    }
    private CreateFormID_FormName(path: string, CaseID: string) {}
    private CreateDocNo_DocName(path: string, CaseID: string) {}
    private _Msg(msg: string): string {
        return msg; // 模擬多國語系轉換
    }
    private Add_Zoo(num: number, length: number): string {
        let str = num.toString();
        while (str.length < length) str = '0' + str;
        return str;
    }
}
CB_IMGPSScan_TLB.ts
比對新檔案
@@ -0,0 +1,123 @@
// ************************************************************************ //
// WARNING
// -------
// The types declared in this file were converted from a Pascal Type Library.
// ************************************************************************ //
export const CB_IMGPSScanMajorVersion = 1;
export const CB_IMGPSScanMinorVersion = 0;
export const LIBID_CB_IMGPSScan = '{F7D1C429-BE85-4FAD-A058-36A41C2AAA89}';
export const IID_ICB_IMGPSScanX = '{E68A01E9-2798-497F-9FB8-4EAFB7CCE5B9}';
export const DIID_ICB_IMGPSScanXEvents = '{B7FB8B05-8BDA-4DFD-BCCF-2B657E8A108A}';
export const CLASS_CB_IMGPSScanX = '{8B54EA2A-547C-48AD-BC71-C11C1340B37E}';
export enum TxActiveFormBorderStyle {
  afbNone = 0x00000000,
  afbSingle = 0x00000001,
  afbSunken = 0x00000002,
  afbRaised = 0x00000003,
}
export enum TxPrintScale {
  poNone = 0x00000000,
  poProportional = 0x00000001,
  poPrintToFit = 0x00000002,
}
export enum TxMouseButton {
  mbLeft = 0x00000000,
  mbRight = 0x00000001,
  mbMiddle = 0x00000002,
}
export enum TxPopupMode {
  pmNone = 0x00000000,
  pmAuto = 0x00000001,
  pmExplicit = 0x00000002,
}
// OLE_COLOR can be represented as a number
export type OLE_COLOR = number;
// IFontDisp can be represented as any for simplicity in Node.js
export type IFontDisp = any;
export interface ICB_IMGPSScanX {
  Visible: boolean;
  AutoScroll: boolean;
  AutoSize: boolean;
  AxBorderStyle: TxActiveFormBorderStyle;
  Caption: string;
  Color: OLE_COLOR;
  Font: IFontDisp;
  KeyPreview: boolean;
  PixelsPerInch: number;
  PrintScale: TxPrintScale;
  Scaled: boolean;
  readonly Active: boolean;
  DropTarget: boolean;
  HelpFile: string;
  PopupMode: TxPopupMode;
  ScreenSnap: boolean;
  SnapBuffer: number;
  DockSite: boolean;
  DoubleBuffered: boolean;
  readonly AlignDisabled: boolean;
  readonly MouseInClient: boolean;
  readonly VisibleDockClientCount: number;
  ParentDoubleBuffered: boolean;
  UseDockManager: boolean;
  Enabled: boolean;
  readonly ExplicitLeft: number;
  readonly ExplicitTop: number;
  readonly ExplicitWidth: number;
  readonly ExplicitHeight: number;
  AlignWithMargins: boolean;
  ParentCustomHint: boolean;
  url: string;
  caseid: string;
  mode: string;
  work_no: string;
  userid: string;
  username: string;
  userunit: string;
  data: string;
  verify: string;
  rewrite: string;
  modename: string;
  language: string;
  loandoc_value: string;
  loandoc_enable: string;
  useproxy: string;
  c_docnolist: string;
  c_docnamelist: string;
  fixfilelist: string;
  is_in_wh: string;
  oldcaseinfo: string;
  printyn: string;
  is_oldcase: string;
  custdocyn: string;
  scancolor: string;
  imgdpi: string;
  filesizelimit: string;
  casenolength: string;
  imgdelete: string;
  isExternal: string;
  check_main_form: string;
  WH_CATEGORY: string;
}
export interface ICB_IMGPSScanXEvents {
  OnActivate(): void;
  OnClick(): void;
  OnCreate(): void;
  OnDblClick(): void;
  OnDestroy(): void;
  OnDeactivate(): void;
  OnKeyPress(Key: number): void;
  OnMouseEnter(): void;
  OnMouseLeave(): void;
  OnPaint(): void;
  OnClosePage(): void;
}
doc/curtis/AI可能讀不到的_TLB_介面.md
比對新檔案
@@ -0,0 +1,44 @@
# IO 相關方法
## Dir
```Pascal
function DirectoryExists(const Directory: string; FollowLink: Boolean = True): Boolean;
function str2Dir(const path: string[]): void
```
## String ini 相關
```Pascal
TIniFile = class(TCustomIniFile)
  public
    destructor Destroy; override;
    function ReadString(const Section, Ident, Default: string): string; override;
    procedure WriteString(const Section, Ident, Value: String); override;
    procedure ReadSection(const Section: string; Strings: TStrings); override;
    procedure ReadSections(Strings: TStrings); override;
    procedure ReadSectionValues(const Section: string; Strings: TStrings); override;
    procedure EraseSection(const Section: string); override;
    procedure DeleteKey(const Section, Ident: String); override;
    procedure UpdateFile; override;
  end;
```
## String dat 相關
delphi 大量使用 字串.LoadFromFile, 字串.SaveToFile
- LoadFromFile
- SaveToFile
```Pascal
procedure LoadFromFile(const FileName: string); overload; virtual;
procedure LoadFromFile(const FileName: string; Encoding: TEncoding); overload; virtual;
procedure LoadFromStream(Stream: TStream); overload; virtual;
procedure LoadFromStream(Stream: TStream; Encoding: TEncoding); overload; virtual;
procedure Move(CurIndex, NewIndex: Integer); virtual;
procedure SaveToFile(const FileName: string); overload; virtual;
procedure SaveToFile(const FileName: string; Encoding: TEncoding); overload; virtual;
procedure SaveToStream(Stream: TStream); overload; virtual;
procedure SaveToStream(Stream: TStream; Encoding: TEncoding); overload; virtual;
```
doc/curtis/Business_Logic_Analysis.md
比對新檔案
@@ -0,0 +1,64 @@
# 業務邏輯與資料解析模組 (Business Logic & Parser) 深度分析
在 `CB_IMGPSScanImp.pas` 中,「業務邏輯與資料解析模組」扮演了將底層掃描影像與上層銀行/保險業務需求結合的橋樑。它將前端網頁傳入的原始設定資料,轉換為系統可理解的「案件 (Case)」、「文件 (Document)」與「表單 (Form)」結構,並執行極度複雜的業務規則檢核 (OMR 檢核)。
若要將此模組進一步細拆,其內部組成可分為以下四大子分類:
---
## 1. 系統參數與組態儲存庫 (Config & Parameter Repository)
**職責**:負責解析由 Web 端透過字串傳入的各類系統設定表(通常以 `!@!` 等特殊符號分隔),並將其儲存於記憶體中供全局查詢。
**核心實作特性**:
*   **字串表解析**:大量使用 `TStringList` 作為輕量級的記憶體資料庫 (`SetSQLData`, `GetSQLData`, `FindSQLData`)。
*   **系統設定下載與維護** (`GetSetInf1` ~ `GetSetInf7`):
    *   `DOC_INF`:文件設定檔(定義文件類型、是否需入庫等)。
    *   `FORM_INF`:表單設定檔(定義條碼對應、長寬、十字定位點、最大頁數)。
    *   `DM_FORM_INF`:相依與互斥規則檔。
    *   `CHECK_RULE_INF`:檢核規則錯誤訊息定義檔。
    *   `WORK_INF`:系統與硬體掃描的預設參數。
## 2. 領域實體對映與結構管理器 (Entity Mapping & Structure Manager)
**職責**:將抽象的條碼 (Barcode) 對映至具體的業務實體,並在檔案系統上維護「案件 -> 文件分份 -> 頁面」的實體目錄與虛擬樹狀結構 (`.dat` 索引檔)。
**核心實作特性**:
*   **條碼解析對映**:`BarCode2FormID`、`BarCode2CaseID`、`FormCode2DocNo`、`DocNo2DocName`。
*   **虛擬檔案系統維護**:
    *   處理文件是否需要拆分多份 (`DocNoNeedDiv`)。
    *   管理 `Context.dat`、`CaseDocNo.dat`、`CaseDocNo_Copies.dat` 等索引檔,以記錄掃描影像與業務文件的歸屬關係。
*   **封裝資訊產生器**:`CreateDocNo_Info`、`CreateCustDocNo_Info` (將目錄結構轉換回 Web 端所需的特定字串格式)。
## 3. OMR 與業務規則檢核引擎 (OMR & Business Rule Engine)
**職責**:整個系統最龐大且複雜的業務邏輯核心,負責確保掃描進來的影像完全符合進件規定,防止缺件或矛盾。
**核心實作特性** (`OMRCheckCase`):
*   **文件級別檢核 (Document-Level Rules)**:
    *   **主文件檢核**:檢查主要文件是否齊全、頁數是否符合資料庫預期。
    *   **相依/互斥檢核**:依據 `DM_FORM_INF`,判斷若存在 A 文件,是否遺漏了必附的 B 文件,或錯誤附上了互斥的 C 文件。
    *   **生命週期檢核**:判斷表單條碼是否已超過停用日期、或超過允許的最大掃描頁數。
*   **OMR 畫記與欄位級別檢核 (Field-Level OMR Rules)** (透過 `Xmltool` 解析 `.xml` 設定檔,並計算指定 `(X, Y)` 座標的黑白像素點 `GetSiteOMR`):
    *   `settype1` (必填檢核):特定的 OMR 區塊必須有畫記。
    *   `settype3` (有值連動必填):A 欄位有畫記時,相關的 B 欄位也必須畫記。
    *   `settype8` (有值連動互斥):A 欄位有畫記時,相關的 B 欄位不能畫記。
    *   `settype4` (有值相依文件):特定欄位有畫記時,系統必須檢查是否已附上指定的文件。
    *   `settype5` (有值互斥文件):特定欄位有畫記時,系統必須檢查不能附上指定的文件。
    *   `settype6` (強制備註):特定欄位有畫記時,要求使用者必須手動輸入備註說明。
    *   `settype7` (OMR 帶值提取):讀取畫記並轉換為特定的業務數值 (`GetValue.xml`),隨後傳回後端。
## 4. 案件異動與舊案引用處理器 (Case Modification & Legacy Handler)
**職責**:處理「補件 (ESCAN)」、「重掃 (RSCAN)」、「引用舊件」等極度複雜的邊界狀態與跨案件資料流動。
**核心實作特性**:
*   **文件引用管理**:`SetUseCase`、`GetUseCase` 記錄與追蹤不同案件之間文件的借用與引用關係 (記錄於 `UseCase.ini`)。
*   **版本相容與轉換**:`OldCasetoNewCase`、`ErrFormtoCurrentForm` (處理舊系統升級或表單版本更迭時的自動替換邏輯)。
*   **防呆與鎖定**:`DeleteDocNoFileForESCAN` 等邏輯,防止在補件模式下誤刪非當次掃描的既有影像。
---
## 💡 未來重構與微服務化建議
若要將此「業務邏輯模組」進行現代化重構,強烈建議引入以下設計模式:
1.  **Repository Pattern (儲存庫模式)**:將目前散落的 `FindSQLData` 與 `TStringList` 查詢,封裝為 `FormConfigRepository`、`DocConfigRepository` 等強型別介面,隔離底層字串處理細節。
2.  **Rule Engine (規則引擎)**:將 `OMRCheckCase` 內部數百行的 `if/else` 與 XML 巡覽,拆分為獨立的實體類別(例如 `RequiredFieldRule`, `DependencyDocumentRule`, `MutexFieldRule`),並透過統一的 `Validator` 介面依序執行,大幅提升可測試性 (Testability) 與維護性。
3.  **Domain Driven Design (領域驅動設計)**:將依賴實體 `.dat` 檔案的狀態管理,重構成記憶體中的 `TCase`, `TDocument`, `TPage` 領域物件,只有在最終儲存或打包時才寫入實體檔案系統。
doc/curtis/Delphi_套件_EnScan_webAssembly_可行性.md
比對新檔案
@@ -0,0 +1,77 @@
# EnScan 的功能打包成 webAssembly 處理 Scan 是否可行
這是一個非常深入的技術架構問題。簡短的回答是:**「直接打包」不可行,但「功能移轉」與「資料對接」是可行的,只是架構需要完全翻轉。**
我們必須從 WebAssembly (Wasm) 的底層本質,來分析為什麼 `EnScan` 與 `DibHandle` 無法直接放入 Wasm:
---
### 1. 為什麼無法將 EnScan 打包成 WebAssembly 進行「掃描」?
**核心障礙:硬體存取與沙盒 (Sandbox) 限制**
* **Wasm 的隔離性:** WebAssembly 運行在瀏覽器的安全沙盒中。它**完全無法**存取你電腦的硬體(如 USB 接口、掃描機)。它甚至看不到你的 C 槽或 Windows 登錄表。
* **TWAIN 協定限制:** `EnScan` 的核心是封裝 Windows 的 **TWAIN API**。TWAIN 是一個極度依賴作業系統訊息(Window Messages)與本地驅動程式(`.ds` 檔案)的協定。Wasm 無法呼叫 Windows API,也無法載入掃描機的 `.dll` 驅動程式。
* **VCL 與 WinAPI 依賴:** `EnScan` 是一個 VCL 元件,深層綁定了 Windows 的 GDI(圖形設備介面)。Wasm 並沒有 Windows GDI,它只有一塊線性記憶體(Linear Memory)和 Canvas API。
---
### 2. Wasm 能處理 `DibHandle` 或 `TMemoryStream` 嗎?
這涉及到兩者記憶體模型的完全不相容:
#### **關於 DibHandle (Windows HGLOBAL)**
* **不可行:** `DibHandle` 本質上是一個 Windows 作業系統核心管理的**記憶體地址代號**。
* **原因:** Wasm 的記憶體是「封閉的線性陣列」。它無法透過一個 Windows 句柄(Handle)去讀取 Windows 系統層級的記憶體。如果你把 `DibHandle` 傳給 Wasm,Wasm 只會把它看作一個無意義的整數。
#### **關於 TMemoryStream**
* **有條件可行:** 如果你使用如 **TMS WEB Core** 這類支援 Delphi 語法轉 Wasm 的工具,它們有實作虛擬的 `TMemoryStream`。
* **本質差異:** * **Delphi Win32 的 `TMemoryStream`**:操作的是實體 RAM。
    * **Wasm 的 `TMemoryStream`**:操作的是瀏覽器分配給 Wasm 實例的一塊 **`ArrayBuffer`**。
    * **兩者不通:** 你無法直接將 Win32 的記憶體指針直接「丟進」Wasm 的記憶體空間。
---
### 3. 可行的「現代化」架構建議:混合模式 (Hybrid)
如果你想用 Wasm 處理掃描後的影像,正確的架構應該是:
#### **第一步:本地端代理 (Local Agent/Service)**
你仍需要一個小的 Delphi Win32 程式(或 Service)跑在使用者電腦上:
1.  呼叫 `EnScan` 進行掃描。
2.  在 `OnAcquire` 中拿到 `DibHandle`。
3.  **關鍵轉化**:在本地端將 `DibHandle` 轉成標準的影像格式(如 **Base64 String** 或 **Uint8Array 二進位流**)。
#### **第二步:通訊橋樑**
本地程式透過 **WebSocket** 或 **Local HTTP Server (REST)** 將這串二進位資料傳送給瀏覽器。
#### **第三步:Wasm 接收與處理**
1.  瀏覽器收到二進位流。
2.  將資料寫入 Wasm 的記憶體(`ArrayBuffer`)。
3.  **這時 Wasm 就可以發揮威力了**:你可以用 Wasm 來做影像分析、條碼辨識、或是用 C++/Rust 編譯出來的高效能演算法進行影像優化。
---
### 4. 總結對照表
| 功能特性 | 原有的 Delphi OCX | 現代 WebAssembly 方案 |
| :--- | :--- | :--- |
| **硬體控制 (TWAIN)** | 直接呼叫 (OK) | **無法存取** (需透過本地 Service 轉發) |
| **記憶體管理** | 使用 Windows Handle (`DibHandle`) | 使用 Wasm 線性記憶體 (`ArrayBuffer`) |
| **影像處理速度** | 極快 (Native) | 極快 (Wasm 近乎 Native) |
| **跨瀏覽器支援** | 僅限 IE / Edge IE Mode | **全瀏覽器支援** (Chrome, Safari, Edge) |
| **安全風險** | 高 (ActiveX 有權限漏洞) | 極高 (沙盒保護) |
### **我的建議:**
不要嘗試把 `EnScan` 直接「編譯」成 Wasm,那會導致數千個 WinAPI 連結錯誤。
你應該將 **`CB_IMGPSScanImp.pas`** 拆分成兩部分:
1.  **Backend (Win32)**: 只負責掃描與存成檔案/串流。
2.  **Frontend (Wasm/Web)**: 負責顯示 `ImageScrollBox` 的功能、UI 邏輯與上傳伺服器。
**你想了解如何寫一個簡單的 Local Service,讓 Delphi 掃描後把資料傳給網頁嗎?** 這通常是目前銀行系統現代化(去 IE 化)最普遍的作法。
doc/curtis/Delphi_套件相依.md
比對新檔案
@@ -0,0 +1,115 @@
# **相依套件分析報告**
本報告針對 ActiveX 元件核心實作檔 CB_IMGPSScanImp.pas 進行功能解構,並分析其與第三方套件的綁定關係,以作為後續維護或現代化升級之參考。
## **壹、 依功能拆分模組與第三方套件綁定分析**
根據原始碼結構,CB_IMGPSScanImp.pas 是一個將介面、硬體、網路與業務邏輯高度耦合的「上帝物件 (God Object)」。若要進行重構或拆件,建議依據以下五個核心模組進行解耦:
### **1. 硬體掃描控制模組 (Scanner Controller)**
- **功能描述**:負責與實體掃描機溝通,處理 TWAIN 介面初始化、設定掃描參數(DPI、色彩、雙面)、觸發掃描指令,以及接收掃描完成的記憶體影像 (DibHandle)。
- **子模組細分建議**:
  - **TWAIN 介面封裝 (TWAIN Wrapper)**:專門負責底層 TWAIN 驅動程式的呼叫、狀態機管理與錯誤處理。
  - **掃描參數管理器 (Scanner Profile Manager)**:負責載入、儲存與套用各類掃描設定 (DPI、對比、亮度、單雙面等)。
  - **事件與回呼接收器 (Acquisition Event Handler)**:專責處理非同步的 `OnAcquire` 事件,負責將底層 `DibHandle` 安全地轉換為應用程式可用的影像物件,並通知上層模組。
- **關鍵方法**:initkscan、StatrTwainScan、OnAcquire、PageEnd、PageDone。
- **第三方套件綁定 (強綁定)**:
  - 依賴 **Envision Imaging SDK** (EnScan) 進行 TWAIN 驅動控制。
  - 依賴 EnMisc 處理底層記憶體或雜項運算。
### **2. 影像處理與轉換模組 (Image Processor)**
- **功能描述**:負責對擷取到的原始影像進行後製處理,包括:裁切(如 A3 裁 A4)、去斜 (Deskew)、旋轉、二值化 (轉黑白)、格式轉換 (轉 TIFF/JPEG),以及讀取影像上的條碼。
- **子模組細分建議**:
  - **影像幾何變換引擎 (Image Transformation Engine)**:專責處理去斜 (Deskew)、旋轉、縮放與裁切等座標/空間運算。
  - **色彩與格式處理器 (Color & Format Converter)**:負責二值化、灰階/彩色轉換,以及 TIFF/JPEG 等不同檔案格式的壓縮與編解碼。
  - **條碼與標記識別器 (Barcode & Marker Recognizer)**:獨立負責分析影像中的條碼內容,或進行 OMR (光學標記辨識) 的初步像素比對。
  - **OMR 與十字定位點分析器 (OMR & Anchor Analyzer)**:專門為「光學標記辨識 (OMR)」服務,負責找出影像上的基準定位點,並計算特定區域內的黑白像素分佈。
- **關鍵方法**:ImageReSize_FormID、CheckNeedCrop、DeskewImg、ConvertToBW。
- **第三方套件綁定 (強綁定)**:
  - 依賴 **Envision Imaging SDK** (EnTifGr, EnDiGrph) 進行 TIFF (CCITT Group 4) 壓縮與 DIB 物件封裝。
  - 依賴 **MPS Barcode** (mpsBarco, BarcodesFinder) 進行影像條碼掃描與解析。
### **3. 安全傳輸與 API 通訊模組 (Transport Manager)**
- **功能描述**:負責將掃描並壓縮後的檔案上傳至伺服器,或從伺服器下載檔案。同時處理與後端 Java Servlet 的參數交換(如校時、取得 FTP 資訊、更新案件狀態)。
- **子模組細分建議**:
  - **HTTP/HTTPS API 請求管理器 (RESTful / Servlet Client Wrapper)**: 專責透過 HTTP(S) 協定,向後端的 Java Servlet 端點發送 GET/POST 請求。
  - **雙協定檔案上傳/下載引擎 (Dual-Protocol File Transfer Engine)**: 處理實體影像檔案的實體傳輸,並根據系統設定動態切換 HTTP 或 FTP 協定。
  - **安全加密與雜湊模組 (Security, Crypto & Hashing Utils)**: 負責資料傳輸前後的加解密運算,以及檔案完整性的 MD5 校驗,確保通訊過程不被竄改。
  - **傳輸前置準備與封裝器 (Payload Archiver & Preparer)**: 在真正啟動傳輸前,將硬碟上散落的 TIFF/JPG 檔案、`.dat` 索引檔打包成單一的 ZIP 封裝檔。
- **關鍵方法**:upFile、dnFile_Get、ProcessServlet_Get、GetServerDate、GetftpInfo。
- **第三方套件綁定 (強綁定)**:
  - 依賴 **SecureBlackbox** (SBSimpleSSL, SBHTTPSClient, SBSimpleFTPS 等) 處理 HTTPS/FTPS 安全加密通訊。
  - 依賴 **Indy** (IdHashMessageDigest) 產生檔案的 MD5 檢核碼。
  - 依賴自訂/第三方加密庫 (Encryp) 處理字串加解密。
### **4. 業務邏輯與資料解析模組 (Business Logic & Parser)**
- **功能描述**:專案核心的領域邏輯,包含接收網頁端傳入的參數字串進行解析、定義文件歸類邏輯(由條碼推導文件類型)、OMR (光學標記辨識) 檢核定義等。
- **子模組細分建議**:
  - **系統參數與組態儲存庫 (Config & Parameter Repository)**:負責解析由 Web 端透過字串傳入的各類系統設定表(如 DOC_INF、FORM_INF 等),並將其儲存於記憶體中供全局查詢。
  - **領域實體對映與結構管理器 (Entity Mapping & Structure Manager)**:將抽象的條碼對映至具體的業務實體,並在檔案系統上維護「案件 -> 文件分份 -> 頁面」的虛擬樹狀結構。
  - **OMR 與業務規則檢核引擎 (OMR & Business Rule Engine)**:處理最核心的檢核邏輯,包含主文件/相依互斥文件檢核、欄位級別的 OMR 畫記檢核,以及基於 XML 設定的各種業務規則。
  - **案件異動與舊案引用處理器 (Case Modification & Legacy Handler)**:專責處理「補件」、「重掃」、「引用舊件」等複雜的邊界狀態與跨案件資料流動。
- **關鍵方法**:SetSQLData、GetSQLData、FormCode2DocNo、OMRCheckCase。
- **第三方套件綁定 (中度綁定)**:
  - 依賴 **Xmltool** 解析 XML 格式的 OMR 座標定義。
  - 依賴 **VCLZip** (VCLZip, VCLUnZip) 將多個影像檔打包成 ZIP 進行單一傳輸。
### **5. UI 與介面呈現模組 (UI View)**
- **功能描述**:負責 ActiveX 控制項的視覺化介面,包含左側的樹狀文件清單 (TreeView)、右側的影像預覽區 (ScrollBox),以及按鈕與多國語言切換。
- **子模組細分建議**:
  - **案件樹狀檢視元件 (TreeView UI)**: 獨立管理左側的案件與文件節點,負責節點的建立、刪除與圖示切換。
  - **多影像滾動視窗 (ScrollBox View)**: 管理右側大圖及縮圖的並排顯示,支援拖拉、選取以及快捷鍵操作。
  - **多國語系引擎 (I18N Manager)**: 統一從設定檔中載入字串,替換畫面上所有的 Label、Button 與 Message 提示。
  - **核心版面與容器管理 (Layout & Container Management)**: 定義整個 ActiveX 控制項的視覺區塊,並透過階層式的 Panel 進行排版管理。
  - **系統狀態與進度回饋 (Status & Progress Feedback)**: 在進行耗時的任務時,鎖定 UI 並給予使用者明確的等待提示。
- **關鍵方法**:InitialLanguage、各類 OnClick 事件。
- **第三方套件綁定 (輕度綁定)**:
  - 依賴 **Envision Imaging** (EnImgScr) 作為支援大影像拖曳的滾動視窗。
  - 依賴 PJMenuSpeedButtons 作為自訂外觀的按鈕元件。
## **貳、 第三方套件功能總覽**
為達成上述複雜的業務需求,本專案引入了多個 Delphi 著名的第三方套件。以下為各套件的大致功能說明:
### **1. Envision Imaging SDK (前綴 En)**
這是一套專為 Delphi 設計的高效能影像與 TWAIN 處理套件,在金融與保險業的文件電子化系統中非常常見。
- **EnScan**:封裝了 TWAIN 協定,讓程式能直接控制實體掃描機,控制進紙、解析度與雙面掃描。
- **EnDiGrph (TDibGraphic)**:封裝 Windows 底層的 DIB (Device Independent Bitmap) 記憶體句柄,讓開發者能像操作一般圖片一樣操作掃描出來的原始資料。
- **EnTifGr**:專門處理 TIFF 影像格式。銀行業愛用 TIFF 是因為它支援「多頁」且黑白壓縮率極高 (Group 4 Fax Compression)。
- **EnImgScr**:提供 TImageScrollBox 視覺元件,用來平滑顯示並縮放超大尺寸的掃描圖檔。
- **EnMisc**:提供影像處理所需的底層輔助數學或資料轉換函式。
### **2. SecureBlackbox (前綴 SB)**
由 EldoS (後被 /n software 收購) 開發的強大安全通訊與加密套件。由於早期 Delphi (D7~D2007) 內建網路庫缺乏完善的 SSL/TLS 支援,此套件被廣泛用於補強安全性。
- **SBHTTPSClient / SBSimpleSSL**:實作了完整的 HTTPS 用戶端通訊協定,用來安全地上傳檔案或呼叫 Servlet API。
- **SBSimpleFTPS**:支援 FTP over SSL (FTPS) 的安全檔案傳輸。
- **SBWinCertStorage / SBX509**:處理 Windows 系統內的憑證儲存區,以及 X.509 數位憑證的驗證(例如確認銀行伺服器的 HTTPS 憑證是否合法)。
### **3. MPS Barcode & BarcodesFinder**
- **功能**:這是一套條碼識別引擎。
- **專案應用**:在掃描過程中(如 PageDone 或 OnAcquire 階段),它會掃描圖片特定區域尋找一維或二維條碼。讀取到的條碼值會被用來決定這張影像屬於哪一種「文件表單」(FormCode),進而實現自動分類歸檔。
### **4. VCLZip (VCLZip, VCLUnZip)**
- **功能**:老牌的 Delphi ZIP 壓縮元件。
- **專案應用**:為了節省網路頻寬與減少伺服器連線次數,掃描完成的多張 TIFF/JPG 檔案會被打包成一個 .zip 檔後,再一次性上傳至伺服器。
### **5. Indy Components (前綴 Id)**
- **IdHashMessageDigest / idHash**:Delphi 內建的 Internet Direct (Indy) 函式庫。
- **專案應用**:在此專案中被用來計算檔案或字串的 **MD5 Hash (雜湊值)**。通常用於確保檔案上傳過程沒有損壞,伺服器比對 MD5 一致才算上傳成功。
### **6. 其他工具 (Tools & Utils)**
- **Xmltool**:負責讀寫 XML 檔案。銀行經常將表單的 OMR (光學標記辨識) 檢核座標或參數寫成 XML 設定檔,透過此工具讀入系統。
- **Encryp**:這可能是開發商自訂或第三方的輕量級加密單元,用來加密本機設定檔 (.ini) 內的敏感資訊或混淆網頁傳入的參數。
doc/curtis/Image_Processer_Analysis.md
比對新檔案
@@ -0,0 +1,69 @@
# 影像處理與轉換模組 (Image Processor) 深度分析
在 `CB_IMGPSScanImp.pas` 中,「影像處理與轉換模組」負責在硬體掃描取得原始影像後,以及上傳/顯示之前,對影像進行一系列的強化、分析與格式轉換。這個模組是決定影像品質、檔案大小與後續 OMR 辨識準確率的關鍵。
若要將此模組進一步細拆,其內部組成可分為以下四大子分類:
---
## 1. 影像幾何與物理變換引擎 (Image Geometric Transformation Engine)
**職責**:專責處理影像的空間座標轉換、旋轉、去斜以及裁切,確保最終儲存的影像端正且符合業務規定的尺寸。
**核心實作特性**:
*   **影像去斜與轉正**:
    *   `DeskewImg`:透過演算法自動偵測影像傾斜角度並將其校正。
    *   `Rotate`:依據條碼辨識出的角度 (`MpsBarcodeinf.r180`) 或使用者手動點擊按鈕 (`SpeedButton14Click` 等),進行 90、180、270 度的旋轉。
*   **影像裁切與分割**:
    *   `CheckNeedCrop`:根據影像寬高比與偵測到的條碼數量,判斷是否為需要分割的 A3 雙頁合併影像。
    *   `CropImg`:給定 `TRect` 座標,將 A3 影像精準裁切為兩張 A4 (`iGraphic_First`, `iGraphic_sec`)。
*   **影像縮放 (Resize)**:
    *   `ImageReSize_FormID` / `ImageReSize_tmp`:依據表單定義檔 (`FORM_INF`) 中的長寬設定與尋找到的十字定位點,強制將影像縮放變形至標準尺寸,以利後續的 OMR 座標對位。
    *   `DpiResize`:調整影像的 DPI 解析度參數。
## 2. 色彩處理與編碼轉換器 (Color & Format Converter)
**職責**:處理影像的色彩深度 (位元深度) 轉換,以及針對不同色彩模式套用最適合的檔案壓縮演算法,以達到最佳的畫質與檔案大小平衡。
**核心實作特性**:
*   **色彩空間轉換**:
    *   `ConvertToBW`:將彩色或灰階影像強制二值化 (Binarization) 轉為黑白影像,這不僅能大幅縮小檔案,也是條碼與 OMR 辨識的必要前置步驟 (系統通常會保留一個隱藏的 `ISB_BW` 物件專做辨識用)。
    *   `ConvertToGray`:將彩色轉換為灰階 (`ifGray256`)。
    *   `Image_Smooth` / `NegativeImg` / `CleanupBorder`:影像平滑化、反相處理與清除黑邊。
*   **格式與壓縮決策**:
    *   針對黑白影像 (`ifBlackWhite`):使用 `tcGroup4` (CCITT Group 4 Fax Compression) 或 `tcPackBits` 壓縮,並儲存為 `.tif`。
    *   針對彩色/灰階影像 (`ifTrueColor`, `ifGray256`, `ifColor256`):轉換為 `TJpegGraphic`,套用 `tcJpeg` 壓縮與指定的壓縮率 (`FJpgCompression` / `SaveQuality`),並儲存為 `.jpg`。
## 3. 條碼識別與解析器 (Barcode Recognizer)
**職責**:獨立負責掃描與解析影像上的一維或二維條碼,這是系統實現「文件自動歸類」的核心依賴。
**核心實作特性**:
*   **條碼引擎呼叫**:
    *   `MpsGetBarcode`:呼叫底層的 MPS Barcode 引擎,傳入二值化的影像 (`ISB_BW.Graphic`)。
    *   回傳 `TMpsBarcodeinf` 結構,包含條碼內容字串陣列 (`Text`) 與每個條碼的方向資訊 (`r180`)。
*   **應用與邏輯綁定**:
    *   透過條碼字串找出對應的 `FormID` 與 `DocNo`。
    *   檢查條碼長度是否符合規範 (`FormIDLength`),過濾雜訊或誤判的條碼。
## 4. OMR 與十字定位點分析器 (OMR & Anchor Analyzer)
**職責**:專門為「光學標記辨識 (OMR)」服務,負責找出影像上的基準定位點,並計算特定區域內的黑白像素分佈。
**核心實作特性**:
*   **基準點尋找 (Anchor Finding)**:
    *   `FindPoint`:根據 XML 定義的模式 (`ANCHOR` 或 `FRAME`),在影像四個角落尋找定位基準點 (`UpLPoint`, `UpRPoint`, `DownLPoint`, `DownRPoint`)。
    *   `CheckSize`:比對找到的定位點距離與標準長寬是否有巨大落差,若找不到則記錄至 `AnchorError.dat`。
*   **像素計算與標記判定 (Pixel Calculation)**:
    *   `GetSiteOMR`:根據 XML 傳入的相對座標字串 (`Site`),將其換算為實際的 `TRect`。
    *   `Get_OMR`:計算該區域內黑色像素的數量。
    *   結合雜訊過濾 (`ClearLine`) 與容差值 (`SafePixel`, `bt`),最終判定該 OMR 欄位是否「有畫記」。
---
## 💡 未來重構與微服務化建議
若要對此影像處理模組進行重構,建議方向如下:
1.  **抽離為獨立的 Pipeline 模式 (管線模式)**:
    目前這些功能散落在 `OnAcquire` 與各個事件中。應將其重構為一條清晰的影像處理管線:`Image -> Deskew -> Crop -> ConvertBW -> BarcodeRead -> Resize -> Compress -> Save`。每個步驟 (Step) 應該是獨立的類別,方便抽換或開關。
2.  **解耦 UI 與影像處理**:
    目前大量依賴畫面上隱藏的 `ISB_BW` (TImageScrollBox) 來進行二值化和條碼辨識。這違反了 MVC 原則且耗費額外的 GDI 資源。應改用純記憶體物件 (如獨立的 `TDibGraphic` 或 `TBitmap`) 在背景執行緒中進行這些運算,不要綁定可見的 UI 元件。
3.  **引入更現代的影像引擎 (如 OpenCV)**:
    早期依賴的 Envision SDK 在尋找十字定位點 (FindPoint) 和去斜 (DeskewImg) 的演算法可能較為老舊。若未來轉型為微服務架構,可將這部分邏輯移植為 Python/C++ 並使用 OpenCV 來達成更精準的高速運算。
doc/curtis/Local_IO_抽像方法列表.md
doc/curtis/Local_IO_路徑與檔案操作分析.md
比對新檔案
@@ -0,0 +1,233 @@
# **IO 路徑與檔案操作分析**
本報告詳列了 CB_IMGPSScanImp.pas 檔案中定義的所有 IO 相關變數,以及執行目錄與檔案管理的關鍵方法,並定義了抽像化所需的介面原型。
## **壹、 關鍵路徑與檔案變數 (Variables)**
程式中定義了多個全域或私有欄位,用來追蹤檔案在硬碟上的實體位置。
### **1. 核心路徑變數與動態組合邏輯**
| **變數名稱** | **類型** | **組合邏輯 (Combination Logic)** | **用途說明** |
| --- | --- | --- | --- |
| **ImagePath** | String | WORK_INF 的 LOCAL_PATH 參數值 | **本機基礎根路徑**。所有暫存資料的起點。 |
| **ScaniniPath** | String | ImagePath + FWork_No + '\' + FUserUnit + '\' | **設定檔存放區**。區分作業別與單位。 |
| **ImageSavePath** | String | 同 ImagePath (初期) 或隨 FMode 變動 | **影像儲存基準路徑**。 |
| **ScanPath** | String | ImageSavePath + ScanCaseno + '\' + ScanDocdir + '\' | **當前掃描實體目錄**。指向最末層的影像資料夾。 |
| **TransPath** | String | ImageSavePath + CaseID + '\Upload\' | **準備上傳區**。在傳送前將檔案結構扁平化組合於此。 |
| **DisplayPath** | String | ImageSavePath + NowCaseNo + '\' | **預覽基準目錄**。用於 UI 點選樹狀圖時讀取特定案件資料。 |
| **LngPath** | String | GetLocalAppDir(Handle) + 'MPS\CB_IMGPS\' | **應用程式資源路徑**。存放在使用者設定檔目錄以避開權限問題。 |
| **CheckXmlPath** | String | ImagePath + 'OMRSITE\' | **OMR 規則暫存區**。存放從伺服器同步下來的 XML 定義。 |
| **SamplePath** | String | ImagePath + 'Sample\' + FWork_No + '\' | **範本影像存放區**。按作業別區分。 |
| **SitePath** | String | ImagePath + 'Site\' | **登打定位存放區**。 |
| **FFtpRootPath** | String | 透過伺服器回傳值動態設定 | **FTP 伺服器根路徑**。指定在遠端 FTP 伺育器上存放影像檔的起始位址。 |
### **2. 檔案名稱變數 (Filenames)**
| **變數名稱** | **類型** | **用途說明** |
| --- | --- | --- |
| **ScanSaveFilename** | String | 下一張掃描影像預定的檔名(通常包含 FormID)。 |
| **PEFileName** | String | 在 PageEnd 階段確定的最終完整檔案路徑。 |
| **AttName** | String | **附件目錄名稱**。根據 FIs_In_Wh 決定為 Attach 或 S_Attach。 |
| **Ext** | String | 副檔名。預設為 .tif,彩色/灰階模式下可能切換為 .jpg。 |
## **貳、 關鍵 IO 操作方法 (Methods)**
以下為需要進行抽像化設計的核心 IO 方法。
### **1. 目錄管理 (Directory Management)**
| **方法名稱** | **作用** | **應用場景** | **建議抽象 IO 介面** |
| --- | --- | --- | --- |
| _DelTree | 遞迴刪除整個目錄樹 | 上傳成功後清空暫存、模式切換初始化目錄 | RemoveDirRecursive(Path) |
| Str2Dir / ForceDirectories | 確保路徑以 \ 結尾並建立路徑 | 初始化或掃描前建立資料夾 | EnsureDirExists(Path) |
| GetNoNameCase | 搜尋尚未配號的臨時目錄 | 在本機搜尋如 未配號0001 到 9999 的可用路徑 | FindNextAvailablePath(Pattern) |
| GetLocalAppDir | 取得 Windows %LocalAppData% 路徑 | 確保在現代 Windows 系統中有權限寫入語言檔與 Log | GetBaseStoragePath() |
### **2. 存在性檢查與搜尋 (Existence & Search)**
| **方法名稱** | **作用** | **應用場景** | **建議抽象 IO 介面** |
| --- | --- | --- | --- |
| FileExists | 檢查指定路徑檔案是否存在 | 在載入 .dat、.ini 或影像前進行安全檢查 | IsFilePresent(Path) |
| DirectoryExists | 檢查指定路徑資料夾是否存在 | 在建立掃描目錄或上傳前確認結構完整性 | IsDirPresent(Path) |
| FindFirst / FindNext | 搜尋符合特定格式的檔案 | 取得實體檔案大小 (FileRec.Size) 或遞迴搜尋檔案 | ListFiles(Path, Pattern) / GetFileInfo(Path) |
### **3. 檔案生命週期與刪除 (File Lifecycle & Deletion)**
| **方法名稱** | **作用** | **應用場景** | **建議抽象 IO 介面** |
| --- | --- | --- | --- |
| ReSortFileName | 影像重新編序命名 | 刪除或插入影像後,修正 001_xxx 等序號連續性 | RenameFile(Old, New) |
| DeleteImageFile | 刪除實體影像檔 | 單張影像刪除並觸發 .dat 內容同步更新 | DeleteFile(Path) |
| DeleteDocNoFile | 刪除特定文件代號下所有影像 | 整批文件(DocNo)的物理刪除與序號重整 | DeleteFilesByPattern(Pattern) |
| DeleteShowFile | 刪除 UI 顯示中的影像清單 | 根據介面選取狀態進行批次物理刪除 | DeleteFileList(List<Path>) |
| RenameFile / MoveFile | 檔案搬移或更名 | 用於「分案」、「移動頁數」或「變更歸類」 | MoveFile(Src, Dest) |
| CopyFile | 複製實體檔案 | 影像搬移至上傳區或引用舊案件影像 | CopyFile(Src, Dest) |
### **4. 資料紀錄與序列化 (Metadata IO)**
| **方法名稱** | **作用** | **應用場景** | **建議抽象 IO 介面** |
| --- | --- | --- | --- |
| DeleteCustomDocDir | 移除自訂文件定義 | 從 CustomDocNo.ini 中移除特定自訂文件的節點 | Storage.IniDeleteSection(Section) |
### **5. 網路傳輸 IO 方法 (Network Retrieval - dnFile_Get)**
專案中使用 `dnFile_Get` 從伺服器端同步必要的環境檔案或規則定義。
| **抓取對象 (目標路徑)** | **遠端請求 Action** | **用途說明** |  |
| --- | --- | --- | --- |
| **CheckXmlPath + filename** | `GetCheckXml` | **下載 OMR 檢核定義檔**。同步伺服器端最新的 XML 座標規則,用於自動判斷影像區域。 | https |
| **SamplePath + filename** | `GetSampleImg` | **下載範本影像**。用於在 UI 介面上提供給操作員對照的標準範例圖檔。 | https |
| **SitePath + filename** | `GetSiteImg` | **下載定位定義圖**。下載與登打、校對位置相關的輔助影像。 | https |
| **LngPath + 'Language.ini'** | `GetLanguage` | **同步多國語言檔**。確保用戶端介面文字(繁體、英文、越南語等)與伺服器同步。 | https |
## **參、 資料紀錄與序列化 (Metadata IO)**
專案中使用了大量的 .dat 與 .ini 檔案來記錄目錄內的結構資訊與使用者偏好。
### **1. .dat 檔案 (結構紀錄)**
| **檔案名稱** | **組合路徑邏輯 (Path Logic)** | **內容與用途** |
| --- | --- | --- |
| **Context.dat** | ImageSavePath + CaseID + '\' + DocDir + '\Context.dat' | **影像清單**。紀錄該資料夾內所有影像檔名的正確順序。 |
| **DocDir.dat** | ImageSavePath + CaseID + '\Upload\DocDir.dat' | **目錄對應表**。紀錄 Upload 下每個檔名所屬的原始 DocDir |
| **CaseDocNo.dat** | ImageSavePath + CaseID + '\CaseDocNo.dat' | **文件目錄索引**。紀錄案件內已建立的實體資料夾名稱。 |
| **CaseDocNo_Copies.dat** | ImageSavePath + CaseID + '\CaseDocNo_Copies.dat' | **份數紀錄**。對應 CaseDocNo.dat 中的資料夾分別有多少份。 |
| **CaseIndex.dat** | ImageSavePath + CaseID + '\CaseIndex.dat' | **案件屬性**。如是否為「授信卷」(Case_loandoc)。 |
| **EditedDocDir.dat** | ImageSavePath + CaseID + '\EditedDocDir.dat' | **異動清單**。紀錄當次操作中被修改過的文件目錄。 |
| **Scan_Memo.dat** | DisplayPath + 'Scan_Memo.dat' | **使用者註記**。紀錄操作員手動輸入的備註內容。 |
| **CaseList.dat** | ImageSavePath + 'CaseList.dat' | **案件總表**。紀錄目前本機快取中存在的所有 CaseID。 |
### **2. .ini 檔案 (設定與偏好)**
| **檔案名稱** | **路徑邏輯** | **回寫 (Write-back)** | **內容與用途** | 寫入關聯 |
| --- | --- | --- | --- | --- |
| **Scan.ini** | ScaniniPath + 'Scan.ini' | **是 (頻繁)** | **掃描器硬體設定**。儲存最後一次選取的 DPI、雙面、色彩模式、亮度、對比等參數。 |  |
| **CustomDocNo.ini** | ScaniniPath + 'CustomDocNo.ini' | **是 (中等)** | **自訂文件代號**。儲存操作員定義的文件分類名稱與內部 ID 映射。 | DeleteCustomDocDir |
| **CB_IMGPSScan.ini** | LngPath + 'CB_IMGPSScan.ini' | **是 (低頻)** | **環境配置**。儲存伺服器 IP、Port、自動更新版本資訊等全域參數。 |  |
| **Language.ini** | LngPath + 'Language.ini' | **否 (唯讀同步)** | **多國語言包**。僅透過 `dnFile_Get` 從伺服器抓取更新,OCX 不自行修改。 |  |
## **肆、 串流與讀寫操作 (Stream & File I/O)**
源碼中頻繁使用 .LoadFromFile 與 .SaveToFile 來實現資料的持久化。
### **1. 使用 TStringList 讀寫 .dat 結構**
| **操作對象** | **讀取場景 (Load)** | **寫入場景 (Save)** | **建議抽象 IO 介面** |
| --- | --- | --- | --- |
| **結構清單 (.dat)** | LoadImgFile 載入以重建樹狀結構 | PageEnd 掃描完成或結構增刪後即時存檔 | ReadTextFile(Path) / WriteTextFile(Path, Content) |
| **備註文件 (Memo)** | 進入備註編輯器時載入舊有內容 | WNoteBtnClick 完成編輯後儲存內容 | ReadTextFile(Path) / WriteTextFile(Path, Content) |
| **上傳包定義** | N/A | CreateFormID_FormName 產生上傳包所需的定義檔 | WriteTextFile(Path, Content) |
|  |  |  |  |
|  |  |  |  |
### **2. 使用 TImageScrollBox / TDibGraphic 讀寫影像**
| **操作對象** | **讀取場景 (Load)** | **寫入場景 (Save)** | **建議抽象 IO 介面** |
| --- | --- | --- | --- |
| **掃描影像原始檔** | view_image_... 載入顯示於預覽窗 | OnAcquire 將記憶體影像 (Dib) 持久化至磁碟 | ReadBinaryFile(Path) / WriteBinaryFile(Path, Blob) |
| **影像處理覆蓋** | ImageReSize_FormID 讀取進行定位縮放 | 處理完成後覆蓋原始影像檔 | ReadBinaryFile(Path) / WriteBinaryFile(Path, Blob) |
| **旋轉/後製處理** | 點擊旋轉按鈕後從磁碟重新讀取 | 旋轉完成後保存更新內容回實體檔案 | ReadBinaryFile(Path) / WriteBinaryFile(Path, Blob) |
## **伍、 檔案路徑構造邏輯示例**
程式中典型的路徑構造方式如下(以 PageEnd 為例):
// 1. 基礎路徑
ScanPath := ImageSavePath + ScanCaseno + '\';
// 2. 進入特定文件目錄 (DocDir)
ScanPath := ScanPath + ScanDocdir + '\';
// 3. 確保實體目錄存在
Str2Dir(ScanPath);
// 4. 構造最終影像路徑
PEFileName := ScanPath + Add_Zoo(PageCount, 3) + '_' + FormID + ext;
## **陸、 IO 方法具體實作邏輯細部說明 (Detailed Implementation Logic)**
本節針對 CB_IMGPSScanImp.pas 內部如何透過 Windows API 實作這些 IO 操作進行細部解說。
### **1. Str2Dir(Path: string) 的運作機制**
這個方法的主要作用是 **「路徑標準化與實體資料夾建立」**。
- **字串處理**:它會先檢查傳入的 Path 字串最後一個字元是否為 \。如果不是,會自動補上。
- **建立資料夾**:**是的,它會在內部建立資料夾**。它會呼叫 Delphi 的 ForceDirectories(Path) 函式。
- **ForceDirectories 的特性**:與一般的 CreateDir 不同,ForceDirectories 會「遞迴」建立路徑中所有不存在的父目錄(例如若路徑為 C:\A\B\C,即使 A 和 B 都不存在,它也會一併建立)。
### **2. _DelTree(DirName: string) 的遞迴邏輯**
由於 Delphi 早期版本沒有提供單一函式來刪除包含檔案的資料夾,此方法透過 Windows 的搜尋機制實現:
- **遍歷搜尋**:使用 FindFirst 和 FindNext 找出資料夾內的所有物件。
- **檔案處理**:若搜尋到的是一般檔案,直接呼叫 DeleteFile。
- **遞迴處理**:若搜尋到的是子資料夾,則「自己呼叫自己」 (_DelTree) 先清空該子資料夾。
- **物理移除**:當資料夾內的所有內容都清空後,呼叫 RemoveDir 將空的資料夾從磁碟移除。
### **3. FindFirst 與 FindNext 的搜尋與疊代邏輯**
這兩個方法是 Delphi 處理「集合式 IO 操作」(如掃描目錄、批次刪除)的基礎,通常搭配 TSearchRec 結構使用。
- **FindFirst(Path, Attr, SearchRec)**:
- **動作**:啟動搜尋任務,將找到的第一個檔案資訊填入 SearchRec。
- **應用例**:在 PM104Click (匯入影像) 時,程式會先執行 FindFirst(FName, faAnyfile, FileRec) 來確認使用者選取的實體檔案是否存在,並藉由 FileRec.Size 取得位元組大小,用以判斷是否超過系統設定的匯入限制(例如 5MB)。
- **FindNext(SearchRec)**:
- **動作**:繼續尋找下一個符合條件的檔案。
- **疊代模式**:通常包裹在 repeat ... until FindNext(SR) <> 0 迴圈中。
- **應用例**:在 _DelTree 實作中,程式利用此疊代邏輯將資料夾內「所有的」子目錄與檔案清空,直到 FindNext 回傳非零值(代表搜尋結束)。
- **FindClose(SearchRec)**:
- **重要性**:釋放搜尋控制代碼(Handle)。若未呼叫,頻繁搜尋會導致系統資源洩漏。
### **4. GetNoNameCase(Path: string) 的搜尋邏輯**
這是一個典型的 **「可用名稱查表」** 實作:
- **迴圈範圍**:從 1 迭代到 9999。
- **格式組合**:使用 Add_Zoo 函式將數字轉為四位數(如 0001),並加上前綴字「未配號」。
- **磁碟檢查**:呼叫 DirectoryExists 檢查 Path + '未配號000x'。
- **結果回傳**:第一個回傳 False(代表該資料夾尚不存在)的名稱即為結果,程式會立即跳出迴圈並選定此名稱。
### **5. ReSortFileName(Path: string) 的連續性維護**
為了保證 UI 上顯示的頁碼順序與磁碟檔名一致,此方法採用「先暫存、後改名」的策略:
- **防止衝突**:先將目錄內所有符合格式的影像更名為帶有臨時前綴(如 @)的檔名。
- **按序更名**:讀取 Context.dat 內的清單,依照目前的順序,將檔案逐一更名為 001_..., 002_... 等格式。
- **同步索引**:最後將更名後的結果重新寫入 Context.dat。
### **6. GetLocalAppDir(Handle: HWND) 的權限繞過**
這是一個系統級別的 IO 位置定位方法:
- **API 呼叫**:呼叫 SHGetSpecialFolderPath (Windows Shell API)。
- **定位 CSIDL_LOCAL_APPDATA**:指向 C:\Users\使用者名稱\AppData\Local\。
- **必要性**:這能解決 OCX 程式被安裝在 Program Files 下時,因為 UAC 權限無法在安裝目錄寫入 Log 或語言檔的問題。
### **🎯 移植至 Web 平台的建議**
當您將這些邏輯移植到 Web 時:
- Str2Dir 應對應到 **IndexedDB** 的 Object Store 建立或 **File System Access API** 的 getDirectoryHandle(搭配 create: true)。
- FindFirst / FindNext 在 Web 端應對應到 Directory Handle 的 entries() 異步疊代器。
- .dat 序列化文件在 Web 端建議直接轉為 **JSON** 格式存儲於瀏覽器的 localStorage 或 IndexedDB 中。
## **柒、 設定檔回寫邏輯 (Write-back Logic)**
本節說明專案中哪些 .ini 檔案會由 OCX 主動更新資料及其觸發時機。
| **檔案名稱** | **回寫內容 (Content)** | **觸發時機 (Trigger)** |
| --- | --- | --- |
| **Scan.ini** | DPI、Duplex、亮度、對比、影像模式 | 掃描前設定變更或啟動掃描時(SaveScanPara)。 |
| **CustomDocNo.ini** | 自訂文件名稱與 ID 映射 | 操作員新增自訂分類或呼叫 `DeleteCustomDocDir` 時。 |
| **CB_IMGPSScan.ini** | 版本號、伺服器 IP/Port | 自動更新完成後更新版號,或手動修改伺服器設定時。 |
|  |  |  |
## **捌、 注意事項**
- **硬編碼路徑**:ActiveFormCreate 中存在寫死的 C:\Temp\IMGPSScan,適合作為最終保險。
- **權限問題**:作為 ActiveX,這些 IO 操作強烈依賴瀏覽器的「安全站台設定」或 IE 的「保護模式」。使用 GetLocalAppDir 是現代化重構中解決 UAC 權限問題的重要手段。
doc/curtis/OCX_溝通與_抽象層抽換.md
比對新檔案
@@ -0,0 +1,249 @@
# **TCB_IMGPSScanX OCX 通訊介面與溝通機制分析**
本表條列了 Delphi 宿主環境(ActiveX[ocx]) 與 TCB_IMGPSScanX 元件之間主要的溝通管道、對應方法及其在系統架構中的作用。
## **壹、 指令與資料交換介面表**
| **通訊方法 (Method)** | **通訊方向** | **溝通內容 (Data Payload)** | **具體作用與目的** |
| --- | --- | --- | --- |
| **SetSQLData** | **宿主 -> OCX** | 以 ` | 或,` 分隔的長字串(如:IP, CaseID, DPI) |
| **GetSQLData** | **宿主 <-> OCX** | 指定 Key (如 'PARA_NO') 取得對應 Value | **資料查詢通道**。宿主可詢問 OCX 目前快取清單中的特定數值,用於介面同步或邏輯判斷。 |
| **GetSetInf1 ~ 7** | **宿主 <- OCX** | 單一字串值 (如 CaseID, 總張數, 成功標記 'Y') | **狀態出口 (Outlets)**。提供標準化的 7 個出口,讓 JavaScript 或宿主 App 輪詢(Polling)目前的執行結果或掃描統計。 |
| **StatrTwainScan** | **宿主 -> OCX** | 無參數 (觸發動作) | **硬體啟動指令**。宿主程式按下「開始掃描」按鈕後呼叫此方法,通知 OCX 喚起 TWAIN 驅動程式。 |
| **GetCurrentVersionNo** | **宿主 <- OCX** | 版本字串 (如 '1.0.3.5') | **版本識別**。宿主程式載入 OCX 後第一步通常會呼叫此處,用以比對伺服器版本並決定是否強制更新。 |
| **upFile** | **宿主 -> OCX** | 無或部分參數 (觸發網路 IO) | **輸出指令**。通知 OCX 將本機暫存區的檔案(如 ZIP 包)依照先前設定好的路徑與權限上傳至伺服器。 |
| **dnFile_Get** | **宿主 -> OCX** | 遠端 Action 關鍵字與本地檔名 | **同步指令**。通知 OCX 向伺服器抓取特定資源(如 XML 規則或語言檔)並存放到指定 IO 路徑。 |
| **ActiveFormCreate** | **系統 -> OCX** | 視窗建立事件 | **環境初始化**。當宿主程式實體化 OCX 時,自動觸發此處執行 GetDefScanIni,確保掃描環境就緒。 |
| **Property CaseID** | **宿主 <-> OCX** | WideString | **屬性直連**。透過 COM 屬性機制直接設定或讀取案件編號。 |
| **Property ScreenSnap** | **宿主 -> OCX** | Boolean | **功能切換**。控制 OCX 是否要在特定時機執行螢幕截圖稽核。 |
## **貳、 溝通機制細部說明**
### **1. 參數化配置 (Config-by-String)**
本專案大量使用 SetSQLData 這種「一條字串走天下」的設計,這在早期 ActiveX 開發中非常流行,優點是規避了 COM 介面頻繁修改 _TLB.pas 的麻煩。您在轉換 TypeScript UI 時,可以將其封裝為一個 JSON 物件,最後再組合成字串傳給這個方法。
### **2. 狀態回饋 (The Inf1~7 Pattern)**
由於 ActiveX 事件 (Events) 的回傳有時在網頁端不夠穩定,開發者選擇了 GetSetInf 系列。宿主程式通常會在執行完某個長耗時動作(如 upFile)後,連續呼叫 GetSetInf3 (成功標記) 與 GetSetInf6 (錯誤訊息) 來決定下一步 UI 流程。
### **3. 雙向同步 (Bidirectional Sync)**
- **同步設定**:宿主透過 GetDefScanIni 觸發,OCX 內部完成硬體設定。
- **同步資料**:宿主透過 dnFile_Get 觸發,OCX 完成本機與伺服器的檔案 IO 同步。
# **參、描服務抽象層實作**
採用 **「抽象介面層 (Abstraction Layer)」** 配合 **「適配器模式 (Adapter Pattern)」**,處理遺留系統(Legacy System)過渡到現代化架構
```tsx
/**
 * 掃描服務資料模型定義
 * 將原本 OCX 雜亂的字串溝通 (SetSQLData) 結構化
 */
export interface ScanConfig {
  serverIp: string;
  caseId: string;
  dpi: number;
  isColor: boolean;
  isDuplex: boolean;
  // ... 其他來自 GetDefScanIni 的參數
}
export interface ScanStatus {
  caseId: string;
  totalPageCount: number;
  isUploadSuccess: boolean;
  lastBarcode: string;
  errorMessage: string;
}
/**
 * 掃描服務抽象介面 (IScanService)
 * UI 層僅與此介面溝通,不直接操作底層實作
 */
export interface IScanService {
  initialize(config: ScanConfig): Promise<void>;
  startScan(): Promise<boolean>;
  uploadFiles(): Promise<boolean>;
  getStatus(): Promise<ScanStatus>;
  getVersion(): Promise<string>;
  downloadResource(action: string, fileName: string): Promise<void>;
}
/**
 * 實作 A: ActiveX 適配器 (Legacy Adapter)
 * 用於目前環境:UI (TS) -> Delphi Browser -> ActiveX (OCX)
 */
export class ActiveXScanAdapter implements IScanService {
  private ocx: any;
  constructor() {
    // 透過 window.external 或 Delphi Bridge 取得 OCX 實例
    this.ocx = (window as any).OCX_INSTANCE;
  }
  async initialize(config: ScanConfig): Promise<void> {
    // 將 JSON 轉回 OCX 慣用的管道分隔字串 (SetSQLData 邏輯)
    const sqlDataStr = `${config.serverIp}|${config.caseId}|${config.dpi}|${config.isColor ? 'C' : 'G'}`;
    this.ocx.SetSQLData(sqlDataStr);
  }
  async startScan(): Promise<boolean> {
    this.ocx.StatrTwainScan();
    return true; // 實際狀態需透過 GetSetInf 輪詢
  }
  async getStatus(): Promise<ScanStatus> {
    return {
      caseId: this.ocx.GetSetInf1(),
      totalPageCount: parseInt(this.ocx.GetSetInf2()),
      isUploadSuccess: this.ocx.GetSetInf3() === 'Y',
      lastBarcode: this.ocx.GetSetInf5(),
      errorMessage: this.ocx.GetSetInf6()
    };
  }
  async uploadFiles(): Promise<boolean> {
    this.ocx.upFile();
    return true;
  }
  async getVersion(): Promise<string> {
    return this.ocx.GetCurrentVersionNo();
  }
  async downloadResource(action: string, fileName: string): Promise<void> {
    this.ocx.dnFile_Get(action, fileName);
  }
}
/**
 * 實作 B: Backend Server 適配器 (Future Adapter)
 * 用於未來環境:UI (TS) -> REST API / WebSocket -> Rust/Go Backend
 */
export class BackendServerAdapter implements IScanService {
  private apiUrl = "http://localhost:8080/api/scanner";
  async initialize(config: ScanConfig): Promise<void> {
    await fetch(`${this.apiUrl}/config`, {
      method: 'POST',
      body: JSON.stringify(config)
    });
  }
  async startScan(): Promise<boolean> {
    const res = await fetch(`${this.apiUrl}/scan`, { method: 'POST' });
    return res.ok;
  }
  async getStatus(): Promise<ScanStatus> {
    const res = await fetch(`${this.apiUrl}/status`);
    return await res.json();
  }
  async uploadFiles(): Promise<boolean> {
    const res = await fetch(`${this.apiUrl}/upload`, { method: 'POST' });
    return res.ok;
  }
  async getVersion(): Promise<string> {
    const res = await fetch(`${this.apiUrl}/version`);
    const data = await res.json();
    return data.version;
  }
  async downloadResource(action: string, fileName: string): Promise<void> {
    // 未來可能直接由後端處理,前端僅發送同步指令
    await fetch(`${this.apiUrl}/sync?action=${action}&file=${fileName}`);
  }
}
/**
 * 服務工廠 (Scanner Factory)
 * 根據環境自動切換實作
 */
export class ScannerFactory {
  static getService(): IScanService {
    if ((window as any).OCX_INSTANCE) {
      console.log("偵測到 ActiveX 環境,啟用 ActiveX 適配器");
      return new ActiveXScanAdapter();
    } else {
      console.log("現代瀏覽器環境,啟用後端 API 適配器");
      return new BackendServerAdapter();
    }
  }
}
```
### **1. 介面標準化(JSON 化)**
原先 OCX 使用 SetSQLData 的「長字串」溝通是非常脆弱的(一項參數順序錯了就全毀)。在 TypeScript 抽象層中,請務必將其轉換為 **強型別的 JSON 物件**(如 ScanConfig 介面)。這樣你的 UI 邏輯在處理設定時會非常直覺。
### **2. 非同步處理(Promise/Async)**
ActiveX 的方法通常是同步阻塞的,而後端 API 是非同步的。
- **建議**:在抽象介面(IScanService)中,所有方法都回傳 Promise。
- **原因**:這樣當你未來切換到後端伺服器(fetch API)時,UI 的調用邏輯(await service.startScan())不需要做任何修改。
### **3. 實作「心跳」或「狀態輪詢」**
原本 GetSetInf1~7 主要是靠宿主程式主動去「問」OCX。
- **抽象層建議**:在 IScanService 中實作一個 subscribeStatus(callback) 的觀察者模式。
- **ActiveX 模式下**:在適配器內部啟動一個 setInterval 去輪詢 GetSetInf。
- **未來後端模式下**:適配器改用 **WebSocket** 接收後端主動推播的掃描事件。
- **UI 層感受**:UI 只需要處理 onStatusUpdate 事件,根本不用管底層是用輪詢還是 WebSocket。
### **4. 資源路徑的轉義(The Bridge Proxy)**
這是我之前提到的關鍵:由於 iframe 裡的 TypeScript 無法存取 C:\Temp,但 ActiveX 依舊會把檔案掃到那裡。
- **策略**:你的 Delphi Browser 需要實作一個簡單的本地 Proxy。
- **實作**:當 TypeScript 需要顯示圖檔時,它請求 http://local.bridge/view/001.tif,Delphi Browser 攔截此請求並從實體路徑讀取檔案回傳。
- **好處**:未來改為 Backend Server 時,Server 也可以提供同樣的 URL 格式,UI 完全無縫接軌。
## **肆、 Delphi 與 JavaScript 通訊橋樑 (Bridge) 實作說明**
當您在 Delphi 宿主程式中使用內嵌瀏覽器時,JavaScript 取得 `OCX_INSTANCE` 的方式取決於底層的瀏覽器引擎:
### **1. 舊版 IE 核心 (TWebBrowser / window.external)**
這是最傳統的方式,Delphi 會透過實作 `IDocHostUIHandler` 介面,將一個自訂的 `IDispatch` 物件掛載到 `window.external` 上。
- **Delphi 端**:將 `TCB_IMGPSScanX` 實例或其包裝類別指派給瀏覽器的 `External` 屬性。
- **JS 端**:直接使用 `window.external.StatrTwainScan()` 呼叫。
### **2. 新版 Edge 核心 (TEdgeBrowser / WebView2 Host Objects)**
這是現代化的做法。Delphi 透過 WebView2 的 API 將物件直接「注入」到 JS 的命名空間中。
- **Delphi 端執行**:
    ```pascal
    // 將 OCX 物件以 'OCX_INSTANCE' 名稱注入
    EdgeBrowser.AddHostObjectToScript('OCX_INSTANCE', MyOcxWrapperObject);
    ```
- **JS 端取得**: WebView2 會將物件放置在 `window.chrome.webview.hostObjects` 底下。
    ```jsx
    const ocx = window.chrome.webview.hostObjects.OCX_INSTANCE;
    ```
### **3. 為什麼需要 Wrapper (包裝類別)?**
通常我們**不建議**直接將 `TCB_IMGPSScanX` 注入,原因如下:
1. **安全性**:直接暴露 COM 元件可能會有安全漏洞。
2. **相容性**:OCX 的某些方法回傳型別(如 Delphi 的自訂 Enum)JavaScript 無法識別。
3. **執行緒優化**:Delphi 可以在 Wrapper 中使用 `TThread.Queue` 確保掃描指令在正確的執行緒執行,避免瀏覽器 UI 凍結。
### **4. 溝通路徑總結**
1. **Delphi 宿主程式** 啟動並實體化 **OCX 控制項**。
2. **Delphi 宿主程式** 建立 **Browser 物件** 並載入 **TypeScript UI (HTML)**。
3. **Delphi 宿主程式** 呼叫瀏覽器 API(如 `AddHostObjectToScript`),將 OCX 的操作介面「注入」到該網頁的 `window` 物件中。
4. **TypeScript** 透過抽象層 (Adapter) 取得該物件,實現「網頁點按鈕,掃描機開動」的效果。
doc/curtis/ScanImp_源碼分析.md
比對新檔案
@@ -0,0 +1,242 @@
# **核心源碼與物件實作解析**
本文件針對 CB_IMGPSScanImp.pas 中的核心方法與隱藏物件進行原始碼級別的邏輯拆解與推導。
## **壹、 核心方法源碼說明**
### **1) TCB_IMGPSScanX.GetCurrentVersionNo**
- **功能描述**:取得目前 OCX 元件的版本號。這通常提供給網頁端的 JavaScript 呼叫,用來判斷客戶端的 OCX 是否需要觸發更新下載。
- **實作推導與底層 API 說明**:
  在 Delphi 中,若要動態讀取編譯進 OCX 的 VS_VERSION_INFO 資源,標準作法是呼叫一系列的 Windows API。這段實作通常包含以下三個關鍵概念:
1. **HInstance (實體句柄)**:
- 這是 Delphi 提供的一個全域變數,代表目前載入記憶體中的模組(即這個 OCX 檔)的「實體句柄 (Instance Handle)」。
- 在 Windows 作業系統中,當一個執行檔 (EXE) 或動態連結檔 (DLL/OCX) 被載入記憶體時,會獲得一個記憶體基底位址,HInstance 就是指向這個位址的指標。Windows 需要這個值來識別「你要操作的是哪一個載入的模組」。
1. **GetModuleFileName 的作用**:
- 這是一個 Windows API。它的作用是利用前面提到的 HInstance,向作業系統反查出這個模組在硬碟上的**「完整實體檔案路徑」**(例如:C:\Windows\Downloaded Program Files\CB_IMGPSScan.ocx)。
- 因為我們需要讀取檔案本身的版本資訊,程式必須先知道「我自己到底存放在硬碟的哪裡」。
1. **後半段 VerQueryValue 的運作邏輯**:
   取得檔案路徑後,讀取版本資訊會經過三部曲,而 VerQueryValue 是最後負責「解析」的關鍵:
- **Step 1 (GetFileVersionInfoSize)**:先問 Windows 這個檔案的版本資訊區塊有多大。
- **Step 2 (GetFileVersionInfo)**:把整塊版本資訊的二進位原始資料 (Raw Data) 完整讀進一段記憶體緩衝區中。
- **Step 3 (VerQueryValue)**:因為剛剛讀進來的是一包無法直接閱讀的二進位資料,VerQueryValue 的作用就是**「在這包資料中進行精準查詢」**。透過傳入特定的查詢路徑(例如傳入 \ 可以取得包含主版號、次版號的 VS_FIXEDFILEINFO 結構體;或是傳入特定語系代碼如 \StringFileInfo\040404E4\FileVersion),作業系統會幫你把二進位資料解析成我們需要的字串或數字版本號(如 1.0.3.5)。
### **2) TCB_IMGPSScanX.ActiveFormCreate**
- **功能描述**:這是 ActiveX 視覺化窗體 (Form) 的生命週期起點。當 IE 瀏覽器或應用程式載入這個 OCX 時,首先觸發的初始化事件。
- **實作推導**:
  它會負責初始化全域變數、設定第三方元件的預設值、並建立暫存資料夾。
    ```pascal
     procedure TCB_IMGPSScanX.ActiveFormCreate(Sender: TObject);
      begin
        // 1. 預設變數初始化
        Def_DeviceDelete := True;
        Def_ScannerReverse := False;
        // 2. 建立本機暫存目錄
        ForceDirectories('C:\Temp\IMGPSScan');
        // 3. UI 語系初始化
        InitialLanguage;
      end;
    ```
### **3) TCB_IMGPSScanX.StatrTwainScan**
- **功能描述**:啟動掃描機的 TWAIN 介面,開始進紙或掃描。(註:Statr 應為 Start 的早期拼字錯誤,這在舊專案中很常見)。
- **實作推導**:
  這個方法會讀取網頁傳進來的參數(如 DPI、色彩),設定給 Scanner 物件,然後發出擷取指令。
    ```pascal
      procedure TCB_IMGPSScanX.StatrTwainScan;
      begin
        // 設定解析度與顏色
        Scanner.Resolution := StrToIntDef(Fimgdpi, 200);
        if Fscancolor = 'C' then
          Scanner.PixelType := ptColor
        else
          Scanner.PixelType := ptGray;
        // 啟動掃描 (會喚起實體掃描機)
        Scanner.Acquire;
      end;
    ```
### **4) TCB_IMGPSScanX.OnAcquire 與資料流物件**
- **功能描述**:這是 Scanner (EnScan) 最核心的事件。當掃描機成功掃完「一張」影像並傳入電腦記憶體時,會觸發此事件。
- **源碼邏輯推導**:
    ```pascal
    procedure TCB_IMGPSScanX.ScannerAcquire(Sender: TObject; DibHandle: THandle; var Handled: Boolean);
      var
        MyStream: TMemoryStream;
        MyGraphic: TDibGraphic;
      begin
        // 1. 將 DibHandle 轉換為 Delphi 影像物件
        MyGraphic := TDibGraphic.Create;
        MyGraphic.Handle := DibHandle;
        // 2. 影像處理 (如去斜、黑白轉換)
        // 3. 存入記憶體串流或實體檔案
        // ...
      end;
    ```
- **三大核心資料物件解釋**:
1. **DibHandle**:這是 Windows 底層的「全域記憶體句柄 (Global Memory Handle)」。掃描機驅動把圖片放在記憶體裡,給你這個代號。你必須透過 TDibGraphic 來「解讀」這塊記憶體。
2. **TMemoryStream**:**記憶體串流**。它的作用像是一塊存在 RAM 裡的虛擬硬碟。掃描後的圖檔在還沒存到實體硬碟前,會先放在這裡進行格式轉換 (如轉成 JPEG) 或進行 MD5 計算,因為在記憶體中操作速度最快。
    - **DeleteStm : TMemoryStream 的逐行操作說明**:
      在 `OnAcquire` 事件中,`DeleteStm` 主要用於「空白頁判定」。其源碼邏輯如下:
    ```pascal
    // 判定是否啟動空白頁刪除功能
    if Def_DeviceDelete then
    begin
      // 1. 在 Heap 記憶體中建立一個暫存串流物件
      DeleteStm := TMemoryStream.Create;
      try
        // 2. 將當前掃描到的影像物件 (MyGraphic) 寫入記憶體串流
        // 這會根據 TIFF G4 等格式進行壓縮,產生實際的檔案位元組資料
        MyGraphic.SaveToStream(DeleteStm);
        // 3. 核心判定:檢查該影像在記憶體中所佔用的位元組大小 (Size)
        // 若小於預設門檻值 (Def_DeviceDeleteSize, 如 3072 Bytes)
        if DeleteStm.Size < Def_DeviceDeleteSize then
        begin
          // 4. 若符合空白頁特徵,則不進行存檔動作,直接跳出此 Procedure
          // 並告知掃描控制項此頁已處理完畢 (Handled := True)
          Handled := True;
          Exit;
        end;
      finally
        // 5. 無論是否為空白頁,操作結束後必須釋放記憶體,避免 Memory Leak
        DeleteStm.Free;
      end;
    end;
    ```
   -
3. **TFileStream**:**檔案串流**。用來直接對硬碟讀寫資料。當確認影像無誤後,會透過 TFileStream 將 TMemoryStream 的內容真實地寫入 C:\Temp\...\001.tif 中。
### **5) TCB_IMGPSScanX.FindISB2View**
- **功能描述**:用來在畫面上尋找並更新用來預覽圖片的元件。
- **名詞解釋**:**ISB 代表 ImageScrollBox**。
  這對應到 uses 區段中的 EnImgScr 套件。TImageScrollBox 是 Envision 提供的一個 UI 元件,專門用來顯示超出螢幕大小的掃描圖檔,支援平滑拖曳與縮放。這個方法通常用來把剛掃描完的 DibHandle 餵給畫面上的 ISB 元件顯示。
### **6) TCB_IMGPSScanX.GetNoNameCase**
- **功能描述**:處理「無案號」或「未知歸屬」的掃描件。
- **源碼實作逐行說明**:
  此方法透過迴圈尋找一個尚未被使用的目錄名稱(格式為「未配號 + 四位數字」)。
```pascal
Function TCB_IMGPSScanX.GetNoNameCase(Path:String):String; // 取未配號XXXX
var
  i : Integer; // 宣告迴圈計數器
begin
  // 1. 啟動一個從 1 到 9999 的迴圈,尋找可用的索引值
  for i := 1 to 9999 do
  begin
    // 2. 檢查特定路徑下的目錄是否已存在
    // _Msg('未配號'):取得「未配號」字串(可能具備多國語言處理)
    // Add_Zoo(i, 4):將數字 i 補齊為 4 位數(例如 1 變為 0001)
    if Not DirectoryExists(Path + _Msg('未配號') + Add_Zoo(i, 4)) then
    begin
      // 3. 若該目錄不存在,代表此名稱可用,設定 Result 並跳出迴圈
      Result := _Msg('未配號') + Add_Zoo(i, 4);
      Break; // 找到第一個可用的名稱後立即停止搜尋
    end;
  end;
end;
```
### **7) TCB_IMGPSScanX.SetSQLData 與 8) GetSQLData**
這兩個方法是 OCX 與網頁 (JS) 溝通的雙向橋樑。
- **SetSQLData (網頁 -> OCX)**:接收由網頁傳來的設定字串(通常用 | 或 , 分隔,例如 192.168.1.1|L12345678|200|C),在內部將字串切開 (Split),並分別賦值給 ServerIP, FCaseID, Fimgdpi, Fscancolor 等內部變數。
- **GetSQLData (OCX -> 網頁)**:將 OCX 內部的處理結果(例如:UploadSuccess|3|0 代表上傳成功、共 3 頁、0 錯誤),組合成一個長字串後回傳給網頁端的 JS 進行後續的頁面跳轉或提示。
### **8) TCB_IMGPSScanX.GetDefScanIni**
- **功能描述**:取得並套用掃瞄參數的預設值與自訂設定。
- **源碼實作解析**:
    - **硬編碼預設值 (Hardcoded Defaults)**:
      程式一開始會先寫死一組標準值,例如 Def_ScanDpi := 300 (預設 300 DPI)、Def_DeviceDeleteSize := 3072 (空白頁判定門檻為 3KB)。
    - **動態配置覆蓋 (Dynamic Overriding)**:
      透過 for i := 0 to WORK_INF_List.Count - 1 do 迴圈遍歷設定檔清單。使用 GetSQLData 取得參數代號 (PARA_NO) 與內容 (PARA_CONTENT)。
- **屬性設定對照表**:
  下表整理了此方法中設定的所有關鍵屬性、其對應的參數代號及功能說明。
  | **屬性名稱 (內部變數)** | **對應 PARA_NO** | **PARA_CONTENT 處理方式(GetSQLData)** | **功能說明** |
      | --- | --- | --- | --- |
  | **Def_DeviceDelete** | SCAN_BLANKDEL_USE | 'Y' -> True, Else -> False | 空白頁自動刪除開關。 |
  | **Def_DeviceDeleteSize** | SCAN_BLANKDEL_SIZE | StrToInt (若空則 0) | 空白頁判定門檻 (Bytes)。 |
  | **Def_ScannerReverse** | SCAN_REVERSE | 'Y' -> True, Else -> False | 影像是否反相。 |
  | **Def_BoardClear** | SCAN_BOARDCLEAR | 'Y' -> True, Else -> False | 是否清除影像黑邊。 |
  | **Def_ScanDpi** | SCAN_DPI | StrToInt (若空則 300) | 掃描解析度。 |
  | **Def_ScanDuplex** | SCAN_DUPLEX | 'Y' -> True, Else -> False | 是否啟用雙面掃描。 |
  | **Def_ScanRotate** | SCAN_ROTATE_MODE | '0'->0, '1'->270, '2'->180, '3'->90 | 掃描旋轉角度映射。 |
  | **Def_ScanDeskew** | SCAN_DESKEW | 'Y' -> True, Else -> False | 是否自動矯正傾斜。 |
  | **Def_ScanImgSetUse** | SCAN_IMGSET_USE | 'Y' -> True, Else -> False | 是否使用亮度/對比設定。 |
  | **Def_ScanBright** | SCAN_BRIGHT | StrToInt (限制 -255~255) | 亮度數值。 |
  | **Def_ScanContrast** | SCAN_CONTRAST | StrToInt (限制 -255~255) | 對比數值。 |
  | **Def_ScanImgShowMode** | SCAN_SHOW_MODE | '0','1','2' 對應模式 | 影像顯示/縮放模式。 |
  | **ScanDenialTime** | CASE_IN_TIME | 直接存為 String | 進件截止時間限制。 |
  | **ScanDenialHint** | SCAN_HINT | 直接存為 String | 掃描畫面提示字串。 |
  | **NoSaveBarCodeList** | NO_SAVE_FORM_ID | CommaText := Value | 不存檔之表單代號清單。 |
  | **ImagePath** | LOCAL_PATH | 直接存為 String | 本機端暫存路徑。 |
  | **GuideFormIDList** | GUIDEFORMID | CommaText := Value | 導引頁表單 ID 清單。 |
  | **DivPageFormIDList** | DIVPAGEFORMID | CommaText := Value | 分案頁表單 ID 清單。 |
  | **FJpgCompression** | FILE_COMPRESSION | StrToInt | JPG 轉 TIF 壓縮比。 |
  | **FMaxUploadSize** | MAX_UPLOAD_SIZE | 直接存為 String | 上傳大小限制 (MB)。 |
## **貳、 記憶體直接操作實作清單**
以下條列專案中直接涉及記憶體操作的程式區段:
### **記憶體操作對應位置表**
| **記憶體操作對象** | **所屬 Function / Procedure** | **關鍵操作關鍵字 / 代碼** | **記憶體特性說明** | 註記 |
| --- | --- | --- | --- | --- |
| **HInstance** | GetCurrentVersionNo | GetModuleFileName(HInstance, ...) | 模組載入位址。 | 增API版本號 |
| **資源緩衝區** | GetCurrentVersionNo | VerQueryValue, GetFileVersionInfo | 透過 Pointer 進行二進位搜尋。 | 增API版本號 |
| **DibHandle** | onAcquire | MyGraphic.Handle := DibHandle; | Windows 全域記憶體句柄轉移。 | 改套件實作/Rust |
| **TMemoryStream** | onAcquire | MemoryStream.Write, .Read | Heap Memory 內部二進位操作。 |  |
## **參、 遺失物件反推與說明**
你提到在原始碼中找不到 _DelTree 和 Scanner 的實作,這是因為它們屬於以下兩種情況:
### **1) TCB_IMGPSScanX._DelTree**
- **這是一個自訂的私有輔助函式 (Private Helper)**。
- **功能**:它的名稱源自 DOS 時代的指令 deltree(刪除整個目錄樹)。在 ActiveX 中,當上傳伺服器成功後,必須清空 C:\Temp\案件編號\ 裡的所有暫存圖檔與資料夾以釋放空間,並防止資料外洩。
- **實作原理**:由於早期的 Delphi 沒有內建遞迴刪除目錄的單一指令,開發者通常會自己寫一個 _DelTree,裡面使用 FindFirst, FindNext 找出所有檔案並用 DeleteFile 刪除,最後用 RemoveDir 砍掉資料夾。
### **2) Scanner**
- **這是一個視覺化/非視覺化的第三方元件實例 (Component Instance)**。
- **為何找不到宣告?** 因為在 Delphi 中,如果你是透過 IDE 的工具箱 (Tool Palette) 將 TEnScan 元件直接拖拉到窗體 (Form) 上,IDE 會自動把它寫在 .dfm 檔裡,並在 TCB_IMGPSScanX 類別的 published 區段自動產生一行 Scanner: TEnScan;。
- **功能**:它就是 **Envision SDK** 裡用來與 TWAIN 硬體溝通的靈魂核心。所有的掃描屬性 (Scanner.Resolution) 與方法 (Scanner.Acquire) 都是對這個物件進行操作。
doc/curtis/Scanner_Controller_Analysis.md
比對新檔案
@@ -0,0 +1,62 @@
# 硬體掃描控制模組 (Scanner Controller) 深度分析
在 `CB_IMGPSScanImp.pas` 中,「硬體掃描控制模組」是系統與實體掃描設備溝通的唯一橋樑。它深度依賴 TWAIN 協定(透過第三方元件 `EnScan` 封裝),負責控制硬體行為並將紙本文件轉換為記憶體中的數位影像資料。
若要將此模組進一步細拆,其內部組成可分為以下三大子分類:
---
## 1. TWAIN 介面封裝與控制 (TWAIN Interface Wrapper)
**職責**:負責管理實體掃描機的連線狀態、開啟/關閉資料來源 (Data Source),以及觸發掃描動作。
**核心實作特性**:
*   **掃描機選擇與初始化**:
    *   `SelectScanner` (`scanner.SelectScanner`):呼叫 TWAIN 介面讓使用者選擇預設的掃描機硬體。
    *   `initkscan` / `CheckScannerConfig`:在系統啟動時檢查掃描機是否已正確配置並支援特定功能(例如檢查 `Scanner.DuplexCap > 0` 確認是否支援雙面掃描)。
*   **掃描生命週期管理** (`StatrTwainScan`):
    *   負責設定 `Scanner.OpenSource`(開啟驅動)。
    *   呼叫 `Scanner.AcquireWithSourceOpen` 開始非同步擷取,並傳入回呼函式 (Callback) 的指標與自訂的狀態結構 (`LongInt(@ScanInfo)`)。
    *   確保無論掃描成功、失敗或中斷,最終都會安全地執行 `Scanner.CloseSource`。
## 2. 掃描參數與設定管理器 (Scanner Profile Manager)
**職責**:負責載入、維護及套用掃描時所需的各項硬體與前處理參數。這些參數通常從資料庫 (`WORK_INF`) 或是本機的 `FBScan.ini` 中讀取。
**核心實作特性**:
*   **硬體基本參數 (Hardware Params)**:
    *   `ScanDpi`:解析度設定,對應 `RequestedXDpi` 與 `RequestedYDpi`。
    *   `ScanColor` (`TImageFormat`):影像色彩模式(黑白 `ifBlackWhite`、灰階 `ifGray256`、彩色 `ifTrueColor`)。
    *   `ScanDuplex`:控制硬體是否開啟雙面進紙 (`Scanner.Duplex`)。
    *   `TwainShowUI`:是否顯示掃描機原廠的 TWAIN 設定畫面 (`Scanner.ShowUI`)。
*   **硬體增強參數 (Enhancement Params)**:
    *   `ScanBright` (亮度) 與 `ScanContrast` (對比度):當啟用 `ScanImgSetUse` 時,將這些參數直接送入掃描硬體。
*   **前置處理規則 (Pre-processing Rules)**:
    *   `DeviceDelete` 與 `DeviceDeleteSize`:定義是否開啟「空白頁自動刪除」以及刪除的位元組閥值。
    *   `ScannerReverse` (反相)、`ScanDeskew` (硬體/初階去斜)、`BoardClear` (清除黑邊)。
## 3. 非同步影像接收與初始轉換器 (Acquisition Event Handler)
**職責**:做為掃描機與應用程式記憶體的交界點,負責攔截驅動程式傳回的原始記憶體區塊 (`DibHandle`),並進行最基礎的安全轉換與前置判斷。
**核心實作特性** (`OnAcquire` 回呼事件):
*   **記憶體接管 (Memory Handover)**:
    *   接收來自 TWAIN 驅動的 `DibHandle`(Device Independent Bitmap 記憶體指標)。
    *   利用 `pScanInfo^.Graphic.AssignFromDibHandle(DibHandle)` 將非管理的記憶體轉換為 Delphi VCL 的 `TTiffGraphic` 物件。
*   **早階條碼快篩與方向校正 (Early Barcode Detection & Rotation)**:
    *   為確保後續處理正確,系統會在剛拿到記憶體影像時,立刻進行一次快速的條碼掃描 (`MpsGetBarcode`)。
    *   讀取條碼所帶的方向資訊 (`MpsBarcodeinf.r180`),並在記憶體中立刻進行 `Rotate` 旋轉,將顛倒或橫放的文件轉正。
*   **基礎裁切與壓縮決策 (Format & Crop Decision)**:
    *   **A3 裁 A4**:呼叫 `CheckNeedCrop` 判斷長寬比例與特定條碼數量,若確認為 A3 合併頁,則立刻在記憶體中將其拆分為 `iGraphic_First` 與 `iGraphic_Sec` 兩張獨立影像。
    *   **格式與壓縮決策**:針對色彩屬性 (`ifBlackWhite`, `ifTrueColor`, `ifGray256`),決定應該套用哪種壓縮演算法(如黑白套用 `tcGroup4` 或 `tcPackBits`,彩色則轉換為 `tcJpeg` 並套用指定的 `FJpgCompression` 壓縮率)。
---
## 💡 未來重構與微服務化建議
若要將此「硬體掃描控制模組」進行現代化升級或跨平台轉型,建議考量以下方向:
1.  **與 UI 徹底解耦**:
    目前的 `OnAcquire` 回呼函式中混雜了大量 UI 操作(如 `ImageScrollBox1.Graphic.Assign(...)`)。應改用「發佈-訂閱模式 (Pub-Sub)」,讓 Scanner Controller 收到影像後只觸發事件,由外部的 UI 層去訂閱並更新畫面。
2.  **Web/雲端化架構 (Web TWAIN / WebSockets)**:
    若未來系統要走向純 Web 化(捨棄 ActiveX),由於瀏覽器無法直接呼叫 Windows 底層的 TWAIN API,此模組必須被改寫為一隻安裝於 Client 端的 **背景常駐服務 (Local Windows Service / Daemon)**。
    該服務可以透過 **WebSocket** 或 **Local REST API** 接收來自網頁端的掃描指令,控制 TWAIN 掃描機,並將接收到的影像轉為 Base64 或二進位流回傳給網頁前端。
3.  **單一職責分離 (Separation of Concerns)**:
    應將 `OnAcquire` 中厚重的「條碼辨識」、「A3 裁切」與「黑邊處理」移出此模組。Scanner Controller 的唯一職責應該只有:**「將紙本轉為電腦中的 Bitmap」**。後續的處理應交給 `Image Processor` (影像處理模組) 透過管線 (Pipeline) 的方式接手處理。
doc/curtis/Transport_Manager_Analysis.md
比對新檔案
@@ -0,0 +1,69 @@
# 安全傳輸與 API 通訊模組 (Transport Manager) 深度分析
在 `CB_IMGPSScanImp.pas` 中,「安全傳輸與 API 通訊模組」負責處理 ActiveX 客戶端與後端伺服器 (如 Java Servlet 平台) 之間的所有資料交換與檔案傳輸。由於涉及機敏的金融/保險文件,此模組高度依賴加密與安全通訊協定。
若要將此模組進一步細拆,其內部組成可分為以下四大子分類:
---
## 1. HTTP/HTTPS API 請求管理器 (RESTful / Servlet Client Wrapper)
**職責**:專責透過 HTTP(S) 協定,向後端的 Java Servlet 端點發送 GET/POST 請求,用於同步時間、下載設定檔或回報案件狀態。
**核心實作特性**:
*   **套件依賴**:底層依賴 SecureBlackbox 的 `TElHTTPSClient` (在變數宣告為 `HTTPSClient`) 處理 SSL/TLS 連線。
*   **字串/表單資料交換**:
    *   `ProcessServlet_Get`:使用 HTTP GET 方式,將加密過的參數 (`SendData`) 送至伺服器,並將伺服器回傳的結果寫入 `FReWrite` 或 `Memo1` 中。
    *   `ProcessServlet` / `ProcessServlet_FormData`:使用 HTTP POST 或 `multipart/form-data` 方式提交較大的資料集(如 OMR 檢核結果、案件完成通知等)。
*   **狀態與錯誤攔截**:檢查伺服器回傳的代碼(例如回傳 `0` 代表成功,`1` 代表錯誤,或攔截回傳 HTML 中包含 `login.js` 來判斷 Session 是否逾時登出)。
## 2. 雙協定檔案上傳/下載引擎 (Dual-Protocol File Transfer Engine)
**職責**:處理實體影像檔案(通常是打包後的 ZIP 檔)或系統授權檔的實體傳輸,並根據系統設定動態切換 HTTP 或 FTP 協定。
**核心實作特性**:
*   **HTTP 檔案傳輸**:
    *   `upFile`:透過 HTTP POST (`multipart/form-data`) 將本地端的檔案 (如 `Img.zip` 或 `.lic` 檔) 上傳至伺服器指定的 Servlet 接收端。
    *   `dnFile_Get` / `dnFile`:透過 HTTP 協定從伺服器下載檔案 (如 `OMRSet.zip`, `KeyinSet.zip` 或舊案影像),並提供下載狀態監控。
*   **FTPS 檔案傳輸 (高頻寬/大檔專用)**:
    *   `GetftpInfo`:先透過 HTTP 向伺服器查詢該案件是否應使用 FTP 傳輸,並取回加密的 FTP 登入資訊。
    *   依賴 SecureBlackbox 的 `TElSimpleFTPSClient` 建立 FTP over SSL 連線。
    *   `SetFtpInfo`、`IIS_Ftp.FtpsConnect`、`IIS_Ftp.FtpsToMain`、`IIS_Ftp.FtpsDownloadFile`:負責實際的 FTP 登入、切換目錄、上傳與下載操作,完成後呼叫 `FtpCaseComplete` 透過 HTTP 通知伺服器傳檔結束。
## 3. 安全加密與雜湊模組 (Security, Crypto & Hashing Utils)
**職責**:負責資料傳輸前後的加解密運算,以及檔案完整性的 MD5 校驗,確保通訊過程不被竄改。
**核心實作特性**:
*   **字串加解密 (String Encryption)**:
    *   利用 `En_DecryptionStr_Base64` 函式(可能來自自訂或第三方的 `Encryp` 單元),對 URL 參數 (如 `checktime`, `col` 欄位清單) 進行加解密,避免敏感資訊在 URL 明文中裸奔。
    *   解析加密過的 FTP 登入資訊字串。
*   **檔案 MD5 雜湊校驗 (File Hashing)**:
    *   使用 Indy 的 `TIdHashMessageDigest5` 元件。
    *   `LoadFileGetMD5`:讀取本機影像檔並計算其 MD5 值,用於比對「異動件 (ESCAN)」模式下,檔案是否與伺服器原有的版本一致,防止誤刪或重複傳輸。
*   **憑證管理 (Certificate Management)**:
    *   `HTTPSClientCertificateValidate`:處理 X.509 憑證驗證事件,通常設定為 `Validate := True` 以略過嚴格的自簽憑證檢查(在內部封閉網路中常見的做法)。
## 4. 傳輸前置準備與封裝器 (Payload Archiver & Preparer)
**職責**:在真正啟動傳輸前,將硬碟上散落的 TIFF/JPG 檔案、`.dat` 索引檔以及 `.ini` 設定檔,打包成單一的封裝檔以利傳輸。
**核心實作特性**:
*   **ZIP 壓縮封裝**:
    *   依賴 `TVCLZip` (VCLZip 模組)。
    *   `ZipMainFile`:讀取 `Context.dat` 等清單,將案件目錄下的所有影像與關聯的索引文字檔打包成 `Img.zip`。
    *   包含檔案大小檢核邏輯:在打包完成後,檢查 ZIP 檔是否超過伺服器允許的上限 (`FMaxUploadSize`),若超過則阻擋上傳。
*   **ZIP 解壓縮處理**:
    *   依賴 `VCLUnZip` 模組。
    *   `ExecuteUnZip` / `ExecuteUnZip_Pwd`:將從伺服器下載回來的 `OMRSet.zip`、`KeyinSet.zip` 或舊案件影像壓縮檔解壓縮至指定的工作目錄中,並支援帶密碼的解壓縮。
---
## 💡 未來重構與微服務化建議
針對「安全傳輸與 API 通訊模組」,若未來系統架構演進,建議考量以下重構方向:
1.  **統一且標準化的 HTTP Client (捨棄舊版第三方庫)**:
    早期的 SecureBlackbox 雖然強大,但現代的開發環境 (如新版 Delphi 或 C#/Java/Node.js) 均已內建完善且高效的 `HttpClient`。建議將所有的 API 呼叫標準化為 RESTful API (JSON 格式),並使用原生的網路庫。
2.  **廢除 FTP,全面改用 HTTP(S) 分塊上傳 (Chunked Upload)**:
    目前系統需動態切換 HTTP 與 FTPS,增加了防火牆設定 (Port 21, Passive Ports) 與除錯的困難度。現代系統處理大檔上傳,主流做法是透過 HTTP/HTTPS 的 **分塊上傳 (Chunked Upload / Multipart)** 搭配斷點續傳機制,建議未來可逐步汰除 FTP 傳輸路徑。
3.  **非同步與背景傳輸 (Asynchronous Transfer)**:
    目前上傳時會透過 `DataLoading` 鎖死整個 UI (`Screen.Cursor := -11`),使用者必須枯等上傳完成。重構時應將 Transport Manager 放入獨立的 Thread 或背景 Service 中執行,並透過回呼 (Callback) 或事件 (Event) 更新進度條,提升使用者體驗。
4.  **Token-based 授權取代自訂加密**:
    目前依賴自訂的 `En_DecryptionStr_Base64` 在 URL 傳遞加密字串作為安全驗證。建議未來升級為標準的 **OAuth 2.0 (JWT Token)** 放在 HTTP Header 中進行授權,更加安全且符合現代 Web 規範。
doc/curtis/UI_View_Analysis.md
比對新檔案
@@ -0,0 +1,86 @@
# UI 視圖與介面呈現模組 (UI View) 深度分析
在 `CB_IMGPSScanImp.pas` 中,「UI 與介面呈現模組」主要負責與使用者的視覺互動,這是作為 ActiveX 控制項的核心表現形式。它管理了表單的佈局、多國語言切換、快捷鍵操作以及各種滑鼠拖拉 (Drag and Drop) 與捲動行為。
以下是針對 UI View 模組的深度組成與分類分析:
---
## 1. 核心版面與容器管理 (Layout & Container Management)
**職責**:定義整個 ActiveX 控制項的視覺區塊,並透過階層式的 Panel 進行排版管理,確保在不同解析度下能正確顯示。
**核心實作特性**:
*   **ActiveX 窗體繼承**:`TCB_IMGPSScanX` 繼承自 `TActiveForm`,並實作 `ICB_IMGPSScanX` 介面,是所有 UI 元件的根容器。
*   **Panel 排版系統**:使用大量的 `TPanel` (如 `Panel1`, `Panel2`, `Panel9` 等) 與 `TSplitter` (如 `Splitter1`, `Splitter2`) 來分割畫面:
    *   **頂部/底部工具列**:放置各種操作按鈕 (`TBitBtn`, `TButton`)。
    *   **左側樹狀區**:放置文件結構清單 (`TreeView1`)。
    *   **右側工作區**:通常包含影像預覽或細節設定面板 (`Panel9`, `ScrollBox1`)。
*   **動態模式切換**:透過 `DisplayMode` 與 `GoViewMode` 根據不同的視圖模式 (VMode) 動態調整右側影像預覽區塊的排列方式 (如 1x1, 1x2, 2x2 等),並控制各 `imgp` (Panel) 的顯示與隱藏。
## 2. 影像預覽與互動視窗 (Image Viewer & Interaction)
**職責**:提供高效能的影像顯示功能,支援大圖縮放、拖曳平移、局部放大以及多頁縮圖並排顯示。
**核心實作特性**:
*   **Envision 影像元件**:深度依賴 `EnImgScr` (TImageScrollBox) 來處理大型 TIFF/JPEG 影像的渲染與捲動。
*   **多圖預覽管理**:
    *   主影像 (`ISB1`) 與縮圖陣列 (`ISB2` ~ `ISB8`)。
    *   背景生成黑白影像 (`ISB_BW`) 供內部條碼辨識或影像處理使用,而不影響前端彩色顯示。
    *   動態建立/釋放:`CreatePreViewISB` 與 `FreePreViewISB` 負責根據案件頁數動態產生縮圖物件。
*   **滑鼠行為模式 (Mouse Mode)**:
    *   透過 `ViewMouseMode` 函式切換 `TImageScrollBox` 的狀態,例如:放大鏡 (`mmAmplifier`)、框選縮放 (`mmZoom`)、手形拖曳 (`mmDrag`)、旋轉 (`mmR90`, `mmR180`, `mmR270`) 或刪除 (`mmDelete`)。
    *   對應工具列按鈕 (`FC0` ~ `FC6`) 的點擊事件 (`FC0Click` ~ `FC6Click`) 來切換這些游標模式。
*   **滑鼠與滾輪事件**:
    *   處理自定義的滾輪捲動 (`WMMOUSEWHEEL`),同步捲動 `ScrollBox1` 或放大縮小影像 (`ZoomPercent`)。
    *   處理影像上的點擊 (`ISBClick`) 與拖拉排序 (`ISBDragDrop`, `ISBDragOver`)。
    *   在影像上繪製選取框 (`PaintShape`, `FreeShapeobj`)。
## 3. 案件與文件樹狀檢視 (Case & Document TreeView)
**職責**:以階層式樹狀結構呈現當前掃描或下載的「案件 -> 文件 -> 頁面」,並提供豐富的右鍵選單功能以管理這些節點。
**核心實作特性**:
*   **TTreeView 結構維護**:
    *   `NewTreeNode`:頂層節點(如:新件、異動件)。
    *   `MyTreeNode1`:案件層級 (CaseID)。
    *   `MyTreeNode2`:文件層級 (DocNo/DocDir)。
    *   `MyTreeNode3`:表單/頁面層級 (FormID)。
*   **視覺狀態回饋**:依據節點的狀態改變圖示 (`ImageIndex`),例如:未配號、檢核失敗、完成檢核、或標記為被引用 (`GetUseCase`) 的文件。
*   **節點資料綁定與更新**:
    *   利用 `DrawDocItem2` 根據實體的 `.dat` 設定檔重新長出整棵樹。
    *   利用 `NewTreeNodeRefresh`, `MyTreeNode1Refresh` 等更新節點後方的筆數或頁數統計。
*   **右鍵選單與節點操作**:
    *   根據選中的節點層級,動態顯示/隱藏 `PopupMenu1` 的選項 (`PopupMenu1Popup`)。
    *   支援在樹狀圖上的拖拉移動 (`TreeView1DragDrop`) 以改變影像歸屬。
## 4. 多國語言與本地化引擎 (I18N & Localization)
**職責**:將 UI 介面上的所有寫死字串抽離,透過外部設定檔實現多國語言的動態切換。
**核心實作特性**:
*   **語言檔載入**:在初始化時透過 HTTP/FTP 下載最新的語言包 (`Language.Lng` 至 `LngPath`)。
*   **動態介面翻譯**:`InitialLanguage` 函式。
    *   利用 `TMemIniFile` 讀取語言檔。
    *   遍歷表單上的所有元件 (`Components`),判斷其類型 (`TButton`, `TBitBtn`, `TMenuItem`, `TLabel`, `TListView`, `TRadioGroup` 等)。
    *   將元件的 `Name` 作為 Key 去 Ini 檔中尋找對應的翻譯字串,並替換其 `Caption` 或 `Hint`。
## 5. 系統狀態與進度回饋 (Status & Progress Feedback)
**職責**:在進行耗時的網路傳輸、影像處理或資料庫比對時,鎖定 UI 並給予使用者明確的等待提示,避免重複操作。
**核心實作特性**:
*   **全域 UI 鎖定 (`DataLoading`)**:
    *   當執行耗時任務時,將滑鼠游標改為漏斗 (`crHourGlass`)。
    *   停用主要的控制面板 (`Panel1.Enabled := False`, `Panel2.Enabled := False`)。
    *   顯示中央的進度提示面板 (`Panel22` 或 `Panel8`)。
*   **動態文字更新**:透過 `Timer2` 定期在進度提示文字 (`ShowText`) 後方加上點號 (`...`),營造系統正在運作的視覺效果。
*   **狀態列 (`StatusBar1`)**:顯示當前版本號 (`GetCurrentVersionNo`)、登入使用者名稱、引用狀態,以及浮動授權 (License) 的剩餘額度。
---
## 💡 未來重構與微服務化建議
若要將此「UI View 模組」進行現代化重構 (例如轉向 Web 介面或更清晰的架構),建議考量以下方向:
1.  **MVC / MVVM 架構解耦**:目前的程式碼將按鈕點擊事件 (Controller)、資料讀取 (Model) 與畫面更新 (View) 混雜在同一個 Pas 檔中。應將邏輯抽離,讓 UI 只負責「顯示資料」與「轉發命令」。
2.  **TreeView 虛擬化 (Virtualization)**:若案件內包含極多頁面,頻繁的 `DrawDocItem2` 清空並重建樹狀節點會導致效能低落。建議改用 Virtual TreeView 或資料綁定 (Data Binding) 的方式,只渲染可見節點。
3.  **分離多國語言引擎**:將 `InitialLanguage` 的邏輯獨立為一個全局的 `Translator` 服務,避免每個 Form 或 Frame 都需要自己實作載入邏輯。
4.  **Web 相容性評估**:
    *   由於深切依賴 `TImageScrollBox` 的 Windows GDI 渲染與特定的滑鼠狀態 (`TMouseMode`),若要轉為 Web (HTML5/Canvas),需尋找或實作對應的 Web Image Viewer 套件。
    *   ActiveX 特有的 COM 介面 (`TActiveForm`, `safecall`) 將被淘汰,改以 WebSocket 或 REST API 提供狀態更新,前端由 React/Vue 等框架接手渲染。
doc/curtis/delphi_conventions.md
比對新檔案
@@ -0,0 +1,249 @@
# **Delphi 命名規範與風格指南 (Naming Conventions)**
## **1. 大小寫敏感度 (Case Sensitivity)**
在探討具體規範前,我們必須先釐清 Delphi 編譯器的特性:
- **Delphi 是「大小寫不敏感」的 (Case-Insensitive):**
  對 Delphi 編譯器而言,CaseID、caseid、CASEID 甚至 cAsEiD 都是**完全相同**的變數。這與 C++, C#, Java, JavaScript (這些是 Case-sensitive) 有著根本上的不同。
- **為什麼還需要規範?**
  既然編譯器不在乎,為什麼大家還是很嚴格遵守大小寫?因為**「人」在乎**。統一的大小寫規範(通常是 PascalCase)能極大提升程式碼的可讀性。
- **何時大小寫「絕對」重要?**
  當 Delphi 與外部世界溝通時!例如:
1. 呼叫 C/C++ 寫的 DLL 函數(如 Windows API)。
2. 處理 JSON/XML 資料解析時。
3. 將 ActiveX/OCX 元件提供給網頁端的 JavaScript 呼叫時(這也是為什麼你在 _TLB.pas 裡會看到像 caseno 這種全小寫的屬性,因為它是為了迎合 JS/Web 的習慣)。
## **2. 常見的程式命名風格 (Casing Styles)**
了解以下三種風格,有助於我們理解 Delphi 的選擇:
1. **PascalCase (帕斯卡命名法)**:每個單字的首字母都大寫。
- *範例*:CalculateTotal, UserName, CaseID
- **Delphi 的最愛**:Delphi 絕大多數的命名都基於這個風格。
1. **camelCase (駱駝命名法)**:第一個單字全小寫,後續單字首字母大寫。
- *範例*:calculateTotal, userName, caseId
- **Delphi 的應用**:偶爾用於區域變數(Local Variables),但在傳統 Delphi 中不如 Java/C# 常見。
1. **snake_case (蛇形命名法)**:全小寫,單字間用底線 _ 分隔。
- *範例*:calculate_total, user_name, case_id
- **Delphi 的應用**:**極少使用**。通常只出現在早期的舊程式碼,或是為了與 C 語言庫(如 SQLite, LibVLC)保持一致時才會使用。
## **3. Delphi 核心命名規範 (前綴字慣例)**
Delphi 最具特色的就是使用**單一字母前綴**來標示該元素的「種類」或「作用域」。
| **元素類型 (Type)** | **前綴** | **命名風格** | **範例與說明** |
| --- | --- | --- | --- |
| **類別 (Class/Type)** | **T** | PascalCase | TForm, TCustomer
*(T 代表 Type,所有類別與自訂型別皆以此開頭)* |
| **介面 (Interface)** | **I** | PascalCase | IUnknown, IScanner
*(I 代表 Interface)* |
| **例外 (Exception)** | **E** | PascalCase | EOutOfMemory, EFileNotFound
*(E 代表 Exception)* |
| **指標 (Pointer)** | **P** | PascalCase | PChar, PInteger
*(P 代表 Pointer)* |
| **私有欄位 (Field)** | **F** | PascalCase | FCaseID, FWidth
*(F 代表 Field,僅存在於類別的 private/protected 區段)* |
| **屬性 (Property)** | **無** | PascalCase | CaseID, Width
*(對外公開的屬性,通常用來讀寫對應的 F 變數)* |
| **方法參數 (Parameter)** | **A** | PascalCase | AOwner, ACaseID
*(A 代表 Argument,用以避免與內部 Field 撞名)* |
## **4. 變數與常數命名**
- **區域變數 (Local Variables)**:
- 通常使用 **PascalCase**,不加前綴。例如:ImageStream, TotalCount。
- 迴圈計數器習慣使用大寫字母:I, J, K。
- **全域變數 (Global Variables)**:
- 盡量避免使用。若必須使用,部分開發者會加上 G 前綴(如 GSystemStatus)。
- **常數 (Constants)**:
- 現代 Delphi 傾向直接使用 PascalCase 配合列舉。部分開發者使用 c 前綴(如 cMaxRetries)。
## **5. 列舉型別 (Enumerations) 的特殊規範**
Delphi 對列舉型別有非常優雅的規範:**型別以 T 開頭,列舉值以該型別的 2~3 個字母縮寫作為小寫前綴。**
- *範例*:
  type
  TTransMode = (tsHttp, tsFtp, tsNone);
  TFormAlign = (alNone, alTop, alBottom, alLeft, alRight, alClient);
## **6. 檔案與單元 (File & Unit) 命名**
在 Delphi 中,檔案名稱(.pas)與程式碼內宣告的單元名稱(unit XXX;)**必須完全一致**。
- **基本規則**:使用 **PascalCase**,避免使用空白與特殊字元。
- **視覺化窗體 (Forms)**:單元檔名通常結尾加上 Frm、Form 或 UI。
- **資料模組 (DataModules)**:單元檔名通常結尾加上 DM。
## **7. 檔案命名後綴與副檔名解析 (File Extensions & Suffixes)**
在 Delphi 的專案架構中,除了單元名稱外,副檔名與特定的檔名後綴也承載了重要的架構意義。
### **7.1 檔名後綴 _TLB**
當你開發或匯入 COM、ActiveX 或 Automation Server 時,Delphi IDE 會自動生成以 _TLB 結尾的檔案(例如 CB_IMGPSScan_TLB.pas)。
- **意義**:TLB 是 **Type Library (類型庫)** 的縮寫。
- **作用**:Type Library 本身是一個二進位檔案,用來跨語言描述 COM 元件的介面。Delphi 為了讓 Object Pascal 能呼叫這些介面,會將其反編譯成 .pas 程式碼,並自動加上 _TLB 後綴。
- **⚠️ 核心規範**:**絕對不要手動修改 _TLB.pas 檔案的內容!** 只要你在 IDE 中重新整理 Type Library,這個檔案就會被完全覆蓋重寫。若要實作功能,應寫在對應的 _Imp.pas 或其他單元中。
### **7.2 常見的 Delphi 副檔名**
以下是 Delphi 專案中常見的副檔名及其具體用途:
| **副檔名** | **類型** | **說明** |
| --- | --- | --- |
| **.pas** | 原始碼 (Source) | **Pascal Source File**。最常見的程式碼檔案(單元/Unit),包含介面宣告 (interface) 與實作邏輯 (implementation)。 |
| **.dpr** | 專案檔 (Project) | **Delphi Project**。專案的主進入點,定義這是一個執行檔 (program) 還是程式庫 (library),並包含專案的啟動邏輯與引入單元列表。 |
| **.dfm** | 介面檔 (Form) | **Delphi Form**。儲存視覺化窗體 (Form) 或資料模組 (DataModule) 上所有元件的屬性與佈局設定。一定與同名的 .pas 檔成對出現。 |
| **.dcu** | 編譯檔 (Compiled) | **Delphi Compiled Unit**。.pas 檔案成功編譯後產生的二進位結果。最終連結器 (Linker) 會將這些 .dcu 打包成 EXE 或 DLL。 |
| **.res** | 資源檔 (Resource) | **Compiled Resource File**。二進位資源檔,包含應用程式圖示 (Icon)、版本資訊或夾帶的檔案。通常在 .dpr 中用 {$R *.res} 編譯指令引入。 |
| **.tlb** | 類型庫 (Type Lib) | **Type Library**。微軟 COM 標準的二進位檔案,用來描述 ActiveX 元件對外公開的介面、方法與屬性,供其他語言(如 C++, JS)參考呼叫。 |
| **.ridl** | 介面定義 (RIDL) | **Restricted Interface Definition Language**。較新版 Delphi 用來取代舊式二進位 .tlb 的純文字介面定義檔。開發者可以透過文字或 IDE 編輯它,編譯時會自動生成對應的 .tlb 與 _TLB.pas。 |
| **.ocx** | 控件擴充檔 | **OLE Control Extension**。本質上是一個特殊格式的 DLL(動態連結函式庫),內部包含了 ActiveX 控件。Windows 系統可以透過 regsvr32 註冊它,讓 IE 瀏覽器或其他應用程式嵌入使用。 |
## **8. Delphi 架構與現代語言概念對應 (Architecture Mapping)**
對於習慣 C#、Java、Python 或 JavaScript 的開發者,了解 Delphi 專屬的結構名詞如何對應到現代語言是非常重要的。
### **8.1 Namespace (命名空間)**
- **現代語言對應**:等同於 C# 的 namespace、Java 的 package,或是 C++ 的 namespace,用來組織程式碼並避免名稱衝突。
- **Delphi 的對應**:
- **早期 Delphi (如 Delphi 7/2007)**:沒有明確的 Namespace 關鍵字,**unit (單元) 本身就充當了扁平化的命名空間**。為避免撞名,開發者強烈依賴「前綴字」(例如 Indy 網路套件都以 Id 開頭,如 IdHTTP;Envision 影像套件以 En 開頭,如 EnScan)。
- **現代 Delphi (XE2 以後)**:正式引入了 **Dotted Unit Names (點綴命名)** 作為 Namespace 的支援。例如原先的 SysUtils.pas 變成了 System.SysUtils.pas,Forms.pas 變成了 Vcl.Forms.pas。
### **8.2 Class (類別)**
- **現代語言對應**:等同於 C# / Java / TS 的 class。
- **Delphi 的對應**:同樣使用 class 關鍵字,但在 Delphi 中必須宣告於 type 區塊之下。如前文所述,類別名稱慣例上**必須以大寫 T 開頭**(例如 TMainForm)。實體化時,Delphi 是呼叫建構子方法(如 TMyClass.Create),而不是使用 new 關鍵字。
### **8.3 Unit (單元, .pas)**
- **現代語言對應**:等同於 ES6 的 **Module (.js/.ts)**、Python 的 **Module (.py)**,或是 C/C++ 中 .h (標頭檔) 與 .cpp (原始檔) 的綜合體。
- **Delphi 的對應**:unit 是 Delphi 程式碼封裝的最核心基礎。一個完整的 unit 會被劃分為兩個主要區塊:
- **interface (介面區)**:等同於對外輸出的 API 或 Header,這裡宣告的型別與函式可透過 uses 關鍵字被其他檔案呼叫。
- **implementation (實作區)**:等同於內部的私有邏輯實作。在這裡宣告的變數或函式,對外部檔案是完全隱藏的。
### **8.4 Library (程式庫, .dpr)**
- **現代語言對應**:等同於 C# 的 **Class Library 專案**、Node.js 的原生 C++ 附加元件,或 C/C++ 的 **Shared Library / 動態連結檔**。
- **Delphi 的對應**:在專案原始檔中(.dpr),如果開頭宣告為 library(而非一般視窗程式的 program),代表它的編譯目標是一個不具獨立執行程序的主體,通常會編譯為 **.dll** 或 **.ocx**。這類專案主要透過 exports 關鍵字,將內部的函式暴露給作業系統或其他語言的程式(如 C++ 或網頁瀏覽器)來呼叫。
## **9 Compiler Directive**
在 Delphi (Object Pascal) 中,`{ }` 符號的意義取決於緊跟在 `{` 後面的第一個字元。這是一個非常基礎但關鍵的語法,讓我們來拆解你的疑問:
---
### 9.1 `{ }` 符號的基本意義:註解 (Comments)
在最基本的情況下,`{ }` 是 **「區塊註解」**。編譯器會完全忽略裡面的內容。
* **`{ TCB_IMGPSScanX }`**:這是一段 **註解**。
    * **作用**:通常開發者寫這行是為了告訴讀程式碼的人:「接下來這一段程式碼是屬於 `TCB_IMGPSScanX` 類別的實作」。它對程式執行**沒有任何影響**。
* **`TCB_IMGPSScanX`**:這是一段 **程式碼 (識別碼)**。
    * **作用**:編譯器會去尋找名為 `TCB_IMGPSScanX` 的類別、型別或變數。如果沒定義,編譯就會報錯。
---
### 9.2 `{$ }` 符號的進階意義:編譯指令 (Compiler Directives)
當 `{` 後面緊跟著 **`$`** 字元時,它就從「註解」變成了 **「編譯指令」**。這不是給人看的,而是**給編譯器下的指令**。
#### 關於 `{$I filename}` (Include 指令)
你看到的 `{$I ...}` 是 **Include Directive**。
* **作用**:它的功能就像是編譯器的「複製貼上」工具。它告訴編譯器:「在編譯這行時,請先去把 `CB_IMGPSScanImp_UI.pas` 檔案裡的文字**原封不動地抓過來,塞到這個位置**,然後再繼續往下編譯」。
* **目的**:這通常用來處理非常巨大的程式碼檔案。就像你之前的 `CB_IMGPSScanImp.pas` 檔案太長了,開發者把它拆成小檔案,最後用這種方式在主檔「匯合」。
---
### 9.3 解析你的範例語法
你提到的這一段:
`{$I CB_IMGPSScanImp_UI.pas}{$I CB_IMGPSScanImp_Scan.pas}...initialization`
#### 這是在做什麼?
這既不是定義,也不是宣告,更不是字串。它是一種 **「程式碼組合行為」**。
1.  **組合實作內容**:編譯器會依序讀入 `UI.pas`、`Scan.pas`、`Data.pas`、`Utils.pas`。這四個檔案裡放的通常是 `TCB_IMGPSScanX` 的各種 `procedure` 和 `function` 實作。
2.  **銜接初始化區段**:當上述所有檔案的內容都被「貼進來」後,程式碼剛好接到了 `initialization` 關鍵字。
#### 語法邏輯如下:
```pascal
implementation
// 編譯器開始貼上檔案
[貼上 UI.pas 的內容]
[貼上 Scan.pas 的內容]
[貼上 Data.pas 的內容]
[貼上 Utils.pas 的內容]
// 貼完後,剛好銜接這支單元的初始化邏輯 (AxCtrl)
initialization
  TActiveFormFactory.Create(...);
  SetLicenseKey(...);
end.
```
### 總結
* **`{ ... }`** = 註解(編譯器不看)。
* **`{$I ...}`** = 編譯器指令(叫編譯器去別的檔案搬程式碼過來貼上)。
* **為什麼這樣寫?** 這是為了讓主檔保持簡潔。主檔只負責「宣告結構」和「啟動初始化」,而「具體的細節邏輯」則被藏在那些被 Include 的檔案裡。
## 10. 註解語法與風格慣例 (Comments Style)
Delphi 提供三種主要的註解方式,以及一些開發者社群常用的視覺化風格:
### 10.1 單行註解 (`//`)
- **用法**:從 `//` 開始到該行結束。
- **視覺強調 (`////`)**:
    - 你在源碼中看到的 `////` 語法上等同於 `//`。
    - **開發者意圖**:通常用於**「標題化」**或**「強力分隔」**。多個斜線能在視覺上形成橫條效果,幫助開發者在快速捲動程式碼時一眼認出關鍵區塊(例如:`//// ***** 預設區 ***** ////`)。
### 10.2 區塊註解 (`{ }`)
- **用法**:被 `{ }` 包圍的所有文字。
- **特點**:常用於長篇說明的標註,或是在調試時暫時關閉一整段程式碼。
- **編譯指令**:若開頭為 `{$`(如 `{$R *.DFM}`),則代表它是給編譯器的指令,而非一般註解。
### 10.3 替代區塊註解 (`(* *)`)
- **用法**:由 `(*` 開始到 `)` 結束。
- **主要用途 (註解嵌套)**:當你想要註解掉一段已經包含 `{ }` 註解的程式碼時,Delphi 規定 `{ }` 不能包在另一對 `{ }` 裡面,此時必須改用 `(* ... *)` 來包裹,才能正確註解。
### 10.4 文件化註解 (`///` XML Documentation)
- **用法**:在方法上方使用連續三個斜線。
- **作用**:現代 Delphi IDE (XE 以後) 會解析這類註解,並在鼠標懸停於方法名上時顯示提示說明(類似 JavaDoc 或 C# XML Doc)。
### 10.5 分隔線風格示例
在大型檔案(如 `CB_IMGPSScanImp.pas`)中,常用註解來劃分功能界線:
```
// ********************************************************************* //
// 這是區塊分隔線風格,常用於自動生成的 TLB 檔
// ********************************************************************* //
{-----------------------------------------------------------------------}
{  這是另一種常見的手寫分隔線風格                                          }
{-----------------------------------------------------------------------}
```
doc/curtis/index.md
比對新檔案
@@ -0,0 +1,15 @@
## TableOfContent
- [IO_說明.md](IO_%E8%AA%AA%E6%98%8E.md)
- [ScanImp_分析.md](ScanImp_%E5%88%86%E6%9E%90.md)
- [介面實作.md](%E4%BB%8B%E9%9D%A2%E5%AF%A6%E4%BD%9C.md)
- [delphi_專案必讀.md](delphi_%E5%B0%88%E6%A1%88%E5%BF%85%E8%AE%80.md)
- [EnScan_拆解.md](EnScan_%E6%8B%86%E8%A7%A3.md)
- 各生命周期 or diagram
- Remote/Local IO 方法
- Remote/Local IO 抽象介面
- OCX 抽像介面
-
doc/curtis/remote_相關方法.md
doc/curtis/分析_尚進行中.md
比對新檔案
@@ -0,0 +1,830 @@
# **相依套件分析報告**
本報告針對 ActiveX 元件核心實作檔 CB_IMGPSScanImp.pas 進行功能解構,並分析其與第三方套件的綁定關係,以作為後續維護或現代化升級之參考。
## **壹、 依功能拆分模組與第三方套件綁定分析**
根據原始碼結構,CB_IMGPSScanImp.pas 是一個將介面、硬體、網路與業務邏輯高度耦合的「上帝物件 (God Object)」。若要進行重構或拆件,建議依據以下五個核心模組進行解耦:
### **1. 硬體掃描控制模組 (Scanner Controller)**
- **功能描述**:負責與實體掃描機溝通,處理 TWAIN 介面初始化、設定掃描參數(DPI、色彩、雙面)、觸發掃描指令,以及接收掃描完成的記憶體影像 (DibHandle)。
- **關鍵方法**:initkscan、StatrTwainScan、OnAcquire、PageEnd、PageDone。
- **第三方套件綁定 (強綁定)**:
- 依賴 **Envision Imaging SDK** (EnScan) 進行 TWAIN 驅動控制。
- 依賴 EnMisc 處理底層記憶體或雜項運算。
### **2. 影像處理與轉換模組 (Image Processor)**
- **功能描述**:負責對擷取到的原始影像進行後製處理,包括:裁切(如 A3 裁 A4)、去斜 (Deskew)、旋轉、二值化 (轉黑白)、格式轉換 (轉 TIFF/JPEG),以及讀取影像上的條碼。
- **關鍵方法**:ImageReSize_FormID、CheckNeedCrop、DeskewImg、ConvertToBW。
- **第三方套件綁定 (強綁定)**:
- 依賴 **Envision Imaging SDK** (EnTifGr, EnDiGrph) 進行 TIFF (CCITT Group 4) 壓縮與 DIB 物件封裝。
- 依賴 **MPS Barcode** (mpsBarco, BarcodesFinder) 進行影像條碼掃描與解析。
### **3. 安全傳輸與 API 通訊模組 (Transport Manager)**
- **功能描述**:負責將掃描並壓縮後的檔案上傳至伺服器,或從伺服器下載檔案。同時處理與後端 Java Servlet 的參數交換(如校時、取得 FTP 資訊、更新案件狀態)。
- **關鍵方法**:upFile、dnFile_Get、ProcessServlet_Get、GetServerDate、GetftpInfo。
- **第三方套件綁定 (強綁定)**:
- 依賴 **SecureBlackbox** (SBSimpleSSL, SBHTTPSClient, SBSimpleFTPS 等) 處理 HTTPS/FTPS 安全加密通訊。
- 依賴 **Indy** (IdHashMessageDigest) 產生檔案的 MD5 檢核碼。
- 依賴自訂/第三方加密庫 (Encryp) 處理字串加解密。
### **4. 業務邏輯與資料解析模組 (Business Logic & Parser)**
- **功能描述**:專案核心的領域邏輯,包含接收網頁端傳入的參數字串進行解析、定義文件歸類邏輯(由條碼推導文件類型)、OMR (光學標記辨識) 檢核定義等。
- **關鍵方法**:SetSQLData (負責解析傳入的長字串至 TStringList)、GetSQLData、FormCode2DocNo、OMRCheckCase。
- **第三方套件綁定 (中度綁定)**:
- 依賴 **Xmltool** 解析 XML 格式的 OMR 座標定義。
- 依賴 **VCLZip** (VCLZip, VCLUnZip) 將多個影像檔打包成 ZIP 進行單一傳輸。
### **5. UI 與介面呈現模組 (UI View)**
- **功能描述**:負責 ActiveX 控制項的視覺化介面,包含左側的樹狀文件清單 (TreeView)、右側的影像預覽區 (ScrollBox),以及按鈕與多國語言切換。
- **關鍵方法**:InitialLanguage、各類 OnClick 事件。
- **第三方套件綁定 (輕度綁定)**:
- 依賴 **Envision Imaging** (EnImgScr) 作為支援大影像拖曳的滾動視窗。
- 依賴 PJMenuSpeedButtons 作為自訂外觀的按鈕元件。
## **貳、 第三方套件功能總覽**
為達成上述複雜的業務需求,本專案引入了多個 Delphi 著名的第三方套件。以下為各套件的大致功能說明:
### **1. Envision Imaging SDK (前綴 En)**
這是一套專為 Delphi 設計的高效能影像與 TWAIN 處理套件,在金融與保險業的文件電子化系統中非常常見。
- **EnScan**:封裝了 TWAIN 協定,讓程式能直接控制實體掃描機,控制進紙、解析度與雙面掃描。
- **EnDiGrph (TDibGraphic)**:封裝 Windows 底層的 DIB (Device Independent Bitmap) 記憶體句柄,讓開發者能像操作一般圖片一樣操作掃描出來的原始資料。
- **EnTifGr**:專門處理 TIFF 影像格式。銀行業愛用 TIFF 是因為它支援「多頁」且黑白壓縮率極高 (Group 4 Fax Compression)。
- **EnImgScr**:提供 TImageScrollBox 視覺元件,用來平滑顯示並縮放超大尺寸的掃描圖檔。
- **EnMisc**:提供影像處理所需的底層輔助數學或資料轉換函式。
### **2. SecureBlackbox (前綴 SB)**
由 EldoS (後被 /n software 收購) 開發的強大安全通訊與加密套件。由於早期 Delphi (D7~D2007) 內建網路庫缺乏完善的 SSL/TLS 支援,此套件被廣泛用於補強安全性。
- **SBHTTPSClient / SBSimpleSSL**:實作了完整的 HTTPS 用戶端通訊協定,用來安全地上傳檔案或呼叫 Servlet API。
- **SBSimpleFTPS**:支援 FTP over SSL (FTPS) 的安全檔案傳輸。
- **SBWinCertStorage / SBX509**:處理 Windows 系統內的憑證儲存區,以及 X.509 數位憑證的驗證(例如確認銀行伺服器的 HTTPS 憑證是否合法)。
### **3. MPS Barcode & BarcodesFinder**
- **功能**:這是一套條碼識別引擎。
- **專案應用**:在掃描過程中(如 PageDone 或 OnAcquire 階段),它會掃描圖片特定區域尋找一維或二維條碼。讀取到的條碼值會被用來決定這張影像屬於哪一種「文件表單」(FormCode),進而實現自動分類歸檔。
### **4. VCLZip (VCLZip, VCLUnZip)**
- **功能**:老牌的 Delphi ZIP 壓縮元件。
- **專案應用**:為了節省網路頻寬與減少伺服器連線次數,掃描完成的多張 TIFF/JPG 檔案會被打包成一個 .zip 檔後,再一次性上傳至伺服器。
### **5. Indy Components (前綴 Id)**
- **IdHashMessageDigest / idHash**:Delphi 內建的 Internet Direct (Indy) 函式庫。
- **專案應用**:在此專案中被用來計算檔案或字串的 **MD5 Hash (雜湊值)**。通常用於確保檔案上傳過程沒有損壞,伺服器比對 MD5 一致才算上傳成功。
### **6. 其他工具 (Tools & Utils)**
- **Xmltool**:負責讀寫 XML 檔案。銀行經常將表單的 OMR (光學標記辨識) 檢核座標或參數寫成 XML 設定檔,透過此工具讀入系統。
- **Encryp**:這可能是開發商自訂或第三方的輕量級加密單元,用來加密本機設定檔 (.ini) 內的敏感資訊或混淆網頁傳入的參數。
# **核心源碼與物件實作解析**
本文件針對 CB_IMGPSScanImp.pas 中的核心方法與隱藏物件進行原始碼級別的邏輯拆解與推導。
## **壹、 核心方法源碼說明**
### **1) TCB_IMGPSScanX.GetCurrentVersionNo**
- **功能描述**:取得目前 OCX 元件的版本號。這通常提供給網頁端的 JavaScript 呼叫,用來判斷客戶端的 OCX 是否需要觸發更新下載。
- **實作推導與底層 API 說明**:
  在 Delphi 中,若要動態讀取編譯進 OCX 的 VS_VERSION_INFO 資源,標準作法是呼叫一系列的 Windows API。這段實作通常包含以下三個關鍵概念:
1. **HInstance (實體句柄)**:
- 這是 Delphi 提供的一個全域變數,代表目前載入記憶體中的模組(即這個 OCX 檔)的「實體句柄 (Instance Handle)」。
- 在 Windows 作業系統中,當一個執行檔 (EXE) 或動態連結檔 (DLL/OCX) 被載入記憶體時,會獲得一個記憶體基底位址,HInstance 就是指向這個位址的指標。Windows 需要這個值來識別「你要操作的是哪一個載入的模組」。
1. **GetModuleFileName 的作用**:
- 這是一個 Windows API。它的作用是利用前面提到的 HInstance,向作業系統反查出這個模組在硬碟上的**「完整實體檔案路徑」**(例如:C:\Windows\Downloaded Program Files\CB_IMGPSScan.ocx)。
- 因為我們需要讀取檔案本身的版本資訊,程式必須先知道「我自己到底存放在硬碟的哪裡」。
1. **後半段 VerQueryValue 的運作邏輯**:
   取得檔案路徑後,讀取版本資訊會經過三部曲,而 VerQueryValue 是最後負責「解析」的關鍵:
- **Step 1 (GetFileVersionInfoSize)**:先問 Windows 這個檔案的版本資訊區塊有多大。
- **Step 2 (GetFileVersionInfo)**:把整塊版本資訊的二進位原始資料 (Raw Data) 完整讀進一段記憶體緩衝區中。
- **Step 3 (VerQueryValue)**:因為剛剛讀進來的是一包無法直接閱讀的二進位資料,VerQueryValue 的作用就是**「在這包資料中進行精準查詢」**。透過傳入特定的查詢路徑(例如傳入 \ 可以取得包含主版號、次版號的 VS_FIXEDFILEINFO 結構體;或是傳入特定語系代碼如 \StringFileInfo\040404E4\FileVersion),作業系統會幫你把二進位資料解析成我們需要的字串或數字版本號(如 1.0.3.5)。
### **2) TCB_IMGPSScanX.ActiveFormCreate**
- **功能描述**:這是 ActiveX 視覺化窗體 (Form) 的生命週期起點。當 IE 瀏覽器或應用程式載入這個 OCX 時,首先觸發的初始化事件。
- **實作推導**:
  它會負責初始化全域變數、設定第三方元件的預設值、並建立暫存資料夾。
    ```pascal
     procedure TCB_IMGPSScanX.ActiveFormCreate(Sender: TObject);
      begin
        // 1. 預設變數初始化
        Def_DeviceDelete := True;
        Def_ScannerReverse := False;
        // 2. 建立本機暫存目錄
        ForceDirectories('C:\Temp\IMGPSScan');
        // 3. UI 語系初始化
        InitialLanguage;
      end;
    ```
### **3) TCB_IMGPSScanX.StatrTwainScan**
- **功能描述**:啟動掃描機的 TWAIN 介面,開始進紙或掃描。(註:Statr 應為 Start 的早期拼字錯誤,這在舊專案中很常見)。
- **實作推導**:
  這個方法會讀取網頁傳進來的參數(如 DPI、色彩),設定給 Scanner 物件,然後發出擷取指令。
    ```pascal
      procedure TCB_IMGPSScanX.StatrTwainScan;
      begin
        // 設定解析度與顏色
        Scanner.Resolution := StrToIntDef(Fimgdpi, 200);
        if Fscancolor = 'C' then
          Scanner.PixelType := ptColor
        else
          Scanner.PixelType := ptGray;
        // 啟動掃描 (會喚起實體掃描機)
        Scanner.Acquire;
      end;
    ```
### **4) TCB_IMGPSScanX.OnAcquire 與資料流物件**
- **功能描述**:這是 Scanner (EnScan) 最核心的事件。當掃描機成功掃完「一張」影像並傳入電腦記憶體時,會觸發此事件。
- **源碼邏輯推導**:
    ```pascal
    procedure TCB_IMGPSScanX.ScannerAcquire(Sender: TObject; DibHandle: THandle; var Handled: Boolean);
      var
        MyStream: TMemoryStream;
        MyGraphic: TDibGraphic;
      begin
        // 1. 將 DibHandle 轉換為 Delphi 影像物件
        MyGraphic := TDibGraphic.Create;
        MyGraphic.Handle := DibHandle;
        // 2. 影像處理 (如去斜、黑白轉換)
        // 3. 存入記憶體串流或實體檔案
        // ...
      end;
    ```
- **三大核心資料物件解釋**:
1. **DibHandle**:這是 Windows 底層的「全域記憶體句柄 (Global Memory Handle)」。掃描機驅動把圖片放在記憶體裡,給你這個代號。你必須透過 TDibGraphic 來「解讀」這塊記憶體。
2. **TMemoryStream**:**記憶體串流**。它的作用像是一塊存在 RAM 裡的虛擬硬碟。掃描後的圖檔在還沒存到實體硬碟前,會先放在這裡進行格式轉換 (如轉成 JPEG) 或進行 MD5 計算,因為在記憶體中操作速度最快。
    - **DeleteStm : TMemoryStream 的逐行操作說明**:
      在 `OnAcquire` 事件中,`DeleteStm` 主要用於「空白頁判定」。其源碼邏輯如下:
    ```pascal
    // 判定是否啟動空白頁刪除功能
    if Def_DeviceDelete then
    begin
      // 1. 在 Heap 記憶體中建立一個暫存串流物件
      DeleteStm := TMemoryStream.Create;
      try
        // 2. 將當前掃描到的影像物件 (MyGraphic) 寫入記憶體串流
        // 這會根據 TIFF G4 等格式進行壓縮,產生實際的檔案位元組資料
        MyGraphic.SaveToStream(DeleteStm);
        // 3. 核心判定:檢查該影像在記憶體中所佔用的位元組大小 (Size)
        // 若小於預設門檻值 (Def_DeviceDeleteSize, 如 3072 Bytes)
        if DeleteStm.Size < Def_DeviceDeleteSize then
        begin
          // 4. 若符合空白頁特徵,則不進行存檔動作,直接跳出此 Procedure
          // 並告知掃描控制項此頁已處理完畢 (Handled := True)
          Handled := True;
          Exit;
        end;
      finally
        // 5. 無論是否為空白頁,操作結束後必須釋放記憶體,避免 Memory Leak
        DeleteStm.Free;
      end;
    end;
    ```
   -
3. **TFileStream**:**檔案串流**。用來直接對硬碟讀寫資料。當確認影像無誤後,會透過 TFileStream 將 TMemoryStream 的內容真實地寫入 C:\Temp\...\001.tif 中。
### **5) TCB_IMGPSScanX.FindISB2View**
- **功能描述**:用來在畫面上尋找並更新用來預覽圖片的元件。
- **名詞解釋**:**ISB 代表 ImageScrollBox**。
  這對應到 uses 區段中的 EnImgScr 套件。TImageScrollBox 是 Envision 提供的一個 UI 元件,專門用來顯示超出螢幕大小的掃描圖檔,支援平滑拖曳與縮放。這個方法通常用來把剛掃描完的 DibHandle 餵給畫面上的 ISB 元件顯示。
### **6) TCB_IMGPSScanX.GetNoNameCase**
- **功能描述**:處理「無案號」或「未知歸屬」的掃描件。
- **源碼實作逐行說明**:
  此方法透過迴圈尋找一個尚未被使用的目錄名稱(格式為「未配號 + 四位數字」)。
```pascal
Function TCB_IMGPSScanX.GetNoNameCase(Path:String):String; // 取未配號XXXX
var
  i : Integer; // 宣告迴圈計數器
begin
  // 1. 啟動一個從 1 到 9999 的迴圈,尋找可用的索引值
  for i := 1 to 9999 do
  begin
    // 2. 檢查特定路徑下的目錄是否已存在
    // _Msg('未配號'):取得「未配號」字串(可能具備多國語言處理)
    // Add_Zoo(i, 4):將數字 i 補齊為 4 位數(例如 1 變為 0001)
    if Not DirectoryExists(Path + _Msg('未配號') + Add_Zoo(i, 4)) then
    begin
      // 3. 若該目錄不存在,代表此名稱可用,設定 Result 並跳出迴圈
      Result := _Msg('未配號') + Add_Zoo(i, 4);
      Break; // 找到第一個可用的名稱後立即停止搜尋
    end;
  end;
end;
```
### **7) TCB_IMGPSScanX.SetSQLData 與 8) GetSQLData**
這兩個方法是 OCX 與網頁 (JS) 溝通的雙向橋樑。
- **SetSQLData (網頁 -> OCX)**:接收由網頁傳來的設定字串(通常用 | 或 , 分隔,例如 192.168.1.1|L12345678|200|C),在內部將字串切開 (Split),並分別賦值給 ServerIP, FCaseID, Fimgdpi, Fscancolor 等內部變數。
- **GetSQLData (OCX -> 網頁)**:將 OCX 內部的處理結果(例如:UploadSuccess|3|0 代表上傳成功、共 3 頁、0 錯誤),組合成一個長字串後回傳給網頁端的 JS 進行後續的頁面跳轉或提示。
### **8) TCB_IMGPSScanX.GetDefScanIni**
- **功能描述**:取得並套用掃瞄參數的預設值與自訂設定。
- **源碼實作解析**:
    - **硬編碼預設值 (Hardcoded Defaults)**:
      程式一開始會先寫死一組標準值,例如 Def_ScanDpi := 300 (預設 300 DPI)、Def_DeviceDeleteSize := 3072 (空白頁判定門檻為 3KB)。
    - **動態配置覆蓋 (Dynamic Overriding)**:
      透過 for i := 0 to WORK_INF_List.Count - 1 do 迴圈遍歷設定檔清單。使用 GetSQLData 取得參數代號 (PARA_NO) 與內容 (PARA_CONTENT)。
- **屬性設定對照表**:
  下表整理了此方法中設定的所有關鍵屬性、其對應的參數代號及功能說明。
  | **屬性名稱 (內部變數)** | **對應 PARA_NO** | **PARA_CONTENT 處理方式(GetSQLData)** | **功能說明** |
      | --- | --- | --- | --- |
  | **Def_DeviceDelete** | SCAN_BLANKDEL_USE | 'Y' -> True, Else -> False | 空白頁自動刪除開關。 |
  | **Def_DeviceDeleteSize** | SCAN_BLANKDEL_SIZE | StrToInt (若空則 0) | 空白頁判定門檻 (Bytes)。 |
  | **Def_ScannerReverse** | SCAN_REVERSE | 'Y' -> True, Else -> False | 影像是否反相。 |
  | **Def_BoardClear** | SCAN_BOARDCLEAR | 'Y' -> True, Else -> False | 是否清除影像黑邊。 |
  | **Def_ScanDpi** | SCAN_DPI | StrToInt (若空則 300) | 掃描解析度。 |
  | **Def_ScanDuplex** | SCAN_DUPLEX | 'Y' -> True, Else -> False | 是否啟用雙面掃描。 |
  | **Def_ScanRotate** | SCAN_ROTATE_MODE | '0'->0, '1'->270, '2'->180, '3'->90 | 掃描旋轉角度映射。 |
  | **Def_ScanDeskew** | SCAN_DESKEW | 'Y' -> True, Else -> False | 是否自動矯正傾斜。 |
  | **Def_ScanImgSetUse** | SCAN_IMGSET_USE | 'Y' -> True, Else -> False | 是否使用亮度/對比設定。 |
  | **Def_ScanBright** | SCAN_BRIGHT | StrToInt (限制 -255~255) | 亮度數值。 |
  | **Def_ScanContrast** | SCAN_CONTRAST | StrToInt (限制 -255~255) | 對比數值。 |
  | **Def_ScanImgShowMode** | SCAN_SHOW_MODE | '0','1','2' 對應模式 | 影像顯示/縮放模式。 |
  | **ScanDenialTime** | CASE_IN_TIME | 直接存為 String | 進件截止時間限制。 |
  | **ScanDenialHint** | SCAN_HINT | 直接存為 String | 掃描畫面提示字串。 |
  | **NoSaveBarCodeList** | NO_SAVE_FORM_ID | CommaText := Value | 不存檔之表單代號清單。 |
  | **ImagePath** | LOCAL_PATH | 直接存為 String | 本機端暫存路徑。 |
  | **GuideFormIDList** | GUIDEFORMID | CommaText := Value | 導引頁表單 ID 清單。 |
  | **DivPageFormIDList** | DIVPAGEFORMID | CommaText := Value | 分案頁表單 ID 清單。 |
  | **FJpgCompression** | FILE_COMPRESSION | StrToInt | JPG 轉 TIF 壓縮比。 |
  | **FMaxUploadSize** | MAX_UPLOAD_SIZE | 直接存為 String | 上傳大小限制 (MB)。 |
## **貳、 記憶體直接操作實作清單**
以下條列專案中直接涉及記憶體操作的程式區段:
### **記憶體操作對應位置表**
| **記憶體操作對象** | **所屬 Function / Procedure** | **關鍵操作關鍵字 / 代碼** | **記憶體特性說明** | 註記 |
| --- | --- | --- | --- | --- |
| **HInstance** | GetCurrentVersionNo | GetModuleFileName(HInstance, ...) | 模組載入位址。 | 增API版本號 |
| **資源緩衝區** | GetCurrentVersionNo | VerQueryValue, GetFileVersionInfo | 透過 Pointer 進行二進位搜尋。 | 增API版本號 |
| **DibHandle** | onAcquire | MyGraphic.Handle := DibHandle; | Windows 全域記憶體句柄轉移。 | 改套件實作/Rust |
| **TMemoryStream** | onAcquire | MemoryStream.Write, .Read | Heap Memory 內部二進位操作。 |  |
## **參、 遺失物件反推與說明**
你提到在原始碼中找不到 _DelTree 和 Scanner 的實作,這是因為它們屬於以下兩種情況:
### **1) TCB_IMGPSScanX._DelTree**
- **這是一個自訂的私有輔助函式 (Private Helper)**。
- **功能**:它的名稱源自 DOS 時代的指令 deltree(刪除整個目錄樹)。在 ActiveX 中,當上傳伺服器成功後,必須清空 C:\Temp\案件編號\ 裡的所有暫存圖檔與資料夾以釋放空間,並防止資料外洩。
- **實作原理**:由於早期的 Delphi 沒有內建遞迴刪除目錄的單一指令,開發者通常會自己寫一個 _DelTree,裡面使用 FindFirst, FindNext 找出所有檔案並用 DeleteFile 刪除,最後用 RemoveDir 砍掉資料夾。
### **2) Scanner**
- **這是一個視覺化/非視覺化的第三方元件實例 (Component Instance)**。
- **為何找不到宣告?** 因為在 Delphi 中,如果你是透過 IDE 的工具箱 (Tool Palette) 將 TEnScan 元件直接拖拉到窗體 (Form) 上,IDE 會自動把它寫在 .dfm 檔裡,並在 TCB_IMGPSScanX 類別的 published 區段自動產生一行 Scanner: TEnScan;。
- **功能**:它就是 **Envision SDK** 裡用來與 TWAIN 硬體溝通的靈魂核心。所有的掃描屬性 (Scanner.Resolution) 與方法 (Scanner.Acquire) 都是對這個物件進行操作。
---
# **IO 路徑與檔案操作分析**
本報告詳列了 CB_IMGPSScanImp.pas 檔案中定義的所有 IO 相關變數,以及執行目錄與檔案管理的關鍵方法,並定義了抽像化所需的介面原型。
## **壹、 關鍵路徑與檔案變數 (Variables)**
程式中定義了多個全域或私有欄位,用來追蹤檔案在硬碟上的實體位置。
### **1. 核心路徑變數與動態組合邏輯**
| **變數名稱** | **類型** | **組合邏輯 (Combination Logic)** | **用途說明** |
| --- | --- | --- | --- |
| **ImagePath** | String | WORK_INF 的 LOCAL_PATH 參數值 | **本機基礎根路徑**。所有暫存資料的起點。 |
| **ScaniniPath** | String | ImagePath + FWork_No + '\' + FUserUnit + '\' | **設定檔存放區**。區分作業別與單位。 |
| **ImageSavePath** | String | 同 ImagePath (初期) 或隨 FMode 變動 | **影像儲存基準路徑**。 |
| **ScanPath** | String | ImageSavePath + ScanCaseno + '\' + ScanDocdir + '\' | **當前掃描實體目錄**。指向最末層的影像資料夾。 |
| **TransPath** | String | ImageSavePath + CaseID + '\Upload\' | **準備上傳區**。在傳送前將檔案結構扁平化組合於此。 |
| **DisplayPath** | String | ImageSavePath + NowCaseNo + '\' | **預覽基準目錄**。用於 UI 點選樹狀圖時讀取特定案件資料。 |
| **LngPath** | String | GetLocalAppDir(Handle) + 'MPS\CB_IMGPS\' | **應用程式資源路徑**。存放在使用者設定檔目錄以避開權限問題。 |
| **CheckXmlPath** | String | ImagePath + 'OMRSITE\' | **OMR 規則暫存區**。存放從伺服器同步下來的 XML 定義。 |
| **SamplePath** | String | ImagePath + 'Sample\' + FWork_No + '\' | **範本影像存放區**。按作業別區分。 |
| **SitePath** | String | ImagePath + 'Site\' | **登打定位存放區**。 |
| **FFtpRootPath** | String | 透過伺服器回傳值動態設定 | **FTP 伺服器根路徑**。指定在遠端 FTP 伺育器上存放影像檔的起始位址。 |
### **2. 檔案名稱變數 (Filenames)**
| **變數名稱** | **類型** | **用途說明** |
| --- | --- | --- |
| **ScanSaveFilename** | String | 下一張掃描影像預定的檔名(通常包含 FormID)。 |
| **PEFileName** | String | 在 PageEnd 階段確定的最終完整檔案路徑。 |
| **AttName** | String | **附件目錄名稱**。根據 FIs_In_Wh 決定為 Attach 或 S_Attach。 |
| **Ext** | String | 副檔名。預設為 .tif,彩色/灰階模式下可能切換為 .jpg。 |
## **貳、 關鍵 IO 操作方法 (Methods)**
以下為需要進行抽像化設計的核心 IO 方法。
### **1. 目錄管理 (Directory Management)**
| **方法名稱** | **作用** | **應用場景** | **建議抽象 IO 介面** |
| --- | --- | --- | --- |
| _DelTree | 遞迴刪除整個目錄樹 | 上傳成功後清空暫存、模式切換初始化目錄 | RemoveDirRecursive(Path) |
| Str2Dir / ForceDirectories | 確保路徑以 \ 結尾並建立路徑 | 初始化或掃描前建立資料夾 | EnsureDirExists(Path) |
| GetNoNameCase | 搜尋尚未配號的臨時目錄 | 在本機搜尋如 未配號0001 到 9999 的可用路徑 | FindNextAvailablePath(Pattern) |
| GetLocalAppDir | 取得 Windows %LocalAppData% 路徑 | 確保在現代 Windows 系統中有權限寫入語言檔與 Log | GetBaseStoragePath() |
### **2. 存在性檢查與搜尋 (Existence & Search)**
| **方法名稱** | **作用** | **應用場景** | **建議抽象 IO 介面** |
| --- | --- | --- | --- |
| FileExists | 檢查指定路徑檔案是否存在 | 在載入 .dat、.ini 或影像前進行安全檢查 | IsFilePresent(Path) |
| DirectoryExists | 檢查指定路徑資料夾是否存在 | 在建立掃描目錄或上傳前確認結構完整性 | IsDirPresent(Path) |
| FindFirst / FindNext | 搜尋符合特定格式的檔案 | 取得實體檔案大小 (FileRec.Size) 或遞迴搜尋檔案 | ListFiles(Path, Pattern) / GetFileInfo(Path) |
### **3. 檔案生命週期與刪除 (File Lifecycle & Deletion)**
| **方法名稱** | **作用** | **應用場景** | **建議抽象 IO 介面** |
| --- | --- | --- | --- |
| ReSortFileName | 影像重新編序命名 | 刪除或插入影像後,修正 001_xxx 等序號連續性 | RenameFile(Old, New) |
| DeleteImageFile | 刪除實體影像檔 | 單張影像刪除並觸發 .dat 內容同步更新 | DeleteFile(Path) |
| DeleteDocNoFile | 刪除特定文件代號下所有影像 | 整批文件(DocNo)的物理刪除與序號重整 | DeleteFilesByPattern(Pattern) |
| DeleteShowFile | 刪除 UI 顯示中的影像清單 | 根據介面選取狀態進行批次物理刪除 | DeleteFileList(List<Path>) |
| RenameFile / MoveFile | 檔案搬移或更名 | 用於「分案」、「移動頁數」或「變更歸類」 | MoveFile(Src, Dest) |
| CopyFile | 複製實體檔案 | 影像搬移至上傳區或引用舊案件影像 | CopyFile(Src, Dest) |
### **4. 資料紀錄與序列化 (Metadata IO)**
| **方法名稱** | **作用** | **應用場景** | **建議抽象 IO 介面** |
| --- | --- | --- | --- |
| DeleteCustomDocDir | 移除自訂文件定義 | 從 CustomDocNo.ini 中移除特定自訂文件的節點 | Storage.IniDeleteSection(Section) |
### **5. 網路傳輸 IO 方法 (Network Retrieval - dnFile_Get)**
專案中使用 `dnFile_Get` 從伺服器端同步必要的環境檔案或規則定義。
| **抓取對象 (目標路徑)** | **遠端請求 Action** | **用途說明** |  |
| --- | --- | --- | --- |
| **CheckXmlPath + filename** | `GetCheckXml` | **下載 OMR 檢核定義檔**。同步伺服器端最新的 XML 座標規則,用於自動判斷影像區域。 | https |
| **SamplePath + filename** | `GetSampleImg` | **下載範本影像**。用於在 UI 介面上提供給操作員對照的標準範例圖檔。 | https |
| **SitePath + filename** | `GetSiteImg` | **下載定位定義圖**。下載與登打、校對位置相關的輔助影像。 | https |
| **LngPath + 'Language.ini'** | `GetLanguage` | **同步多國語言檔**。確保用戶端介面文字(繁體、英文、越南語等)與伺服器同步。 | https |
## **參、 資料紀錄與序列化 (Metadata IO)**
專案中使用了大量的 .dat 與 .ini 檔案來記錄目錄內的結構資訊與使用者偏好。
### **1. .dat 檔案 (結構紀錄)**
| **檔案名稱** | **組合路徑邏輯 (Path Logic)** | **內容與用途** |
| --- | --- | --- |
| **Context.dat** | ImageSavePath + CaseID + '\' + DocDir + '\Context.dat' | **影像清單**。紀錄該資料夾內所有影像檔名的正確順序。 |
| **DocDir.dat** | ImageSavePath + CaseID + '\Upload\DocDir.dat' | **目錄對應表**。紀錄 Upload 下每個檔名所屬的原始 DocDir |
| **CaseDocNo.dat** | ImageSavePath + CaseID + '\CaseDocNo.dat' | **文件目錄索引**。紀錄案件內已建立的實體資料夾名稱。 |
| **CaseDocNo_Copies.dat** | ImageSavePath + CaseID + '\CaseDocNo_Copies.dat' | **份數紀錄**。對應 CaseDocNo.dat 中的資料夾分別有多少份。 |
| **CaseIndex.dat** | ImageSavePath + CaseID + '\CaseIndex.dat' | **案件屬性**。如是否為「授信卷」(Case_loandoc)。 |
| **EditedDocDir.dat** | ImageSavePath + CaseID + '\EditedDocDir.dat' | **異動清單**。紀錄當次操作中被修改過的文件目錄。 |
| **Scan_Memo.dat** | DisplayPath + 'Scan_Memo.dat' | **使用者註記**。紀錄操作員手動輸入的備註內容。 |
| **CaseList.dat** | ImageSavePath + 'CaseList.dat' | **案件總表**。紀錄目前本機快取中存在的所有 CaseID。 |
### **2. .ini 檔案 (設定與偏好)**
| **檔案名稱** | **路徑邏輯** | **回寫 (Write-back)** | **內容與用途** | 寫入關聯 |
| --- | --- | --- | --- | --- |
| **Scan.ini** | ScaniniPath + 'Scan.ini' | **是 (頻繁)** | **掃描器硬體設定**。儲存最後一次選取的 DPI、雙面、色彩模式、亮度、對比等參數。 |  |
| **CustomDocNo.ini** | ScaniniPath + 'CustomDocNo.ini' | **是 (中等)** | **自訂文件代號**。儲存操作員定義的文件分類名稱與內部 ID 映射。 | DeleteCustomDocDir |
| **CB_IMGPSScan.ini** | LngPath + 'CB_IMGPSScan.ini' | **是 (低頻)** | **環境配置**。儲存伺服器 IP、Port、自動更新版本資訊等全域參數。 |  |
| **Language.ini** | LngPath + 'Language.ini' | **否 (唯讀同步)** | **多國語言包**。僅透過 `dnFile_Get` 從伺服器抓取更新,OCX 不自行修改。 |  |
## **肆、 串流與讀寫操作 (Stream & File I/O)**
源碼中頻繁使用 .LoadFromFile 與 .SaveToFile 來實現資料的持久化。
### **1. 使用 TStringList 讀寫 .dat 結構**
| **操作對象** | **讀取場景 (Load)** | **寫入場景 (Save)** | **建議抽象 IO 介面** |
| --- | --- | --- | --- |
| **結構清單 (.dat)** | LoadImgFile 載入以重建樹狀結構 | PageEnd 掃描完成或結構增刪後即時存檔 | ReadTextFile(Path) / WriteTextFile(Path, Content) |
| **備註文件 (Memo)** | 進入備註編輯器時載入舊有內容 | WNoteBtnClick 完成編輯後儲存內容 | ReadTextFile(Path) / WriteTextFile(Path, Content) |
| **上傳包定義** | N/A | CreateFormID_FormName 產生上傳包所需的定義檔 | WriteTextFile(Path, Content) |
|  |  |  |  |
### **2. 使用 TImageScrollBox / TDibGraphic 讀寫影像**
| **操作對象** | **讀取場景 (Load)** | **寫入場景 (Save)** | **建議抽象 IO 介面** |
| --- | --- | --- | --- |
| **掃描影像原始檔** | view_image_... 載入顯示於預覽窗 | OnAcquire 將記憶體影像 (Dib) 持久化至磁碟 | ReadBinaryFile(Path) / WriteBinaryFile(Path, Blob) |
| **影像處理覆蓋** | ImageReSize_FormID 讀取進行定位縮放 | 處理完成後覆蓋原始影像檔 | ReadBinaryFile(Path) / WriteBinaryFile(Path, Blob) |
| **旋轉/後製處理** | 點擊旋轉按鈕後從磁碟重新讀取 | 旋轉完成後保存更新內容回實體檔案 | ReadBinaryFile(Path) / WriteBinaryFile(Path, Blob) |
## **伍、 檔案路徑構造邏輯示例**
程式中典型的路徑構造方式如下(以 PageEnd 為例):
// 1. 基礎路徑
ScanPath := ImageSavePath + ScanCaseno + '\';
// 2. 進入特定文件目錄 (DocDir)
ScanPath := ScanPath + ScanDocdir + '\';
// 3. 確保實體目錄存在
Str2Dir(ScanPath);
// 4. 構造最終影像路徑
PEFileName := ScanPath + Add_Zoo(PageCount, 3) + '_' + FormID + ext;
## **陸、 IO 方法具體實作邏輯細部說明 (Detailed Implementation Logic)**
本節針對 CB_IMGPSScanImp.pas 內部如何透過 Windows API 實作這些 IO 操作進行細部解說。
### **1. Str2Dir(Path: string) 的運作機制**
這個方法的主要作用是 **「路徑標準化與實體資料夾建立」**。
- **字串處理**:它會先檢查傳入的 Path 字串最後一個字元是否為 \。如果不是,會自動補上。
- **建立資料夾**:**是的,它會在內部建立資料夾**。它會呼叫 Delphi 的 ForceDirectories(Path) 函式。
- **ForceDirectories 的特性**:與一般的 CreateDir 不同,ForceDirectories 會「遞迴」建立路徑中所有不存在的父目錄(例如若路徑為 C:\A\B\C,即使 A 和 B 都不存在,它也會一併建立)。
### **2. _DelTree(DirName: string) 的遞迴邏輯**
由於 Delphi 早期版本沒有提供單一函式來刪除包含檔案的資料夾,此方法透過 Windows 的搜尋機制實現:
- **遍歷搜尋**:使用 FindFirst 和 FindNext 找出資料夾內的所有物件。
- **檔案處理**:若搜尋到的是一般檔案,直接呼叫 DeleteFile。
- **遞迴處理**:若搜尋到的是子資料夾,則「自己呼叫自己」 (_DelTree) 先清空該子資料夾。
- **物理移除**:當資料夾內的所有內容都清空後,呼叫 RemoveDir 將空的資料夾從磁碟移除。
### **3. FindFirst 與 FindNext 的搜尋與疊代邏輯**
這兩個方法是 Delphi 處理「集合式 IO 操作」(如掃描目錄、批次刪除)的基礎,通常搭配 TSearchRec 結構使用。
- **FindFirst(Path, Attr, SearchRec)**:
- **動作**:啟動搜尋任務,將找到的第一個檔案資訊填入 SearchRec。
- **應用例**:在 PM104Click (匯入影像) 時,程式會先執行 FindFirst(FName, faAnyfile, FileRec) 來確認使用者選取的實體檔案是否存在,並藉由 FileRec.Size 取得位元組大小,用以判斷是否超過系統設定的匯入限制(例如 5MB)。
- **FindNext(SearchRec)**:
- **動作**:繼續尋找下一個符合條件的檔案。
- **疊代模式**:通常包裹在 repeat ... until FindNext(SR) <> 0 迴圈中。
- **應用例**:在 _DelTree 實作中,程式利用此疊代邏輯將資料夾內「所有的」子目錄與檔案清空,直到 FindNext 回傳非零值(代表搜尋結束)。
- **FindClose(SearchRec)**:
- **重要性**:釋放搜尋控制代碼(Handle)。若未呼叫,頻繁搜尋會導致系統資源洩漏。
### **4. GetNoNameCase(Path: string) 的搜尋邏輯**
這是一個典型的 **「可用名稱查表」** 實作:
- **迴圈範圍**:從 1 迭代到 9999。
- **格式組合**:使用 Add_Zoo 函式將數字轉為四位數(如 0001),並加上前綴字「未配號」。
- **磁碟檢查**:呼叫 DirectoryExists 檢查 Path + '未配號000x'。
- **結果回傳**:第一個回傳 False(代表該資料夾尚不存在)的名稱即為結果,程式會立即跳出迴圈並選定此名稱。
### **5. ReSortFileName(Path: string) 的連續性維護**
為了保證 UI 上顯示的頁碼順序與磁碟檔名一致,此方法採用「先暫存、後改名」的策略:
- **防止衝突**:先將目錄內所有符合格式的影像更名為帶有臨時前綴(如 @)的檔名。
- **按序更名**:讀取 Context.dat 內的清單,依照目前的順序,將檔案逐一更名為 001_..., 002_... 等格式。
- **同步索引**:最後將更名後的結果重新寫入 Context.dat。
### **6. GetLocalAppDir(Handle: HWND) 的權限繞過**
這是一個系統級別的 IO 位置定位方法:
- **API 呼叫**:呼叫 SHGetSpecialFolderPath (Windows Shell API)。
- **定位 CSIDL_LOCAL_APPDATA**:指向 C:\Users\使用者名稱\AppData\Local\。
- **必要性**:這能解決 OCX 程式被安裝在 Program Files 下時,因為 UAC 權限無法在安裝目錄寫入 Log 或語言檔的問題。
### **🎯 移植至 Web 平台的建議**
當您將這些邏輯移植到 Web 時:
- Str2Dir 應對應到 **IndexedDB** 的 Object Store 建立或 **File System Access API** 的 getDirectoryHandle(搭配 create: true)。
- FindFirst / FindNext 在 Web 端應對應到 Directory Handle 的 entries() 異步疊代器。
- .dat 序列化文件在 Web 端建議直接轉為 **JSON** 格式存儲於瀏覽器的 localStorage 或 IndexedDB 中。
## **柒、 設定檔回寫邏輯 (Write-back Logic)**
本節說明專案中哪些 .ini 檔案會由 OCX 主動更新資料及其觸發時機。
| **檔案名稱** | **回寫內容 (Content)** | **觸發時機 (Trigger)** |
| --- | --- | --- |
| **Scan.ini** | DPI、Duplex、亮度、對比、影像模式 | 掃描前設定變更或啟動掃描時(SaveScanPara)。 |
| **CustomDocNo.ini** | 自訂文件名稱與 ID 映射 | 操作員新增自訂分類或呼叫 `DeleteCustomDocDir` 時。 |
| **CB_IMGPSScan.ini** | 版本號、伺服器 IP/Port | 自動更新完成後更新版號,或手動修改伺服器設定時。 |
## **捌、 注意事項**
- **硬編碼路徑**:ActiveFormCreate 中存在寫死的 C:\Temp\IMGPSScan,適合作為最終保險。
- **權限問題**:作為 ActiveX,這些 IO 操作強烈依賴瀏覽器的「安全站台設定」或 IE 的「保護模式」。使用 GetLocalAppDir 是現代化重構中解決 UAC 權限問題的重要手段。
---
# **TCB_IMGPSScanX OCX 通訊介面與溝通機制分析**
本表條列了 Delphi 宿主環境(ActiveX[ocx]) 與 TCB_IMGPSScanX 元件之間主要的溝通管道、對應方法及其在系統架構中的作用。
## **壹、 指令與資料交換介面表**
| **通訊方法 (Method)** | **通訊方向** | **溝通內容 (Data Payload)** | **具體作用與目的** |
| --- | --- | --- | --- |
| **SetSQLData** | **宿主 -> OCX** | 以 ` | 或,` 分隔的長字串(如:IP, CaseID, DPI) |
| **GetSQLData** | **宿主 <-> OCX** | 指定 Key (如 'PARA_NO') 取得對應 Value | **資料查詢通道**。宿主可詢問 OCX 目前快取清單中的特定數值,用於介面同步或邏輯判斷。 |
| **GetSetInf1 ~ 7** | **宿主 <- OCX** | 單一字串值 (如 CaseID, 總張數, 成功標記 'Y') | **狀態出口 (Outlets)**。提供標準化的 7 個出口,讓 JavaScript 或宿主 App 輪詢(Polling)目前的執行結果或掃描統計。 |
| **StatrTwainScan** | **宿主 -> OCX** | 無參數 (觸發動作) | **硬體啟動指令**。宿主程式按下「開始掃描」按鈕後呼叫此方法,通知 OCX 喚起 TWAIN 驅動程式。 |
| **GetCurrentVersionNo** | **宿主 <- OCX** | 版本字串 (如 '1.0.3.5') | **版本識別**。宿主程式載入 OCX 後第一步通常會呼叫此處,用以比對伺服器版本並決定是否強制更新。 |
| **upFile** | **宿主 -> OCX** | 無或部分參數 (觸發網路 IO) | **輸出指令**。通知 OCX 將本機暫存區的檔案(如 ZIP 包)依照先前設定好的路徑與權限上傳至伺服器。 |
| **dnFile_Get** | **宿主 -> OCX** | 遠端 Action 關鍵字與本地檔名 | **同步指令**。通知 OCX 向伺服器抓取特定資源(如 XML 規則或語言檔)並存放到指定 IO 路徑。 |
| **ActiveFormCreate** | **系統 -> OCX** | 視窗建立事件 | **環境初始化**。當宿主程式實體化 OCX 時,自動觸發此處執行 GetDefScanIni,確保掃描環境就緒。 |
| **Property CaseID** | **宿主 <-> OCX** | WideString | **屬性直連**。透過 COM 屬性機制直接設定或讀取案件編號。 |
| **Property ScreenSnap** | **宿主 -> OCX** | Boolean | **功能切換**。控制 OCX 是否要在特定時機執行螢幕截圖稽核。 |
## **貳、 溝通機制細部說明**
### **1. 參數化配置 (Config-by-String)**
本專案大量使用 SetSQLData 這種「一條字串走天下」的設計,這在早期 ActiveX 開發中非常流行,優點是規避了 COM 介面頻繁修改 _TLB.pas 的麻煩。您在轉換 TypeScript UI 時,可以將其封裝為一個 JSON 物件,最後再組合成字串傳給這個方法。
### **2. 狀態回饋 (The Inf1~7 Pattern)**
由於 ActiveX 事件 (Events) 的回傳有時在網頁端不夠穩定,開發者選擇了 GetSetInf 系列。宿主程式通常會在執行完某個長耗時動作(如 upFile)後,連續呼叫 GetSetInf3 (成功標記) 與 GetSetInf6 (錯誤訊息) 來決定下一步 UI 流程。
### **3. 雙向同步 (Bidirectional Sync)**
- **同步設定**:宿主透過 GetDefScanIni 觸發,OCX 內部完成硬體設定。
- **同步資料**:宿主透過 dnFile_Get 觸發,OCX 完成本機與伺服器的檔案 IO 同步。
# **參、描服務抽象層實作**
採用 **「抽象介面層 (Abstraction Layer)」** 配合 **「適配器模式 (Adapter Pattern)」**,處理遺留系統(Legacy System)過渡到現代化架構
```tsx
/**
 * 掃描服務資料模型定義
 * 將原本 OCX 雜亂的字串溝通 (SetSQLData) 結構化
 */
export interface ScanConfig {
  serverIp: string;
  caseId: string;
  dpi: number;
  isColor: boolean;
  isDuplex: boolean;
  // ... 其他來自 GetDefScanIni 的參數
}
export interface ScanStatus {
  caseId: string;
  totalPageCount: number;
  isUploadSuccess: boolean;
  lastBarcode: string;
  errorMessage: string;
}
/**
 * 掃描服務抽象介面 (IScanService)
 * UI 層僅與此介面溝通,不直接操作底層實作
 */
export interface IScanService {
  initialize(config: ScanConfig): Promise<void>;
  startScan(): Promise<boolean>;
  uploadFiles(): Promise<boolean>;
  getStatus(): Promise<ScanStatus>;
  getVersion(): Promise<string>;
  downloadResource(action: string, fileName: string): Promise<void>;
}
/**
 * 實作 A: ActiveX 適配器 (Legacy Adapter)
 * 用於目前環境:UI (TS) -> Delphi Browser -> ActiveX (OCX)
 */
export class ActiveXScanAdapter implements IScanService {
  private ocx: any;
  constructor() {
    // 透過 window.external 或 Delphi Bridge 取得 OCX 實例
    this.ocx = (window as any).OCX_INSTANCE;
  }
  async initialize(config: ScanConfig): Promise<void> {
    // 將 JSON 轉回 OCX 慣用的管道分隔字串 (SetSQLData 邏輯)
    const sqlDataStr = `${config.serverIp}|${config.caseId}|${config.dpi}|${config.isColor ? 'C' : 'G'}`;
    this.ocx.SetSQLData(sqlDataStr);
  }
  async startScan(): Promise<boolean> {
    this.ocx.StatrTwainScan();
    return true; // 實際狀態需透過 GetSetInf 輪詢
  }
  async getStatus(): Promise<ScanStatus> {
    return {
      caseId: this.ocx.GetSetInf1(),
      totalPageCount: parseInt(this.ocx.GetSetInf2()),
      isUploadSuccess: this.ocx.GetSetInf3() === 'Y',
      lastBarcode: this.ocx.GetSetInf5(),
      errorMessage: this.ocx.GetSetInf6()
    };
  }
  async uploadFiles(): Promise<boolean> {
    this.ocx.upFile();
    return true;
  }
  async getVersion(): Promise<string> {
    return this.ocx.GetCurrentVersionNo();
  }
  async downloadResource(action: string, fileName: string): Promise<void> {
    this.ocx.dnFile_Get(action, fileName);
  }
}
/**
 * 實作 B: Backend Server 適配器 (Future Adapter)
 * 用於未來環境:UI (TS) -> REST API / WebSocket -> Rust/Go Backend
 */
export class BackendServerAdapter implements IScanService {
  private apiUrl = "http://localhost:8080/api/scanner";
  async initialize(config: ScanConfig): Promise<void> {
    await fetch(`${this.apiUrl}/config`, {
      method: 'POST',
      body: JSON.stringify(config)
    });
  }
  async startScan(): Promise<boolean> {
    const res = await fetch(`${this.apiUrl}/scan`, { method: 'POST' });
    return res.ok;
  }
  async getStatus(): Promise<ScanStatus> {
    const res = await fetch(`${this.apiUrl}/status`);
    return await res.json();
  }
  async uploadFiles(): Promise<boolean> {
    const res = await fetch(`${this.apiUrl}/upload`, { method: 'POST' });
    return res.ok;
  }
  async getVersion(): Promise<string> {
    const res = await fetch(`${this.apiUrl}/version`);
    const data = await res.json();
    return data.version;
  }
  async downloadResource(action: string, fileName: string): Promise<void> {
    // 未來可能直接由後端處理,前端僅發送同步指令
    await fetch(`${this.apiUrl}/sync?action=${action}&file=${fileName}`);
  }
}
/**
 * 服務工廠 (Scanner Factory)
 * 根據環境自動切換實作
 */
export class ScannerFactory {
  static getService(): IScanService {
    if ((window as any).OCX_INSTANCE) {
      console.log("偵測到 ActiveX 環境,啟用 ActiveX 適配器");
      return new ActiveXScanAdapter();
    } else {
      console.log("現代瀏覽器環境,啟用後端 API 適配器");
      return new BackendServerAdapter();
    }
  }
}
```
### **1. 介面標準化(JSON 化)**
原先 OCX 使用 SetSQLData 的「長字串」溝通是非常脆弱的(一項參數順序錯了就全毀)。在 TypeScript 抽象層中,請務必將其轉換為 **強型別的 JSON 物件**(如 ScanConfig 介面)。這樣你的 UI 邏輯在處理設定時會非常直覺。
### **2. 非同步處理(Promise/Async)**
ActiveX 的方法通常是同步阻塞的,而後端 API 是非同步的。
- **建議**:在抽象介面(IScanService)中,所有方法都回傳 Promise。
- **原因**:這樣當你未來切換到後端伺服器(fetch API)時,UI 的調用邏輯(await service.startScan())不需要做任何修改。
### **3. 實作「心跳」或「狀態輪詢」**
原本 GetSetInf1~7 主要是靠宿主程式主動去「問」OCX。
- **抽象層建議**:在 IScanService 中實作一個 subscribeStatus(callback) 的觀察者模式。
- **ActiveX 模式下**:在適配器內部啟動一個 setInterval 去輪詢 GetSetInf。
- **未來後端模式下**:適配器改用 **WebSocket** 接收後端主動推播的掃描事件。
- **UI 層感受**:UI 只需要處理 onStatusUpdate 事件,根本不用管底層是用輪詢還是 WebSocket。
### **4. 資源路徑的轉義(The Bridge Proxy)**
這是我之前提到的關鍵:由於 iframe 裡的 TypeScript 無法存取 C:\Temp,但 ActiveX 依舊會把檔案掃到那裡。
- **策略**:你的 Delphi Browser 需要實作一個簡單的本地 Proxy。
- **實作**:當 TypeScript 需要顯示圖檔時,它請求 http://local.bridge/view/001.tif,Delphi Browser 攔截此請求並從實體路徑讀取檔案回傳。
- **好處**:未來改為 Backend Server 時,Server 也可以提供同樣的 URL 格式,UI 完全無縫接軌。
## **肆、 Delphi 與 JavaScript 通訊橋樑 (Bridge) 實作說明**
當您在 Delphi 宿主程式中使用內嵌瀏覽器時,JavaScript 取得 `OCX_INSTANCE` 的方式取決於底層的瀏覽器引擎:
### **1. 舊版 IE 核心 (TWebBrowser / window.external)**
這是最傳統的方式,Delphi 會透過實作 `IDocHostUIHandler` 介面,將一個自訂的 `IDispatch` 物件掛載到 `window.external` 上。
- **Delphi 端**:將 `TCB_IMGPSScanX` 實例或其包裝類別指派給瀏覽器的 `External` 屬性。
- **JS 端**:直接使用 `window.external.StatrTwainScan()` 呼叫。
### **2. 新版 Edge 核心 (TEdgeBrowser / WebView2 Host Objects)**
這是現代化的做法。Delphi 透過 WebView2 的 API 將物件直接「注入」到 JS 的命名空間中。
- **Delphi 端執行**:
    ```pascal
    // 將 OCX 物件以 'OCX_INSTANCE' 名稱注入
    EdgeBrowser.AddHostObjectToScript('OCX_INSTANCE', MyOcxWrapperObject);
    ```
- **JS 端取得**: WebView2 會將物件放置在 `window.chrome.webview.hostObjects` 底下。
    ```jsx
    const ocx = window.chrome.webview.hostObjects.OCX_INSTANCE;
    ```
### **3. 為什麼需要 Wrapper (包裝類別)?**
通常我們**不建議**直接將 `TCB_IMGPSScanX` 注入,原因如下:
1. **安全性**:直接暴露 COM 元件可能會有安全漏洞。
2. **相容性**:OCX 的某些方法回傳型別(如 Delphi 的自訂 Enum)JavaScript 無法識別。
3. **執行緒優化**:Delphi 可以在 Wrapper 中使用 `TThread.Queue` 確保掃描指令在正確的執行緒執行,避免瀏覽器 UI 凍結。
### **4. 溝通路徑總結**
1. **Delphi 宿主程式** 啟動並實體化 **OCX 控制項**。
2. **Delphi 宿主程式** 建立 **Browser 物件** 並載入 **TypeScript UI (HTML)**。
3. **Delphi 宿主程式** 呼叫瀏覽器 API(如 `AddHostObjectToScript`),將 OCX 的操作介面「注入」到該網頁的 `window` 物件中。
4. **TypeScript** 透過抽象層 (Adapter) 取得該物件,實現「網頁點按鈕,掃描機開動」的效果。
---
# Transport 相關套件分析
---
# Image Processor 相關套件分析
separate/scanImp/CB_IMGPSScanImp_Data.pas
比對新檔案
檔案太大
separate/scanImp/CB_IMGPSScanImp_Data.ts
比對新檔案
@@ -0,0 +1,819 @@
import * as fs from 'fs';
import * as path from 'path';
export function applyDataMixins(cls: any) {
    cls.prototype.Get_HelpFile = function(): string {
        let Result = String(this.HelpFile);
        return Result;
    };
    cls.prototype.Set_HelpFile = function(Value: string): void {
        this.HelpFile = String(Value);
    };
    cls.prototype.DocNoIsExistImg = function(DocNopath: string): boolean {
        let i: number;
        let ST: any;
        let Result = false;
        ST = new this.TStringList();
        if (fs.existsSync(DocNopath + '\\Context.dat')) {   /////20190319 Hong 當有空的Docno目錄時會掛掉,增加這行
            ST.LoadFromFile(DocNopath + '\\Context.dat');
        }
        for (i = 0; i < ST.Count; i++) {
            if (this.ISExistImg(DocNopath + ST.Strings[i])) {
                Result = false;
                return Result; // Exit;
                // Break;
            }
        }
        Result = true;
        return Result;
    };
    cls.prototype.HTTPSClientCertificateValidate = function(Sender: any, X509Certificate: any, Validate: { val: boolean }): void {
        Validate.val = true;
    };
    cls.prototype.HTTPSClientRedirection = function(Sender: any, OldURL: string, NewURL: { val: string }, AllowRedirection: { val: boolean }): void {
        AllowRedirection.val = true;
    };
    cls.prototype.GetServerDate = function(): boolean {
        let Result = false;     // http://192.168.0.101:8080/fbnp/servlet/CWC01?act=getservertime
        if (!this.ProcessServlet_Get(this.HTTPSClient, this.FUrl + 'service/imgpsc/IMGPSC01/servertimeforocx', '', this.FReWrite, this.Memo1, false)) {
            this.HttpErrStr = this._Msg('錯誤代碼:') + this.HttpError.HttpErrorCode.toString() + ',' + this.HttpError.HttpReason;
            Result = false;
            return Result; // Exit;
        }
        if (this.Memo1.Lines.Strings[0] === '1') {
            this.HttpErrStr = this._Msg('錯誤原因:') + this.Memo1.Lines.Strings[1];
            Result = false;
            return Result; // Exit;
        }
        else if (this.Memo1.Lines.Strings[0] === '0') {
            this.ServerDate = this.Memo1.Lines.Strings[1];
            this.ServerTime = this.ServerDate.substring(8, 14); // Copy(ServerDate,9,6)
            this.ServerDate = this.ServerDate.substring(0, 8); // Copy(ServerDate,1,8)
            this.Balance = this.GetBalance(this.ServerTime); //Server 跟Local的時間差
            Result = true;
        }
        else if (this.Memo1.Lines.Text.indexOf('<script type="text/javascript" src="scripts/CW00/login.js"></script>') >= 0) {
            this.HttpErrStr = this._Msg('錯誤原因:') + this._Msg('閒置過久或被登出,請重新登入');
            Result = false;
            return Result; // Exit;
        }
        return Result;
    };
    cls.prototype.GetSetInf1 = function(): boolean { //取系統設定資訊Mode1 DOC_INF
        let ColumeStr: string;
        let S: any;
        let EnCodeDateTime: string;
        let Result = false;     // http://192.168.0.101:8080/fbnp/servlet/CWC01?act=getservertime
        EnCodeDateTime = this.En_DecryptionStr_Base64('E', this.ServerDate + this.GetBalance2Time(this.Balance), this.Mpskey);
        ColumeStr = 'WORK_NO,DOC_NO,DOC_U_DESC,DOC_TYPE,DOC_VERSION,FORM_PAGES,START_DATE,STOP_DATE,IS_DOC_DIV,IS_IN_WH';
        //If not ProcessServlet(HTTPSClient,FURL+'servlet/CWC02 ','checktime='+EnCodeDateTime+'&mode=1&col='+Doc_Inf_Colume+'&workno='+FWork_No,FReWrite.Text,Memo1) Then
        if (!this.ProcessServlet_Get(this.HTTPSClient, this.FUrl + 'service/imgpsc/IMGPSC01/tables', 'checktime=' + EnCodeDateTime + '&mode=1&col=' + this.En_DecryptionStr_Base64('E', ColumeStr, this.Mpskey) + '&work_no=' + this.FWork_no, this.FReWrite, this.Memo1, false)) {
            this.HttpErrStr = this._Msg('錯誤代碼:') + this.HttpError.HttpErrorCode.toString() + ',' + this.HttpError.HttpReason;
            Result = false;
            return Result; // Exit;
        }
        if (this.Memo1.Lines.Strings[0] === '1') {
            this.HttpErrStr = this._Msg('錯誤原因:') + this.Memo1.Lines.Strings[1];
            Result = false;
            return Result; // Exit;
        }
        else if (this.Memo1.Lines.Strings[0] === '0') {
            S = new this.TStringList();
            S.Text = this.Memo1.Lines.Text;
            this.SetSQLData(ColumeStr, S, this.Doc_Inf_List);
            S.Free();
            Result = true;
        }
        else if (this.Memo1.Lines.Text.indexOf('<script type="text/javascript" src="scripts/CW00/login.js"></script>') >= 0) {
            this.HttpErrStr = this._Msg('錯誤原因:') + this._Msg('閒置過久或被登出,請重新登入');
            Result = false;
            return Result; // Exit;
        }
        return Result;
    };
    cls.prototype.GetSetInf2 = function(): boolean { //取系統設定資訊Mode2  DM_FORM_INF
        let ColumeStr: string;
        let S: any;
        let EnCodeDateTime: string;
        let Result = false;     // http://192.168.0.101:8080/fbnp/servlet/CWC01?act=getservertime
        EnCodeDateTime = this.En_DecryptionStr_Base64('E', this.ServerDate + this.GetBalance2Time(this.Balance), this.Mpskey);
        ColumeStr = 'WORK_NO,MAIN_FORM_ID,DOC_VERSION,DEPE_FORM_ID,MUTEX_FORM_ID';
        //If not ProcessServlet(HTTPSClient,FURL+'servlet/CWC02 ','checktime='+EnCodeDateTime+'&mode=2&col='+ColumeStr+'&workno='+FWork_No,FReWrite.Text,Memo1) Then
        if (!this.ProcessServlet_Get(this.HTTPSClient, this.FUrl + 'service/imgpsc/IMGPSC01/tables', 'checktime=' + EnCodeDateTime + '&mode=2&col=' + this.En_DecryptionStr_Base64('E', ColumeStr, this.Mpskey) + '&work_no=' + this.FWork_no, this.FReWrite, this.Memo1, false)) {
            this.HttpErrStr = this._Msg('錯誤代碼:') + this.HttpError.HttpErrorCode.toString() + ',' + this.HttpError.HttpReason;
            Result = false;
            return Result; // Exit;
        }
        if (this.Memo1.Lines.Strings[0] === '1') {
            this.HttpErrStr = this._Msg('錯誤原因:') + this.Memo1.Lines.Strings[1];
            Result = false;
            return Result; // Exit;
        }
        else if (this.Memo1.Lines.Strings[0] === '0') {
            S = new this.TStringList();
            S.Text = this.Memo1.Lines.Text;
            this.SetSQLData(ColumeStr, S, this.DM_FORM_INF_List);
            S.Free();
            Result = true;
        }
        else if (this.Memo1.Lines.Text.indexOf('<script type="text/javascript" src="scripts/CW00/login.js"></script>') >= 0) {
            this.HttpErrStr = this._Msg('錯誤原因:') + this._Msg('閒置過久或被登出,請重新登入');
            Result = false;
            return Result; // Exit;
        }
        return Result;
    };
    cls.prototype.GetSetInf3 = function(): boolean { //取系統設定資訊mode3  FORM_INF
        let ColumeStr: string;
        let S: any;
        let EnCodeDateTime: string;
        let Result = false;     // http://192.168.0.101:8080/fbnp/servlet/CWC01?act=getservertime
        EnCodeDateTime = this.En_DecryptionStr_Base64('E', this.ServerDate + this.GetBalance2Time(this.Balance), this.Mpskey);
        //ColumeStr := 'T1.WORK_NO,T1.FORM_ID,T1.DOC_KIND,T1.DOC_NO,T1.DOC_VERSION,T1.FORM_NAME,T1.FORM_DESC,T1.DIVISION,T1.ANCHOR,T1.MAX_PAGE,T1.FORM_HEIGHT,T1.FORM_WIDTH,T1.MERGE_IMAGE,T1.CC_FORM_ID,T1.CC_MERGE_FORMID,T2.DOC_TYPE'; {T1.CC_FORM_ID,T1.CC_MERGE_FORMID,}
        ColumeStr = 'T1.WORK_NO,T1.FORM_ID,T1.DOC_NO,T1.DOC_VERSION,T1.FORM_NAME,T1.FORM_DESC,T1.DIVISION,T1.ANCHOR,T1.MAX_PAGE,T1.FORM_HEIGHT,T1.FORM_WIDTH,T1.IS_PRINT,T2.DOC_TYPE'; //{T1.CC_FORM_ID,T1.CC_MERGE_FORMID,}
        //If not ProcessServlet(HTTPSClient,FURL+'servlet/CWC02 ','checktime='+EnCodeDateTime+'&mode=3&col='+ColumeStr+'&workno='+FWork_No,FReWrite.Text,Memo1) Then
        if (!this.ProcessServlet_Get(this.HTTPSClient, this.FUrl + 'service/imgpsc/IMGPSC01/tables', 'checktime=' + EnCodeDateTime + '&mode=3&col=' + this.En_DecryptionStr_Base64('E', ColumeStr, this.Mpskey) + '&work_no=' + this.FWork_no, this.FReWrite, this.Memo1, false)) {
            this.HttpErrStr = this._Msg('錯誤代碼:') + this.HttpError.HttpErrorCode.toString() + ',' + this.HttpError.HttpReason;
            Result = false;
            return Result; // Exit;
        }
        if (this.Memo1.Lines.Strings[0] === '1') {
            this.HttpErrStr = this._Msg('錯誤原因:') + this.Memo1.Lines.Strings[1];
            Result = false;
            return Result; // Exit;
        }
        else if (this.Memo1.Lines.Strings[0] === '0') {
            S = new this.TStringList();
            S.Text = this.Memo1.Lines.Text;
            this.SetSQLData(ColumeStr, S, this.FORM_INF_List);
            S.Free();
            Result = true;
        }
        else if (this.Memo1.Lines.Text.indexOf('<script type="text/javascript" src="scripts/CW00/login.js"></script>') >= 0) {
            this.HttpErrStr = this._Msg('錯誤原因:') + this._Msg('閒置過久或被登出,請重新登入');
            Result = false;
            return Result; // Exit;
        }
        return Result;
    };
    cls.prototype.GetSetInf4 = function(): boolean { //取系統設定資訊mode4  CHECK_RULE_INF
        let ColumeStr: string;
        let S: any;
        let EnCodeDateTime: string;
        let Result = false;     // http://192.168.0.101:8080/fbnp/servlet/CWC01?act=getservertime
        EnCodeDateTime = this.En_DecryptionStr_Base64('E', this.ServerDate + this.GetBalance2Time(this.Balance), this.Mpskey);
        ColumeStr = 'WORK_NO,CHECK_NO,CHECK_RULE_DESC,MESG_SHOW_TYPE,MESG_DISP_TYPE,CHECK_MESG,SCAN_MODE';
        //If not ProcessServlet(HTTPSClient,FURL+'servlet/CWC02 ','checktime='+EnCodeDateTime+'&mode=4&col='+ColumeStr+'&workno='+FWork_No,FReWrite.Text,Memo1) Then
        if (!this.ProcessServlet_Get(this.HTTPSClient, this.FUrl + 'service/imgpsc/IMGPSC01/tables', 'checktime=' + EnCodeDateTime + '&mode=4&col=' + this.En_DecryptionStr_Base64('E', ColumeStr, this.Mpskey) + '&work_no=' + this.FWork_no, this.FReWrite, this.Memo1, false)) {
            this.HttpErrStr = this._Msg('錯誤代碼:') + this.HttpError.HttpErrorCode.toString() + ',' + this.HttpError.HttpReason;
            Result = false;
            return Result; // Exit;
        }
        if (this.Memo1.Lines.Strings[0] === '1') {
            this.HttpErrStr = this._Msg('錯誤原因:') + this.Memo1.Lines.Strings[1];
            Result = false;
            return Result; // Exit;
        }
        else if (this.Memo1.Lines.Strings[0] === '0') {
            S = new this.TStringList();
            S.Text = this.Memo1.Lines.Text;
            this.SetSQLData(ColumeStr, S, this.CHECK_RULE_INF_List);
            S.Free();
            Result = true;
        }
        else if (this.Memo1.Lines.Text.indexOf('<script type="text/javascript" src="scripts/CW00/login.js"></script>') >= 0) {
            this.HttpErrStr = this._Msg('錯誤原因:') + this._Msg('閒置過久或被登出,請重新登入');
            Result = false;
            return Result; // Exit;
        }
        this.CheckRule2OMRErrInfo();
        return Result;
    };
    cls.prototype.GetSetInf5 = function(): boolean { //取系統設定資訊mode5  MEMO_INF
        let ColumeStr: string;
        let S: any;
        let EnCodeDateTime: string;
        let Result = false;     // http://192.168.0.101:8080/fbnp/servlet/CWC01?act=getservertime
        EnCodeDateTime = this.En_DecryptionStr_Base64('E', this.ServerDate + this.GetBalance2Time(this.Balance), this.Mpskey);
        ColumeStr = 'T1.WORK_NO,T1.MEMO_TYPE,T1.MEMO_CONTENT,T2.MEMO_TYPE_NAME';
        //If not ProcessServlet(HTTPSClient,FURL+'servlet/CWC02 ','checktime='+EnCodeDateTime+'&mode=5&col='+ColumeStr+'&workno='+FWork_No,FReWrite.Text,Memo1) Then
        if (!this.ProcessServlet_Get(this.HTTPSClient, this.FUrl + 'service/imgpsc/IMGPSC01/tables', 'checktime=' + EnCodeDateTime + '&mode=5&col=' + this.En_DecryptionStr_Base64('E', ColumeStr, this.Mpskey) + '&work_no=' + this.FWork_no, this.FReWrite, this.Memo1, false)) {
            this.HttpErrStr = this._Msg('錯誤代碼:') + this.HttpError.HttpErrorCode.toString() + ',' + this.HttpError.HttpReason;
            Result = false;
            return Result; // Exit;
        }
        if (this.Memo1.Lines.Strings[0] === '1') {
            this.HttpErrStr = this._Msg('錯誤原因:') + this.Memo1.Lines.Strings[1];
            Result = false;
            return Result; // Exit;
        }
        else if (this.Memo1.Lines.Strings[0] === '0') {
            S = new this.TStringList();
            S.Text = this.Memo1.Lines.Text;
            this.SetSQLData(ColumeStr, S, this.MEMO_INF_List);
            S.Free();
            Result = true;
        }
        else if (this.Memo1.Lines.Text.indexOf('<script type="text/javascript" src="scripts/CW00/login.js"></script>') >= 0) {
            this.HttpErrStr = this._Msg('錯誤原因:') + this._Msg('閒置過久或被登出,請重新登入');
            Result = false;
            return Result; // Exit;
        }
        return Result;
    };
    cls.prototype.GetSetInf6 = function(): boolean { //取系統設定資訊mode5  WORK_INF
        let ColumeStr: string;
        let S: any;
        let EnCodeDateTime: string;
        let Result = false;     // http://192.168.0.101:8080/fbnp/servlet/CWC01?act=getservertime
        EnCodeDateTime = this.En_DecryptionStr_Base64('E', this.ServerDate + this.GetBalance2Time(this.Balance), this.Mpskey);
        ColumeStr = 'WORK_NO,PARA_NO,PARA_CONTENT';
        //If not ProcessServlet(HTTPSClient,FURL+'servlet/CWC02 ','checktime='+EnCodeDateTime+'&mode=5&col='+ColumeStr+'&workno='+FWork_No,FReWrite.Text,Memo1) Then
        if (!this.ProcessServlet_Get(this.HTTPSClient, this.FUrl + 'service/imgpsc/IMGPSC01/tables', 'checktime=' + EnCodeDateTime + '&mode=6&col=' + this.En_DecryptionStr_Base64('E', ColumeStr, this.Mpskey) + '&work_no=' + this.FWork_no, this.FReWrite, this.Memo1, false)) {
            this.HttpErrStr = this._Msg('錯誤代碼:') + this.HttpError.HttpErrorCode.toString() + ',' + this.HttpError.HttpReason;
            Result = false;
            return Result; // Exit;
        }
        if (this.Memo1.Lines.Strings[0] === '1') {
            this.HttpErrStr = this._Msg('錯誤原因:') + this.Memo1.Lines.Strings[1];
            Result = false;
            return Result; // Exit;
        }
        else if (this.Memo1.Lines.Strings[0] === '0') {
            S = new this.TStringList();
            S.Text = this.Memo1.Lines.Text;
            this.SetSQLData(ColumeStr, S, this.WORK_INF_List);
            S.Free();
            Result = true;
        }
        else if (this.Memo1.Lines.Text.indexOf('<script type="text/javascript" src="scripts/CW00/login.js"></script>') >= 0) {
            this.HttpErrStr = this._Msg('錯誤原因:') + this._Msg('閒置過久或被登出,請重新登入');
            Result = false;
            return Result; // Exit;
        }
        return Result;
    };
    cls.prototype.GetSetInf7 = function(): boolean { //取系統設定資訊mode5  LASTEST_FORM_INF
        let ColumeStr: string;
        let S: any;
        let EnCodeDateTime: string;
        //SELECT FORM_ID,DOC_NO,DOC_VERSION FROM FORM_INF WHERE (DOC_NO,DOC_VERSION) in (SELECT DOC_NO, MAX(DOC_VERSION) FROM FORM_INF GROUP BY DOC_NO) ORDER BY DOC_NO
        let Result = false;     // http://192.168.0.101:8080/fbnp/servlet/CWC01?act=getservertime
        EnCodeDateTime = this.En_DecryptionStr_Base64('E', this.ServerDate + this.GetBalance2Time(this.Balance), this.Mpskey);
        ColumeStr = 'FORM_ID,DOC_NO';
        //If not ProcessServlet(HTTPSClient,FURL+'servlet/CWC02 ','checktime='+EnCodeDateTime+'&mode=5&col='+ColumeStr+'&workno='+FWork_No,FReWrite.Text,Memo1) Then
        if (!this.ProcessServlet_Get(this.HTTPSClient, this.FUrl + 'service/imgpsc/IMGPSC01/tables', 'checktime=' + EnCodeDateTime + '&mode=7&col=' + this.En_DecryptionStr_Base64('E', ColumeStr, this.Mpskey) + '&work_no=' + this.FWork_no, this.FReWrite, this.Memo1, false)) {
            this.HttpErrStr = this._Msg('錯誤代碼:') + this.HttpError.HttpErrorCode.toString() + ',' + this.HttpError.HttpReason;
            Result = false;
            return Result; // Exit;
        }
        if (this.Memo1.Lines.Strings[0] === '1') {
            this.HttpErrStr = this._Msg('錯誤原因:') + this.Memo1.Lines.Strings[1];
            Result = false;
            return Result; // Exit;
        }
        else if (this.Memo1.Lines.Strings[0] === '0') {
            S = new this.TStringList();
            S.Text = this.Memo1.Lines.Text;
            this.SetSQLData(ColumeStr, S, this.LASTEST_FORM_INF_List);
            S.Free();
            Result = true;
        }
        else if (this.Memo1.Lines.Text.indexOf('<script type="text/javascript" src="scripts/CW00/login.js"></script>') >= 0) {
            this.HttpErrStr = this._Msg('錯誤原因:') + this._Msg('閒置過久或被登出,請重新登入');
            Result = false;
            return Result; // Exit;
        }
        return Result;
    };
    cls.prototype.SetIn_WH_DocNo = function(): void { //將要入庫的DocNo抽出來另存入list裡
        let i: number;
        for (i = 1; i < this.Doc_Inf_List.Count; i++) {
            if (this.GetSQLData(this.Doc_Inf_List, 'IS_IN_WH', i) === 'Y') {
                this.IN_WH_DocNoList.Add(this.GetSQLData(this.Doc_Inf_List, 'DOC_NO', i));
            }
        }
        // {Showmessage(IN_WH_DocNoList.Text);
        // StringtoFile(IN_WH_DocNoList.Text,'D:\121.txt');}
    };
    cls.prototype.DeleteDocNoFile = function(Path: string, DocNo: string): boolean {  //刪除指定DocNo文件
        let i: number;
        let FName: string;
        let Result = false;
        for (i = this.ContextList.Count - 1; i >= 0; i--) {
            FName = this.ContextList.Strings[i];
            if (DocNo === this.FormCode2DocNo(this.FileName2FormCode(FName))) {
                if (fs.existsSync(Path + FName)) fs.unlinkSync(Path + FName); // DeleteFile(Path+FName);
                this.ContextList.Delete(i);
                this.Context_DocnoList.Delete(i);
                Result = true; //有刪到指定文件
            }
        }
        this.ContextList.SaveToFile(Path + 'Context.dat');
        this.Context_DocnoList.SaveToFile(Path + 'Context_DocNo.dat');
        this.ReSortFileName(Path);
        this.ContextList.LoadFromFile(Path + 'Context.dat');
        this.Context_DocnoList.LoadFromFile(Path + 'Context_DocNo.dat');
        if (fs.existsSync(Path + 'CustomDocNo.dat')) {
            this.Cust_DocNoList.LoadFromFile(Path + 'CustomDocNo.dat');
        }
        return Result;
    };
    cls.prototype.DeleteShowFile = function(Path: string): void { //刪除顯示中的影像
        let i: number;
        let DelFile: string;
        for (i = 0; i < this.NowShowFileList.Count; i++) {
            DelFile = this.NowShowFileList.Strings[i];
            if (fs.existsSync(Path + DelFile)) fs.unlinkSync(Path + DelFile); // DeleteFile(Path+DelFile);
            this.SetContextList('D', -1, this.NowCaseno, this.NowDocNo, DelFile);
        }
    };
    cls.prototype.GetDataDocNoPage = function(MainDocNo: string, MainVersion: string): number {  //取記錄的文件_版本頁數
        let P: string;
        let Result = 0;
        if (this.FindSQLData(this.Doc_Inf_List, 'FORM_PAGES', 'DOC_NO,DOC_VERSION', MainDocNo + ',' + MainVersion, 0, this.FindResult)) {
            P = this.GetFindResult('FORM_PAGES');
            if (P !== '') {
                Result = parseInt(P, 10);
            } else {
                Result = 0;
            }
        }
        return Result;
    };
    cls.prototype.CheckCaseDocNoPage = function(CaseID: string, DocNo: string, Version: string, Pages: number): number { //取案件裡的文件_版本頁數
        let i: number, n: number, Count: number;
        let S: any, S2: any;
        let FormCode: string, iPage: string;
        let docInt = 0, tempInt = 0;
        let v: number, v2: number;
        Count = 0;
        docInt = 0;
        tempInt = 0;
        S = new this.TStringList();
        S2 = new this.TStringList();
        //ShowMessage('page='+IntToStr(Pages));
        try {
            S.LoadFromFile(this.ImageSavePath + CaseID + '\\upload\\Context.dat');
            S2.LoadFromFile(this.ImageSavePath + CaseID + '\\upload\\DocDir.dat'); //2017 1220 改成只承認第一份的
            for (i = 1; i <= Pages; i++) { //從0到pages-1  改成 1到pages 20170316 這樣可以修改檢核的頁數問題
                iPage = this.Add_Zoo(i, 2);
                for (n = 0; n < S.Count; n++) {
                    if (this.FWH_category === 'N' && this.FIs_In_Wh === 'Y') {
                        if (this.ISExistImg(this.ImageSavePath + CaseID + '\\upload\\' + S.Strings[n])) {
                            continue;
                        }
                    }
                    if (S2.Strings[n].length > 8 && S2.Strings[n].indexOf(DocNo) !== -1) { //2017 1220 改成只承認第一份的
                        //LogFile1.LogToFile(logTimeString+'有進'+docno+','+S2.Strings[n]+IntToStr(Pos(DocNo,S2.Strings[n])));
                        v = S2.Strings[n].indexOf('(');
                        v2 = S2.Strings[n].indexOf(')');
                        tempInt = parseInt(S2.Strings[n].substring(v + 1, v2), 10);
                        if (docInt === 0) {
                            docInt = tempInt;
                        }
                        if (docInt !== tempInt) {
                            //LogFile1.LogToFile(logTimeString+'docInt='+IntToStr(docInt)+',tempInt='+IntToStr(tempInt));
                            return Count; // Break returning Count
                        }
                    }
                    FormCode = this.FileName2FormCode(S.Strings[n]);
                    //LogFile1.LogToFile(logTimeString+'S.Strings[n]='+S.Strings[n]);
                    //Showmessage('1:'+version+','+FormCode2Version(FormCode)+','+DocNo+','+FormCode2DocNo(FormCode)+','+iPage+','+FormCode2Page(FormCode));
                    //LogFile1.LogToFile(logTimeString+'FormCode='+FormCode);
                    this.LogFile1.LogToFile(this.logTimeString() + 'CheckCaseDocNoPage caseno=' + CaseID + ',' + Version + ',' + this.FormCode2Version(FormCode) + ',' + DocNo + ',' + this.FormCode2DocNo(FormCode) + ',' + iPage + ',' + this.FormCode2Page(FormCode));
                    //ShowMessage(DocNo+','+IntToStr(docInt)+','+IntToStr(tempInt));
                    //LogFile1.LogToFile(logTimeString+'FormCode='+FormCode);
                    if (Version === this.FormCode2Version(FormCode) && DocNo === this.FormCode2DocNo(FormCode) && iPage === this.FormCode2Page(FormCode)) {
                        this.LogFile1.LogToFile(this.logTimeString() + 'CheckCaseDocNoPage caseno=' + CaseID + ',' + Version + ',' + this.FormCode2Version(FormCode) + ',' + DocNo + ',' + this.FormCode2DocNo(FormCode) + ',' + iPage + ',' + this.FormCode2Page(FormCode));
                        //Showmessage(version+','+DocNo+',iPage='+iPage);
                        //Showmessage(inttostr(Count+1));
                        Count++;
                        break;   //找到了...離開
                    }
                }
            }
        } finally {
            S.Free();
            S2.Free();
        }
        return Count;
    };
    cls.prototype.TransCaseID = function(Path: string, CaseID: string, MainCase: boolean): boolean { //傳送案件
        let i: number, n: number, v: number;
        let ZipFileList: any;
        let UpFormID: string;
        let pages: number;
        let TransName: string;
        let MaskPath: string;
        let HaveMask = false;
        let S: string;
        let SendData: string;
        let Doc_Data = '', Doc_Data1 = '';
        let In_Doc1 = '', In_Doc2 = '';
        let AttachYN: string; //是否有附件 Y:有 N:沒有
        let ST1: any, ST2: any, ST3: any;
        let str1 = '', str2 = '';
        let must_formidStr = '';
        let last_add_formidstr = '';
        let ScanListStr = '';
        let casepath: string;
        let filesizeInt = 0;
        let case_page: string;
        let Fname: string;
        let FileRec: any;
        let Result = true;
        TransName = CaseID;
        MaskPath = Path + 'MaskImg\\';
        if (fs.existsSync(Path + 'Context.dat')) {
            this.ContextList.LoadFromFile(Path + 'Context.dat');
            this.Context_DocnoList.LoadFromFile(Path + 'Context_DocNo.dat');
        }
        if (fs.existsSync(Path + 'CustomDocNo.dat')) {
            this.Cust_DocNoList.LoadFromFile(Path + 'CustomDocNo.dat');
        }
        pages = this.ContextList.Count;
        case_page = pages.toString();
        if (this.FMode === 'NSCAN' || this.FMode === 'ESCAN' || this.FMode === 'ASCAN' || this.FMode === 'DSCAN' || this.FMode === 'SSCAN' || this.FMode === 'MSCAN' || this.FMode === 'RI_SCAN' || this.FMode === 'RSCAN') {
            //Showmessage('1');
            UpFormID = this.GetCaseFormID(Path);
            //{if UpformID = '' then             //20131213  yuu說不管主form
            //begin
            //  Showmessage(_msg('取不到主FormID!!'));
            //  Result := False;
            //  DataLoading(False,False);
            //  Exit;
            //end;}
        }
        this.CaseResort2Scanlist(Path); //檔名照設定排序產生scanlist.dat
        //CaseResort(Path);  //檔名照設定排序
        this.CreateFormID_FormName(Path, CaseID);  //產生FormID_FormName.dat
        this.CreateDocNo_DocName(Path, CaseID); //產生DocNo_Name.dat
        Doc_Data = this.CreateDocNo_Info(CaseID);  //產生保管袋文件 Docno,份數,頁數;Docno,份數,頁數 的回傳字串
        Doc_Data1 = this.CreateCustDocNo_Info(CaseID);  //產生自定文件 Docname,份數,頁數;Docno,份數,頁數 的回傳字串
        In_Doc1 = this.CreateDocnoFrom_Info(CaseID); //產生被引進的保管袋文件資訊  Docno[tab]份數[tab]案件編號#13#10Docno[tab]份數[tab]案件編號
        In_Doc2 = this.CreateCustDocNoFrom_Info(CaseID);   //產生被引進的自定文件資訊  Docno[tab]份數[tab]案件編號#13#10Docno[tab]份數[tab]案件編號
        AttachYN = this.CreateAttach_Info(CaseID); //是否還有附件 Y:有 N:沒有
        this.ReadCaseIndex(Path);
        //LoanDoc := 'Y';
        //產生遮罩影像
        //  if FWork_No = 'CW' then
        //    HaveMask := Case2Mask(Path,MaskPath);
        //產生遮罩影像
        // S := S +#13+'5-->'+ Timetostr(now);
        ///////必要formid 20170315 start  //////////////////////////////
        must_formidStr = '';
        last_add_formidstr = '';
        ST1 = new this.TStringList();
        ST1.LoadFromFile(Path + 'FormCode_Name.dat');
        //ShowMessage(ST1.Text);
        //ShowMessage(LastInitFormidList.Text);
        ST2 = new this.TStringList();
        ST3 = new this.TStringList();
        for (i = 0; i < ST1.Count; i++) {
            if (ST1.Strings[i].indexOf('_') !== 0 && ST1.Strings[i].indexOf('_') !== -1) {
                str1 = ST1.Strings[i].substring(0, ST1.Strings[i].indexOf('_'));
                ST2.Add(str1);
                must_formidStr = must_formidStr + str1 + '@#,';
            }
        }
        must_formidStr = must_formidStr.substring(0, must_formidStr.length - 3);
        //ShowMessage('must_formidStr='+must_formidStr);
        //ShowMessage('AST2='+ST2.Text);
        for (i = 0; i < this.LastInitFormidList.Count; i++) {
            if (ST2.IndexOf(this.LastInitFormidList.Strings[i]) !== -1) {
                ST2.Delete(ST2.IndexOf(this.LastInitFormidList.Strings[i]));
            }
        }
        //ShowMessage('BST2='+ST2.Text);
        for (i = 0; i < ST2.Count; i++) {
            last_add_formidstr = last_add_formidstr + ST2.Strings[i] + '@#,';
        }
        last_add_formidstr = last_add_formidstr.substring(0, last_add_formidstr.length - 3);
        ST3.LoadFromFile(Path + 'scanlist.dat');
        for (i = 0; i < ST3.Count; i++) {
            if (ScanListStr === '') {
                ScanListStr = this.FileName2FormCode(ST3.Strings[i]);
            } else {
                ScanListStr = `${ScanListStr},${this.FileName2FormCode(ST3.Strings[i])}`;
            }
        }
        ST1.Free();
        ST2.Free();
        ST3.Free();
        //ShowMessage('last_add_formidstr='+last_add_formidstr);
        ///////必要formid 20170315 end //////////////////////////
        ///保留外部影像  start///////////////////////////////
        casepath = Path.substring(0, Path.indexOf('Upload'));
        //ShowMessage('casepath='+casepath);
        //FIsExternal:='Y';
        if (this.FMode === 'ESCAN' && this.FIsExternal === 'Y') {
            if (fs.existsSync(casepath + 'Download\\FirstImg.zip')) {
                fs.copyFileSync(casepath + 'Download\\FirstImg.zip', Path + 'FirstImg.zip');
            }
            else {
                fs.copyFileSync(casepath + 'Download\\' + CaseID + '.zip', Path + 'FirstImg.zip');
            }
        }
        ///保留外部影像  end///////////////////////////////
        //file_size 計算  就先不做 20170316
        filesizeInt = 0;
        //////壓檔/////
        this.ZipMainFile(Path, Path, 'Img.zip');
        if (HaveMask) {
            this.ZipMaskFile(Path, MaskPath, Path, 'MaskImg.zip');  //有遮罩設定的才產生
        }
        /////壓檔////
        ///檢查上傳的zip大小////
        Fname = Path + 'Img.zip';
        // FindFirst(FName, faAnyfile, FileRec);
        // NodeJS equivalent using fs.statSync
        let stat = fs.statSync(Fname);
        //FMaxUploadSize
        //ShowMessage(IntToStr(FileRec.Size));
        //Result:=False;
        //exit;           //目前上傳檔案大小為xxMB,已超過50MB,無法上傳    %.3f  ,[FileRec.Size / 1048576]
        if (stat.size > parseInt(this.FMaxUploadSize, 10) * 1048576) { // 檢查檔案大小
            this.Showmessage(this.Format(this._Msg('%s目前上傳檔案大小為%.3fMB,已超過%sMB,無法上傳'), [CaseID, stat.size / 1048576, this.FMaxUploadSize]));
            //ShowMessage(Format('%s目前上傳檔案大小為%.3fMB,已超過'+FMaxUploadSize+'MB,無法上傳',[caseid,FileRec.Size / 1048576]) );
            Result = false;
            return Result; // Exit;
        }
        ///檢查上傳的zip大小////
        //ShowMessage('last_add_formidstr='+last_add_formidstr);
        if (!this.GetFtpinfo(CaseID, 'upload')) {   //取案件上傳方式
            //Showmessage(_Msg()Inttostr(HttpError.HttpErrorCode)+' '+HttpError.HttpReason+'.');
            this.DownFileErrStr = this._Msg('取案件上傳資訊失敗!!') + this.HttpErrStr;
            Result = false;
            return Result; // Exit;
        }
        SendData = 'data=' + this.HTTPEncode(this.UTF8Encode(this.FData))
            + '&verify=' + this.FVerify
            + '&form_id=' + UpFormID
            + '&loan_doc=' + this.Case_loandoc
            + '&case_no=' + TransName
            + '&doc_data=' + this.HTTPEncode(this.UTF8Encode(Doc_Data))
            + '&doc_data1=' + this.HTTPEncode(this.UTF8Encode(Doc_Data1))
            + '&attach=' + AttachYN
            + '&case_page=' + case_page
            + '&file_size=' + filesizeInt.toString()
            + '&must_formid=' + must_formidStr  //擁有的 formid
            + '&last_add_formid=' + last_add_formidstr   //當次新加的 formid
            + '&form_code=' + ScanListStr      //scanlist.dat 表單代號
            + '&ftp_image_path=' + this.FFtpExtraPath   //加傳FTP目錄  HTTP上傳時會是空白
            + '&in_doc1=' + this.HTTPEncode(this.UTF8Encode(In_Doc1))
            + '&in_doc2=' + this.HTTPEncode(this.UTF8Encode(In_Doc2));
        if (this.TransMode === this.TTransMode.tsHttp) {
            ////上傳/////
            this.ShowText = CaseID + this._Msg('資料上傳中(Http),請稍候');
            this.DataLoading(true, true);
            if (!this.upFile(this.HTTPSClient, this.FUrl, 'service/imgpsc/IMGPSC02/caseupload', SendData, 'file', Path + 'Img.zip', this.FReWrite, this.Memo1, false)) {
                this.Showmessage(this.HttpError.HttpErrorCode.toString() + ' ' + this.HttpError.HttpReason + '.');
                Result = false;
                return Result; // Exit;
            }
            if (this.Memo1.Lines.Strings[0] === '1') {
                this.Showmessage(this.Format(this._Msg('') + this._Msg(''), [CaseID]) + this.Memo1.Lines.Strings[1] + '。');
                Result = false;
                return Result; // Exit;
            }
            else if (this.Memo1.Lines.Text.indexOf('<script type="text/javascript" src="scripts/IMGPS00/login.js"></script>') > 0) {
                this.Showmessage(this.Format(this._Msg('') + this._Msg('') + this._Msg('閒置過久或被登出,請重新登入'), [CaseID]));
                Result = false;
                return Result; // Exit;
            }
            ////上傳////
        } else if (this.TransMode === this.TTransMode.tsFtp) {
            this.ShowText = CaseID + this._Msg('資料上傳中(Ftp),請稍候');
            this.DataLoading(true, true);
            this.SetFtpInfo();
            try {
                if (!this.IIS_Ftp.FtpsConnect()) {
                    this.Showmessage(this.Format('無法連上Ftp主機,錯誤原因:%s', [this.FtpErrReason]));
                    Result = false;
                    return Result; // Exit;
                }
                if (!this.IIS_Ftp.FtpsToMain(this.FFtpExtraPath, CaseID + '.zip', Path + 'Img.zip', this.display1)) {
                    this.Showmessage(this.Format(this._Msg('上傳案件(%s)時,發生錯誤,錯誤原因:%s'), [CaseID, this.FtpErrStr]));
                    Result = false;
                    return Result; // Exit;
                }
                if (!this.FtpCaseComplete(SendData)) {    //Ftp上傳後通知完成
                    this.Showmessage(this.Format(this._Msg('通知案件(%s)Ftp上傳完成時,發生錯誤!!'), [CaseID]) + this.HttpErrStr);
                    Result = false;
                    return Result; // Exit;
                }
            } finally {
                this.IIS_Ftp.FtpsClose();
            }
        }
        if (this.FMode === 'ESCAN') {    //上傳舊件引入檔案      //20140616 原本先搬舊件再搬新件,改為先搬新件再搬舊件
            if (!this.TransOldCaseFile(this.ImageSavePath + CaseID + '\\')) {
                Result = false;
                return Result; // Exit;
            }
        }
        // 呼叫Server完成 /////
        //{If not CaseComplete(Path,CaseID,MainCase) Then
        //begin
        //  Showmessage(_Msg('通知案件傳送完成時,網路發生錯誤!!')+HttpErrStr);
        //  DataLoading(False,False);
        //  Result := False;
        //  Exit;
        //end;  }
        /// 呼叫Server完成////
        ////刪檔////
        //_DelTree(Path);  //會只刪TransPath
        //ShowMessage('STOP');
        this._DelTree(this.ImageSavePath + CaseID);
        this.SetCaseList('D', -1, CaseID);
        ////刪檔////
        return Result;
    };
    ['Set_caseid', 'Set_data', 'Set_c_docnolist', 'Set_fixfilelist', 'Set_oldcaseinfo'].forEach(p => {
        cls.prototype[p] = function(v: string) {
            let fieldName = 'F' + p.substring(4).replace('caseid', 'CaseID').replace('data', 'Data').replace('c_docnolist', 'C_DocNoList').replace('fixfilelist', 'FixFileList').replace('oldcaseinfo', 'OldCaseInfo');
            this[fieldName] = v;
        };
        cls.prototype['Get_' + p.substring(4)] = function() {
            let fieldName = 'F' + p.substring(4).replace('caseid', 'CaseID').replace('data', 'Data').replace('c_docnolist', 'C_DocNoList').replace('fixfilelist', 'FixFileList').replace('oldcaseinfo', 'OldCaseInfo');
            return this[fieldName];
        };
    });
    cls.prototype.Set_is_oldcase = function(v: string) { this.FIs_OldCase = v.toUpperCase(); };
    cls.prototype.Get_is_oldcase = function() { return this.FIs_OldCase; };
    cls.prototype.Set_casenolength = function(v: string) { this.FCaseNoLength = v === '' ? 0 : parseInt(v, 10); this.CaseIDLength = this.FCaseNoLength; };
    cls.prototype.Get_casenolength = function() { return this.FCaseNoLength.toString(); };
    cls.prototype.Set_filesizelimit = function(v: string) { this.FFileSizeLimit = v === '' ? 5120 : parseInt(v, 10); };
    cls.prototype.Get_filesizelimit = function() { return this.FFileSizeLimit.toString(); };
    cls.prototype.Set_check_main_form = function(v: string) { this.FCheck_main_form = v; };
    cls.prototype.Get_check_main_form = function() { return this.FCheck_main_form; };
    cls.prototype.Set_WH_CATEGORY = function(v: string) { this.FWH_category = v; };
    cls.prototype.Get_WH_CATEGORY = function() { return this.FWH_category; };
    cls.prototype.Set_isExternal = function(v: string) { this.FIsExternal = v; };
    cls.prototype.Get_isExternal = function() { return this.FIsExternal; };
    cls.prototype.Set_imgdelete = function(v: string) { this.FImgDelete = v; };
    cls.prototype.Get_imgdelete = function() { return this.FImgDelete; };
    cls.prototype.Set_custdocyn = function(v: string) { this.FCustDocYN = v.toUpperCase(); };
    cls.prototype.Get_custdocyn = function() { return this.FCustDocYN; };
    cls.prototype.Set_printyn = function(v: string) { this.FPrintyn = v.toUpperCase(); };
    cls.prototype.Get_printyn = function() { return this.FPrintyn; };
    cls.prototype.Set_work_no = function(v: string) { this.FWork_no = v; };
    cls.prototype.Get_work_no = function() { return this.FWork_no; };
    cls.prototype.Set_verify = function(v: string) { this.FVerify = v; };
    cls.prototype.Get_verify = function() { return this.FVerify; };
    cls.prototype.Set_userunit = function(v: string) { this.FUserUnit = v; };
    cls.prototype.Get_userunit = function() { return this.FUserUnit; };
    cls.prototype.Set_username = function(v: string) { this.FUserName = v; };
    cls.prototype.Get_username = function() { return this.FUserName; };
    cls.prototype.Set_userid = function(v: string) { this.FUserID = v; };
    cls.prototype.Get_userid = function() { return this.FUserID; };
    cls.prototype.Set_useproxy = function(v: string) { this.FUseProxy = v.toUpperCase(); };
    cls.prototype.Get_useproxy = function() { return this.FUseProxy; };
    cls.prototype.Set_url = function(v: string) { this.FUrl = v; };
    cls.prototype.Get_url = function() { return this.FUrl; };
    cls.prototype.Set_rewrite = function(v: string) { this.FReWrite = v; };
    cls.prototype.Get_rewrite = function() { return this.FReWrite; };
    cls.prototype.Set_modename = function(v: string) { this.FModeName = v; };
    cls.prototype.Get_modename = function() { return this.FModeName; };
    cls.prototype.Set_mode = function(v: string) { this.FMode = v.toUpperCase(); };
    cls.prototype.Get_mode = function() { return this.FMode; };
    cls.prototype.Set_loandoc_value = function(v: string) { this.FLoanDoc_Value = v; };
    cls.prototype.Get_loandoc_value = function() { return this.FLoanDoc_Value; };
    cls.prototype.Set_loandoc_enable = function(v: string) { this.FLoanDoc_Enable = v; };
    cls.prototype.Get_loandoc_enable = function() { return this.FLoanDoc_Enable; };
    cls.prototype.Set_language = function(v: string) {
        this.FLanguage = v.toLowerCase();
        if (this.FLanguage === 'zh-tw') {
            this.FLanguage = 'zh_tw';
        }
    };
    cls.prototype.Get_language = function() { return this.FLanguage; };
    cls.prototype.Set_is_in_wh = function(v: string) { this.FIs_In_Wh = v.toUpperCase(); };
    cls.prototype.Get_is_in_wh = function() { return this.FIs_In_Wh; };
    cls.prototype.Set_c_docnamelist = function(v: string) { this.FC_DocNameList = v; };
    cls.prototype.Get_c_docnamelist = function() { return this.FC_DocNameList; };
    cls.prototype.DataLoading = function(Loading: boolean, UseTimer: boolean): void {
        if (Loading) {
            // Screen.Cursor := -11;
            if (UseTimer) {
                this.Panel22.Caption = this.ShowText;
                this.Panel22.Left = Math.floor(this.Panel9.Width / 2) - Math.floor(this.Panel22.Width / 2);
                this.Panel22.Top = Math.floor(this.Panel9.Height / 2) - Math.floor(this.Panel22.Height / 2);
                this.Panel22.Visible = true;
                this.Timer2.Enabled = true;
            } else {
                this.Panel8.Left = Math.floor(this.Panel9.Width / 2) - Math.floor(this.Panel8.Width / 2);
                this.Panel8.Top = Math.floor(this.Panel9.Height / 2) - Math.floor(this.Panel8.Height / 2);
                this.Panel8.Visible = true;
            }
            this.Application.ProcessMessages();
            this.Panel1.Enabled = false;
            this.Panel2.Enabled = false;
        } else {
            this.Panel22.Visible = false;
            this.Panel8.Visible = false;
            this.Timer2.Enabled = false;
            this.Panel1.Enabled = true;
            this.Panel2.Enabled = true;
            // Screen.Cursor := 0;
        }
    };
    cls.prototype.ReduceLogFile = function(): void {
        let ST1: any;
        let I: number;
        ST1 = new this.TStringList();
        if (fs.existsSync(this.LngPath + 'IMGPSCheck.log')) {
            ST1.LoadFromFile(this.LngPath + 'IMGPSCheck.log');
            if (ST1.Count > 100000) {
                for (I = 0; I <= 10000; I++) {
                    ST1.Delete(0);
                }
                ST1.SaveToFile(this.LngPath + 'IMGPSCheck.log');
            }
        }
        ST1.Free();
    };
    cls.prototype.ClearCaseIndex = function(): void {
        this.AddCredit1RG.Enabled = false;
        this.AddCredit1RG.ItemIndex = -1;
    };
}
separate/scanImp/CB_IMGPSScanImp_Main.pas
比對新檔案
@@ -0,0 +1,1089 @@
unit CB_IMGPSScanImp;
//TEST
{$WARN SYMBOL_PLATFORM OFF}
interface
uses
  EnScan,   { for Scanner }
  EnDiGrph, { for TDibGraphic }
  EnMisc,   { for MinFloat }
  EnTifGr,  { for TTifGraphic }
  {$IFDEF Production}
  CB_IMGPSScan_TLB,
  {$ENDIF}
  {$IFDEF Test}
  CB_IMGPSScan_test_TLB,
  {$ENDIF}
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ActiveX, AxCtrls, StdVcl, VCLUnZip, VCLZip, Encryp,
  ExtCtrls, ComCtrls, Menus, StdCtrls, Gauges, EnImgScr, PJMenuSpeedButtons,
  Buttons, ImgList, SBSimpleSSL, SBHTTPSClient, SBWinCertStorage, SBX509,
  SBCustomCertStorage, SBUtils,mpsBarco,BarcodesFinder,HTTPApp,ErrList,
  Xmltool,inifiles,printers,IdHashMessageDigest, idHash, LogFile,ShellApi,
  SBSocket,IIS_Ftp, SBSimpleFTPS;
Type
  TTransMode =(tsHttp,tsFtp,tsNone);
var
  Ch_WriteNote : Boolean;
  RejectCase : Boolean;
  ErrIndex : Integer;
  //*****預設區*****//
    Def_DeviceDelete : Boolean;  //空白頁刪除啟動
    Def_DeviceDeleteSize : Integer;  //空白頁Size
    Def_ScannerReverse : Boolean; //是否需反相
    Def_BoardClear : Boolean; //是否清黑邊
    Def_ScanDpi : Integer;    //掃瞄DPI
    Def_ScanDuplex : Boolean; //是否雙面掃瞄
    Def_ScanRotate : Integer; //掃瞄時旋轉角度
    Def_ScanDeskew : Boolean; //是否傾斜矯正
    Def_ScanBright : Integer; //亮度
    Def_ScanContrast : Integer; //對比
    Def_ScanImgShowMode : Integer; //0:清楚影像 1:不清楚影像 2:不顯示
    Def_ScanImgSetUse : Boolean; //是否使用亮度對比設定
  //*****預設區*****//
Const
  ISBName = 'PreViewISB';
{獲取自身版本號所需要 }
type
  TVersionLanguage = (vlArabic, vlBulgarian, vlCatalan, vlTraditionalChinese,
     vlCzech, vlDanish, vlGerman, vlGreek, vlUSEnglish, vlCastilianSpanish,
     vlFinnish, vlFrench, vlHebrew, vlHungarian, vlIcelandic, vlItalian,
     vlJapanese, vlKorean, vlDutch, vlNorwegianBokmel, vlPolish,
     vlBrazilianPortuguese, vlRhaetoRomanic, vlRomanian, vlRussian,
     vlCroatoSerbian, vlSlovak, vlAlbanian, vlSwedish, vlThai, vlTurkish,
     vlUrdu, vlBahasa, vlSimplifiedChinese, vlSwissGerman, vlUKEnglish,
     vlMexicanSpanish, vlBelgianFrench, vlSwissItalian, vlBelgianDutch,
     vlNorwegianNynorsk, vlPortuguese, vlSerboCroatian, vlCanadianFrench,
     vlSwissFrench, vlUnknown);
const LanguageValues: array[TVersionLanguage] of Word = ($0401, $0402, $0403,
   $0404, $0405, $0406, $0407, $0408, $0409, $040A, $040B, $040C, $040D,
   $040E, $040F, $0410, $0411, $0412, $0413, $0414, $0415, $0416, $0417,
   $0418, $0419, $041A, $041B, $041C, $041D, $041E, $041F, $0420, $0421,
   $0804, $0807, $0809, $080A, $080C, $0810, $0813, $0814, $0816, $081A,
   $0C0C, $100C, $0000);
{獲取自身版本號所需要 end}
type
  TScanMode = (smNew, smReplace, smInsert, smSample, smRTS);
  TpScanInfo = ^TScanInfo;
  TScanInfo = record
    MultiPage  : Boolean;
    { supplementary info when MultiPage is True }
    Graphic    : TTiffGraphic;
    Stream     : TFileStream;
    ImageCount : LongInt;
  end;
  TOMRErrInfo = record
    Display : Boolean;  //是否顯示
    Ignore : Boolean; //可否刪除
    Info : String;
    Mode : String;
  end;
  TScrollRec = Record
    HScroll : Integer;
    VScroll : Integer;
    Rate    : Single;
  end;
type
  TCB_IMGPSScanX = class(TActiveForm, ICB_IMGPSScanX)
    Panel1: TPanel;
    TransBtn: TBitBtn;
    Panel21: TPanel;
    ViewModeBtn: TPJMenuSpeedButton;
    Button2: TButton;
    Button1: TButton;
    OptionBtn: TBitBtn;
    SelectScanBtn: TBitBtn;
    NextPageBtn: TBitBtn;
    PrePageBtn: TBitBtn;
    FC6: TBitBtn;
    FC5: TBitBtn;
    FC4: TBitBtn;
    FC3: TBitBtn;
    FC2: TBitBtn;
    FC1: TBitBtn;
    FC0: TBitBtn;
    Panel23: TPanel;
    PJLinkedMenuSpeedButton2: TPJLinkedMenuSpeedButton;
    NewScanBtn: TBitBtn;
    AddScanBtn: TBitBtn;
    Panel4: TPanel;
    Panel2: TPanel;
    Splitter1: TSplitter;
    Panel3: TPanel;
    Label7: TLabel;
    Panel6: TPanel;
    CB1: TCheckBox;
    ScanDuplexCB: TCheckBox;
    ScanFlatCB: TCheckBox;
    Panel18: TPanel;
    Panel12: TPanel;
    Panel7: TPanel;
    Panel9: TPanel;
    Shape1: TShape;
    ImageScrollBox1: TImageScrollBox;
    imgp8: TPanel;
    lb8: TLabel;
    ISB8: TImageScrollBox;
    imgp7: TPanel;
    lb7: TLabel;
    ISB7: TImageScrollBox;
    imgp6: TPanel;
    lb6: TLabel;
    ISB6: TImageScrollBox;
    imgp5: TPanel;
    lb5: TLabel;
    ISB5: TImageScrollBox;
    imgp4: TPanel;
    lb4: TLabel;
    ISB4: TImageScrollBox;
    imgp3: TPanel;
    lb3: TLabel;
    ISB3: TImageScrollBox;
    imgp2: TPanel;
    lb2: TLabel;
    ISB2: TImageScrollBox;
    imgp1: TPanel;
    lb1: TLabel;
    Memo1: TMemo;
    Display1: TMemo;
    Panel22: TPanel;
    Panel8: TPanel;
    Label2: TLabel;
    Gauge1: TGauge;
    ScrollBar1: TScrollBar;
    Panel10: TPanel;
    PageLV: TListView;
    StatusBar1: TStatusBar;
    ImageList4: TImageList;
    ImageList3: TImageList;
    ImageList1: TImageList;
    PopupMenu2: TPopupMenu;
    mode1: TMenuItem;
    mode2: TMenuItem;
    mode3: TMenuItem;
    mode4: TMenuItem;
    N51: TMenuItem;
    ImageList2: TImageList;
    OpenDialog1: TOpenDialog;
    PopupMenu3: TPopupMenu;
    PM301: TMenuItem;
    PM302: TMenuItem;
    PM303: TMenuItem;
    Timer1: TTimer;
    TomEncryption1: TTomEncryption;
    SaveDialog1: TSaveDialog;
    Timer2: TTimer;
    VCLZip1: TVCLZip;
    PopupMenu4: TPopupMenu;
    PM401: TMenuItem;
    PM402: TMenuItem;
    PM403: TMenuItem;
    PM404: TMenuItem;
    ElWinCertStorage: TElWinCertStorage;
    ElMemoryCertStorage: TElMemoryCertStorage;
    Panel5: TPanel;
    AddCredit1RG: TRadioGroup;
    Panel11: TPanel;
    SampleScanBtn: TBitBtn;
    WNoteBtn: TBitBtn;
    PopupMenu1: TPopupMenu;
    PM101: TMenuItem;
    N12: TMenuItem;
    PM102: TMenuItem;
    MenuItem1: TMenuItem;
    PM103: TMenuItem;
    PM104: TMenuItem;
    N7: TMenuItem;
    PM106: TMenuItem;
    PM107: TMenuItem;
    PM108: TMenuItem;
    PM109: TMenuItem;
    PopupMenu5: TPopupMenu;
    PM501: TMenuItem;
    PM502: TMenuItem;
    PM503: TMenuItem;
    PM504: TMenuItem;
    N5: TMenuItem;
    PM505: TMenuItem;
    PM506: TMenuItem;
    N29: TMenuItem;
    PM510: TMenuItem;
    PM509: TMenuItem;
    PM507: TMenuItem;
    N15: TMenuItem;
    PM508: TMenuItem;
    ExportBt: TButton;
    ImportBt: TButton;
    CheckCaseBtn: TBitBtn;
    DenialTimeLb: TLabel;
    Panel14: TPanel;
    ScrollBox1: TScrollBox;
    Label1: TLabel;
    Button3: TButton;
    Panel15: TPanel;
    Button4: TButton;
    PopupMenu6: TPopupMenu;
    PM602: TMenuItem;
    PM601: TMenuItem;
    PM605: TMenuItem;
    PM603: TMenuItem;
    PM604: TMenuItem;
    N8: TMenuItem;
    Panel16: TPanel;
    SpeedButton3: TSpeedButton;
    SpeedButton14: TSpeedButton;
    SpeedButton15: TSpeedButton;
    SpeedButton16: TSpeedButton;
    SpeedButton17: TSpeedButton;
    SpeedButton18: TSpeedButton;
    SpeedButton19: TSpeedButton;
    SpeedButton20: TSpeedButton;
    SpeedButton21: TSpeedButton;
    SpeedButton22: TSpeedButton;
    Edit1: TEdit;
    PM110: TMenuItem;
    Image1: TImage;
    UseOldCaseLb: TLabel;
    PM111: TMenuItem;
    SmoothCB: TCheckBox;
    ISB_BW: TImageScrollBox;
    N1: TMenuItem;
    N2: TMenuItem;
    ISB1: TImageScrollBox;
    Label3: TLabel;
    LogFile1: TLogFile;
    ScanGrayCB: TCheckBox;
    AttFileGB: TGroupBox;
    AttListBox: TListBox;
    Panel20: TPanel;
    AddAttFileLB: TLabel;
    DelAttFileLB: TLabel;
    Splitter2: TSplitter;
    Panel17: TPanel;
    TreeView1: TTreeView;
    Panel13: TPanel;
    PrtLb: TLabel;
    CaseHelpBtn: TBitBtn;
    HTTPSClient: TElHTTPSClient;
    Button5: TButton;
    FTPSClient1: TElSimpleFTPSClient;
    Button6: TButton;
    procedure ActiveFormCreate(Sender: TObject);
    procedure Panel9Resize(Sender: TObject);
    procedure ISB1Click(Sender: TObject);
    procedure WNoteBtnClick(Sender: TObject);
    procedure CaseHelpBtnClick(Sender: TObject);
    procedure FC0Click(Sender: TObject);
    procedure FC1Click(Sender: TObject);
    procedure FC2Click(Sender: TObject);
    procedure FC3Click(Sender: TObject);
    procedure FC4Click(Sender: TObject);
    procedure FC5Click(Sender: TObject);
    procedure FC6Click(Sender: TObject);
    procedure PrePageBtnClick(Sender: TObject);
    procedure NextPageBtnClick(Sender: TObject);
    procedure OptionBtnClick(Sender: TObject);
    procedure SelectScanBtnClick(Sender: TObject);
    procedure mode1Click(Sender: TObject);
    procedure mode2Click(Sender: TObject);
    procedure mode3Click(Sender: TObject);
    procedure mode4Click(Sender: TObject);
    procedure N51Click(Sender: TObject);
    procedure ScrollBar1Change(Sender: TObject);
    procedure ISB1EndScroll(Sender: TObject);
    procedure ISB1ImageMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure ISB1ImageMouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    procedure ISB1ImageMouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure PM508Click(Sender: TObject);
    procedure PM401Click(Sender: TObject);
    procedure PM402Click(Sender: TObject);
    procedure PM403Click(Sender: TObject);
    procedure PM404Click(Sender: TObject);
    procedure TreeView1Click(Sender: TObject);
    procedure NewScanBtnClick(Sender: TObject);
    procedure AddScanBtnClick(Sender: TObject);
    procedure PageLVClick(Sender: TObject);
    procedure PageLVKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure PageLVMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure PM101Click(Sender: TObject);
    procedure PM104Click(Sender: TObject);
    procedure PM102Click(Sender: TObject);
    procedure PM103Click(Sender: TObject);
    procedure PM106Click(Sender: TObject);
    procedure PM107Click(Sender: TObject);
    procedure PM108Click(Sender: TObject);
    procedure PM109Click(Sender: TObject);
    procedure PM301Click(Sender: TObject);
    procedure PM302Click(Sender: TObject);
    procedure PM303Click(Sender: TObject);
    procedure PM501Click(Sender: TObject);
    procedure PM502Click(Sender: TObject);
    procedure PM503Click(Sender: TObject);
    procedure PM504Click(Sender: TObject);
    procedure PM505Click(Sender: TObject);
    procedure PM510Click(Sender: TObject);
    procedure PM509Click(Sender: TObject);
    procedure PM507Click(Sender: TObject);
    procedure PopupMenu1Popup(Sender: TObject);
    procedure PopupMenu4Popup(Sender: TObject);
    procedure PopupMenu5Popup(Sender: TObject);
    procedure SampleScanBtnClick(Sender: TObject);
    procedure StatusBar1DblClick(Sender: TObject);
    procedure ExportBtClick(Sender: TObject);
    procedure ImportBtClick(Sender: TObject);
    procedure HTTPSClientCertificateValidate(Sender: TObject;
      X509Certificate: TElX509Certificate; var Validate: Boolean);
    procedure ScanDuplexCBClick(Sender: TObject);
    procedure CheckCaseBtnClick(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    procedure Timer2Timer(Sender: TObject);
    procedure TransBtnClick(Sender: TObject);
    procedure TreeView1DragDrop(Sender, Source: TObject; X, Y: Integer);
    procedure TreeView1DragOver(Sender, Source: TObject; X, Y: Integer;
      State: TDragState; var Accept: Boolean);
    procedure TreeView1KeyUp(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure TreeView1MouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure ViewModeBtnMouseEnter(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure AddCredit1RGClick(Sender: TObject);
    procedure CB1Click(Sender: TObject);
    procedure Button4Click(Sender: TObject);
    procedure ISB1Enter(Sender: TObject);
    procedure TreeView1MouseMove(Sender: TObject; Shift: TShiftState; X,
      Y: Integer);
    procedure PM601Click(Sender: TObject);
    procedure PopupMenu6Popup(Sender: TObject);
    procedure PM604Click(Sender: TObject);
    procedure PM605Click(Sender: TObject);
    procedure SpeedButton3Click(Sender: TObject);
    procedure SpeedButton14Click(Sender: TObject);
    procedure SpeedButton15Click(Sender: TObject);
    procedure SpeedButton16Click(Sender: TObject);
    procedure SpeedButton17Click(Sender: TObject);
    procedure SpeedButton18Click(Sender: TObject);
    procedure SpeedButton19Click(Sender: TObject);
    procedure SpeedButton20Click(Sender: TObject);
    procedure SpeedButton21Click(Sender: TObject);
    procedure SpeedButton22Click(Sender: TObject);
    procedure ActiveFormKeyUp(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure PM110Click(Sender: TObject);
    procedure PM602Click(Sender: TObject);
    procedure PrtLbClick(Sender: TObject);
    procedure Panel1DblClick(Sender: TObject);
    procedure Panel11DblClick(Sender: TObject);
    procedure UseOldCaseLbClick(Sender: TObject);
    procedure PM111Click(Sender: TObject);
    procedure ImageScrollBox1NewGraphic(const Graphic: TDibGraphic);
    procedure SmoothCBClick(Sender: TObject);
    procedure N1Click(Sender: TObject);
    procedure TreeView1MouseEnter(Sender: TObject);
    procedure ScrollBox1MouseEnter(Sender: TObject);
    procedure ScanGrayCBClick(Sender: TObject);
    procedure AddAttFileLBClick(Sender: TObject);
    procedure DelAttFileLBClick(Sender: TObject);
    procedure AttListBoxDblClick(Sender: TObject);
    procedure AttListBoxClick(Sender: TObject);
    procedure TreeView1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure HTTPSClientRedirection(Sender: TObject; const OldURL: string;
      var NewURL: string; var AllowRedirection: Boolean);
    procedure Button5Click(Sender: TObject);
    procedure Button6Click(Sender: TObject);
  private
    { Private declarations }
    HotKeyId1,HotKeyId2,HotKeyId3,HotKeyId4 :Integer;
    ////////傳入參數/////////
    FUrl : String;
    FCaseID : String;
    FMode : String;        //NSCAN:新件;ESCAN:修改 20170306 DSCAN:被配合待掃瞄清單使用
    FModeName : String;
    FWork_no : String;
    FUserID : String;
    FUserName : String;
    FUserUnit : String;
    FData : String;
    FVerify : String;
    FReWrite : String;
    FLanguage : String;
    FLoanDoc_Value : String; //新增授信卷的值
    FLoanDoc_Enable : String; //新增授信卷是否可異動
    FUseProxy : String; //是否使用Proxy
    FC_DocNoList : String; //外面傳來要新增的文件編號
    FC_DocNameList : String; //外面傳來要新增的自訂文件
    FFixFileList : String; //要鎖住的檔名
    FIs_In_Wh : String; //是否只顯示入庫文件  (Y:只顯示入庫 N:只顯示非入庫 空白:都顯示)
    FOldCaseInfo : String; //舊件資訊  CaseID_year[tab]CaseID_year
    FPrintyn : String; //是否可列印
    FIs_OldCase : String; //是否是舊案第一次進入
    FCustDocYN : String; //是否可自訂文件
    //20170222 新增
    FImgDPI:integer;//DPI  依業務別決定 150~~1200  預設300
    FScanColor:integer;///掃瞄色彩  依業務別決定   0 :黑白  1:灰階   2:彩色 預設黑白
    FFileSizeLimit:integer;//匯入單一檔案大小限  依業務別決定  以KB為單位  預設5*1024kb
    FCaseNoLength:integer;//案件編號長度檢查  依業務別決定   無預設  一定要傳入
    FImgDelete:string; // Y/N  有權限可在補件時刪除影像
    FIsExternal:string;//Y/N
    FCheck_main_form:string; //Y/N
    FWH_category:string;  //Y/N  Y是歸類時有區分入庫非入庫文件  N 是不區分
    ////////傳入參數///////
    TransMode : TTransMode;   //用何種方式上傳檔案
    //********Http參數********
    HttpErrStr : String; //錯誤訊息
    //********Http參數********
    //********Ftp參數*********
    FFtpIP : String;
    FFtpID : String;
    FFtpPwd : String;
    FFtpRootPath : String;
    FFtpExtraPath : String;
    FFtpPort : Integer;
    FFtpProtocol : TFtpProtocol;
    //********Ftp參數*********
    //********ini參數********
    DeviceDelete : Boolean;     //空白頁刪除啟動
    DeviceDeleteSize : Integer;  //空白頁Size
    ScannerReverse : Boolean; //是否需反相
    BoardClear : Boolean; //是否清黑邊
    ScanDpi : Integer;    //掃瞄DPI
    ScanDuplex : Boolean; //是否雙面掃瞄
    ScanRotate : Integer; //掃瞄時旋轉角度
    ScanDeskew : Boolean; //是否傾斜矯正
    ScanBright : Integer; //亮度
    ScanContrast : Integer; //對比
    ScanImgShowMode : Integer; //0:清楚影像 1:不清楚影像 2:不顯示
    ScanImgSetUse : Boolean; //是否使用亮度對比設定
    //********ini參數********
    ScanColor : TImageFormat;
    //********時間********
    ServerDate : String;
    ServerTime : String;
    Balance : Longint; //local跟server的時間差
    ScanDenialTime : String; //停止進件時間
    //********時間********
    ScanDenialHint : String; //掃描顯示提醒字串
    //********路徑********
    ScanPath : String;       // SpecifyDir\Workid\userunit\mode
    ImagePath : String;      // SpecifyDir\Workid\userunit\mode
    ImageSavePath : String;   // ImagePath\
    ScaniniPath : String;    // SpecifyDir\Workid\userunit\
    LngPath :String;         //多國語言檔目錄
    CheckXmlPath : String;   //檢核用的Xml存放目錄
    SitePath : String;      //登打位置(切簽名用)
    SamplePath : String;     //範本目錄
    TransPath : String;     //檔案上傳的路徑(補充件會多一層目錄)
    //********路徑********
    ScanSaveFilename : String; //掃瞄的檔名
    ScanCaseno : String;  //掃瞄時的案號
    ScanDocDir : String;  //掃描時的文件代號目錄
    //********清單區********
    Doc_Inf_List : TStringList;  //Doc_Inf 清單
    DM_FORM_INF_List :TstringList;  //DM_FORM_INF 清單
    FORM_INF_List : TStringList; //FORM_INF的清單
    CHECK_RULE_INF_List : TStringList;  //CHECK_RULE_INF  清單
    MEMO_INF_List : TStringList;  //MEMO_INF 清單
    WORK_INF_List : TStringList;  //WORK_INF 清單
    LASTEST_FORM_INF_List : TStringList;  // LASTEST_FORM_INF 清單
    FindResult : TStringList;    //找SQLData的結果
    CaseList : TStringList;    //記錄掃瞄案件的順序
    Context_DocnoList : TStringlist; //案件裡的Docno清單
    CaseDocNoList : TStringlist;  //案件裡的DocNo清單(不重複)
    CaseDocNo_CopiesList : TStringlist; //案件裡的DocNo份數清單
    ContextList : TStringlist; //案件裡的檔案清單
    AttContextList : TStringlist; //案件裡的附加檔案清單
    OMRFileList : TStringList; //要OMR檢核的文件(只檢查每種Form的第一頁)
    Cust_DocNoList : TStringlist; //自行定義文件名稱
    IN_WH_DocNoList : TStringlist; //入庫的文件清單
    FormCode_PageSize : TStringList; //文件的預設大小  FormCode_Height_Width
    DocNo_NeedDoc : TStringList; //有Docno時要相依的文件   DocNo_相依文件_相依文件
    DocNo_NoDoc : TstringList; //有Docno時互斥的文件   DocNo_互斥文件_互斥文件
    DocNo_VerinCase : TStringlist; //案件裡的DocNo+版本的清單
    NoSaveBarCodeList : TStringlist; //不儲存的條碼清單
    FormID_List : TStringlist;    //FormID 清單  20130403 因為原FormCode2Docno會很慢..把FormID抽出來
    DocNo_List : TStringlist;     //DocNo 清單  20130403 因為原FormCode2Docno會很慢..把DocNo抽出來
    NowShowFileList : TStringlist;  //目前顯示的影像清單
    NowSelectFileList : TStringlist; //目前被點選的影像清單
    GuideFormIDList : TStringlist; //要當導引頁表單清單
    DivPageFormIDList:TStringlist; //要當分案頁表單清單
    LastInitFormidList:TStringList;
    LastAddFormidList:TstringList;
    SampleFormIDList:TStringList; //20170627 已存在範本的formid
    ExistImgList:TStringList;  //20170724  已經存在的影像list for ESCAN  //Img的完整路徑
    reSizeExistImgList:TstringList; //20171012被縮放的舊圖MD5存入
    //********清單區********
    //********顯示區********
    NowCaseno : String; //目前顯示的案件編號
    NowDocNo : String;  //目前的文件編號
    NowDocDir : String; //目前的文件目錄
    NowFormCode : String; //目前顯示的表單編號
    NowFormName : String; //目前顯示的表單名稱
    NowPage : Integer;   //目前點選的頁碼
    DisplayPath : String; //目前顯示的目錄
    //********顯示區********
    //******索引資料*********
    Case_loandoc : String; //是否新增授信卷
    //******索引資料*********
    //********十字定位點資訊********
    UpLPoint : Tpoint;  //左上方的十字點
    UpRPoint : Tpoint;  //右上方的十字點
    DownLPoint : Tpoint;  //左下方的十字點
    DownRPoint : Tpoint;  //右下方的十字點
    Point_Width : String;   //十字點的寬
    Point_Height : String;   //十字點的高
    //********十字定位點資訊********
    CaseIDLength : Integer; //案件編號長度
    FormIDLength : Integer; //FormID長度
    DocNoLength : Integer;  //Docno長度
    PEFileName : String; //掃描時的檔名
    DownFileErrStr : String;  //下載影像時發生的錯誤
    ISB : TImageScrollBox;
    ScanInfo    : TScanInfo;
    TwainShowUI : Boolean;
    MpsBarcodeinf : TMpsBarcodeinf;
    ScanMode : TScanMode;
    Mpskey : String;
    Seg : Integer; //顯示的邊界值
    VMode : Integer; //顯示的index
    NowClick : Integer;   //目前點到的按鈕功能Index
    ScanIP : String;  //掃瞄端的IP
    DisplayISB : TImageScrollBox; //被點到的影像
    SelectISB : TImageScrollBox; //被點到的縮圖
    SelectPage : Integer; //被點到的頁數
    NewTreeNode,MyTreeNode1,MyTreeNode2,MyTreeNode3 : TTreenode;
    InitialOk : Boolean; //資訊載入是否完成
    ShowText : String; //DataLoading時要秀出的訊息
    Ext : String; //附檔名  .tif .jpg
    SafePixel : Integer;  //OMR容忍誤差值點數
    OMRErrInfo : Array[1..11] of TOMRErrInfo;  //檢核的方式及訊息
    ScrollRec : Array[1..8] of TScrollRec; //瀏覽窗的Scroll記錄
    RecHozPos,RecVerPos : Integer;  //記錄MPSViewX1的ScrollBar位置
    ReczoomPercent : Single;
    SortMode : Boolean;
    PreMytreeNode2Name:String;
    HS,VS : Integer;
    iRate : Single;
    Bt : Integer; //去直線時橫線判斷的容忍值
    BarCodeRotate : Integer; //條碼要轉的角度
    HaveAppDoc : Boolean; //補全時是否有補入要保書
    PageLVclear : Boolean;
    CaseCount,PageCount : Integer; //總案件量及總頁數
    Item : TMenuItem;
    SampleAnchorMode : String; //範本掃瞄十字線的模式  NONE:無;ANCHOR:十字;FRAME:邊框
    DownImgStatus : String;  //下載影像的狀態(NO_DATA:沒資料;NO_FILE:沒影像)
    TransForm_Field : String; //要用OMR勾選確認是否轉換FORMID的欄位名稱
    NowWork_No : String; //現在的作業別
    CropBarcode : String; //要切影像的條碼
    Has_Authorize :String; //是否有授權書影像
    AttName : String; //未歸類目錄名稱
    NowGuideFormID : String;
    NowDivPageFormID:String;
    FirstDocDir : String;
    FMaxUploadSize:String;// 上傳zip大小限制
    FJpgCompression:integer;// 20171211 jpg to tif 的壓縮率
    Draging : Boolean;
    MDown : Boolean;  //20181210 用來判斷滑鼠右鍵要Popupmenu是否有MouseDonw發生
    FEvents: ICB_IMGPSScanXEvents;
    procedure HotKeyDown (var Msg : TMessage);message WM_HOTKEY;
    Procedure InitialLanguage(Sender: TObject);   //畫面載入多國語言
    //Function _Msg(S:String):String;
    //*********SQL相關************
    Procedure SetSQLData(ColumeStr:String;FromList,ToList:TStringlist); //把SQL值塞入
    Function GetSQLData(TableList:TStringlist;Colname:String;colNo:Integer):String; //依欄位及索引取值
    Function FindSQLData(TableList:TStringlist;ColumeStr,KeyColumeStr,KeyStr:String;ColNo:Integer;Var ResultList:TStringlist):Boolean; //找指定的資料
    Function GetFindResult(Col:String):String;
    //*********SQL相關************
    //*********FTP相關************
    Function GetFtpinfo(CaseID,Action:String):Boolean;
    Procedure SetFtpInfo;     //餵入FTP資訊
    Function FtpCaseComplete(SendData:String):Boolean;
    //*********FTP相關************
    //*******轉換區*********
    Function FindDivFormCode(FormCode:String):Boolean; //找有沒有分案的條碼
    Function FormCode2FormName(CaseID,FormCode:String):String; //用FormCode轉成文件名稱
    Function FormCode2FileName(FormCode:String;List:TStrings):String; //用FormCode找出檔名(第一頁)
    Function FileName2FormCode(FileName:String):String; //從檔名取出FormCode
    Function FileName2FormName(CaseID,FileName:String):String; //從檔名取出文件名稱
    Function FileName2ScanPage(FileName:String):Integer; //從檔名取出掃瞄頁數
    Function FileName2NoQuene_Filename(FileName:String):String; //取出沒有序號的檔名
    Function FileName2Index(FileName:String):Integer; //從檔名取出在ContextList的序號
    Function FileName2NowDcoNo(FileName:String;CtList,DNList:TStrings):String; //從檔名取出歸屬的文件代號
    Function FormCode2DocNo(FormCode:String):String;   //FormCode轉Docno
    Function FormCode2Version(FormCode:String):String; //FormCode轉版本
    Function FormCode2Page(FormCode:String):String; //FormCode轉文件頁數
    Function DocNo2DocName(CaseID,DocNo:String):String; //Docno轉Doc名稱
    Function DocNo2FileName(DocNo:String;List:TStrings):String; //用DocNo找出檔名(第一頁)
    Function FormCode2WorkNo(FormCode:String):String; //用FormCode取出作業別
    Function DocNo2WorkNo(DocNo:String):String; //用DocNo取出作業別
    Function DocNo2DocNoDir(Path,DocNo:String):String;    //DocNo轉成DocNo(份數)目錄
    Function DocNoDir2DocNo(DocNoDir:String):String; //DocNo(份數)目錄轉成DocNo
    Function DocNoDir2Index(Path,DocNoDir:String):Integer; //DocNo(份數)目錄轉成index
    Function DocNoNeedDiv(DocNo:String):Boolean; //是否是需分份數的文件代號
    //Function CaseNo2DocNo(CaseNo:String):TStringList;
    Function CaseNode2Info(Node:TTreeNode;Mode:Char):String;   //案件Node取案件編號  Mode: I:Caseno;P:Page
    Function DocNode2Info(Node:TTreeNode;Mode:Char):String;     //文件Node取文件代號 Mode: I:Docno;N:Docname;P:Page;G:Group
    Function FormNode2Info(Node:TTreeNode;Mode:Char):String;   //表單Node取表單代號  Mode: I:FormID;N:FormName;P:Page
    //*******轉換區*********
    Procedure PriorPage(Page:Integer); //上一頁
    Procedure NextPage(Page:Integer); //下一頁
    Function DocNoExistsinTree(CaseNode:TTreeNode;DocNo:String):Boolean; //DocNo是否己存在樹裡
    Function DocnoNeedGroup(DocNo:String):Boolean; //傳入的DocNo是否需分組
    function GetSiteOMR(FileName, Site: String;bt: Integer): Integer;
    Function FindISB2View(Vmode:Integer):TImageScrollBox; //找空的ISB來顯示
    Procedure R_W_ScanIni(Mode:Char); //'R'讀取;'W'寫入
    Procedure GetDefScanIni; //取得掃瞄的預設值
    procedure DesableImage;
    procedure EnableImage(v:integer;Sender : TObject);
    Procedure ViewMouseMode(v:Integer);
    Procedure GoViewMode;
    Procedure DisplayMode(index,H_Count,W_Count:Integer;BasePanel:TPanel);
    Function GetServerDate : Boolean; //取主機時間
    Function GetSetInf1 : Boolean; //取系統設定資訊mode1  DOC_INF
    Function GetSetInf2 : Boolean; //取系統設定資訊mode2  DM_FORM_INF
    Function GetSetInf3 : Boolean; //取系統設定資訊mode3  FORM_INF
    Function GetSetInf4 : Boolean; //取系統設定資訊mode4  CHECK_RULE_INF
    Function GetSetInf5 : Boolean; //取系統設定資訊mode5  MEMO_INF
    Function GetSetInf6 : Boolean; //取系統設定資訊mode6  WORK_INF
    Function GetSetInf7 : Boolean; //取系統設定資訊mode7  LASTESTFORM_INF
    Procedure SetFormID_DocNo; //將FormID及Docno抽出來另存入list裡
    Procedure SetIn_WH_DocNo; //將要入庫的DocNo抽出來另存入list裡
    Procedure DataLoading(Loading,UseTimer:Boolean);  //資料載入中要停止點選的動作
    procedure ClearView(stkv:Integer); //清除瀏覽窗的影像
    Function DrawDocItem2(CaseNode : TTreenode;Caseno:String):Boolean;  //畫出文件名稱的Tree
    Procedure initkscan; //檢查掃描器的功能
    procedure LoadImgFile; //載入案件
    procedure LoadImgFile1; //載入案件
    procedure LoadAttFile(CaseID:String); //載入附加檔案
    Procedure DistinctFormCode(CaseID:String); //案件裡的FormCode取出第一頁
    Function OMRCheckCase(CaseID:String):Boolean; //OMR檢核
    Procedure OMRErr2ini(CaseID,Reason,FileName,Site,RelaFileName,RelaSite,Anchor,Anchor1:String;Del,Ingnore,Display:Boolean); //OMR檢核失敗寫入ini
    Procedure OMRErrini2List(CaseID:String;ErrlistForm : TErrlistForm); //OMR檢核失敗從ini寫入ListView
    Function DownLanguage:Boolean;  //下載多國語言檔
    Function FindMpsView(Vmode:Integer):TImageScrollBox;
    Function CaseAsk(CaseID:String):Integer;  //詢問是否可上傳  (-1:失敗;0:可以;1:不行;)
    Function CaseComplete(Path,CaseID:String;MainCase:Boolean):Boolean;  //通知傳送完成
    Function GetCaseFormID(Path:String):String;  //取案件的FormID
    Procedure CreateFormID_FormName(Path,CaseID:String);  //產生FormID_FormName.dat
    Procedure CreateDocNo_DocName(Path,CaseID:String);  //產生DocNo_DocName.dat
    Procedure CreateIn_WH(CaseID:String);  //產生In_WH.dat
    Function CreateDocNo_Info(CaseID:String):String; //產生保管袋文件 DocNo[tab]份數[tab]總頁數[tab]是否異動[換行]DocNo[tab]份數[tab]總頁數[tab]是否異動
    Function CreateCustDocNo_Info(CaseID:String):String; //產生自訂文件 DocName[tab]份數[tab]總頁數[tab]是否異動[#13#10]DocName[tab]份數[tab]總頁數[tab]是否異動
    //Function CreateCustDocNo_Info(path,CaseID:String):String; overload
    Function CreateAttach_Info(CaseID:String):String; //產生是否有Attach Y:有 N:沒有
    Function CreateDocnoFrom_Info(CaseID:String):String; //產生被引進的保管袋文件資訊  Docno[tab]份數[tab]案件編號#13#10Docno[tab]份數[tab]案件編號
    Function CreateCustDocNoFrom_Info(CaseID:String):String; //產生被引進的自定文件資訊  Docno[tab]份數[tab]案件編號#13#10Docno[tab]份數[tab]案件編號
    Function GetDocNoEdit(CaseID,DocNo,DocName:String):String; //取出DocNo是否被異動 (Y/N)
    Function GetDocNo_Count(Path,DocNo:String):Integer;  //取出文件份數
    Function GetDocNo_Page(Path,DocNo:String):Integer;  //取出文件總頁數
    Function FormIDExists(FormCode:String;CheckDate:Boolean;index:Integer):Boolean;  //檢查FormID是否存在及是否要檢查啟用停用日期
    Function Case_DocNoExists(CaseID,Docno:String):Boolean; //Docno是否存在案件裡
    Procedure ReSortFileName_New(Path:String); //檔名重新排序
    Procedure ReSortFileName(Path:String); //檔名重新排序
    Procedure ReSortFileName2Scanlist(Path:String); //檔名重新排序給Scanlist.dat
    Function GetOMRCheckSet:Boolean;  //下載OMR檢核XML檔
    Function GetKeyinSet : Boolean; //取登打設定
    Procedure CheckRule2OMRErrInfo;   //檢核規則填入OMRErrINFo Record
    Procedure ReNameContext(Path,OldName,NewName:String);
    Procedure DeleteImageFile(Path,FileName,CaseID:String); // 刪除檔案
    Procedure DeleteFormCodeFile(CaseID,DocDir,FormID:String);  //刪除指定FormID文件
    Function DeleteDocNoFile(Path,DocNo:String):Boolean;  //刪除指定DocNo文件
    Procedure DeleteShowFile(Path:String); //刪除顯示中的影像
    Function GetDataDocNoPage(MainDocNo,MainVersion:String):Integer;  //取記錄的文件_版本頁數
    Function CheckCaseDocNoPage(CaseID,DocNo,Version:String;Pages:Integer):Integer; //檢查案件裡的文件_版本頁數
    Function FindFormCodePages(CaseID,FormCode:String):Integer;  //計算案件裡FormID的頁數
    Function GetDataFormCodePages(FormCode:String):Integer;   //取記錄的FormcID的頁數
    Procedure CaseReSize(CaseID:String); //案件的影像縮放
    Procedure ImageReSize_FormID(CaseID,FileName:String);  //依十字定位點做縮放
    Procedure ImageReSize_tmp(FormID,FileName:String);  //依十字定位點做縮放(暫存檔)
    Function TransCaseID(Path,CaseID:String;MainCase:Boolean):Boolean; //傳送案件
    Procedure NewTreeNodeRefresh;
    Procedure MyTreeNode1Refresh;
    Procedure MyTreeNode2ReFresh(CaseID:String);
    Procedure MyTreeNode3ReFresh(CaseID:String);
    Function Node2DocNo(Node2:TTreeNode):String;  //MyTreeNode2取DocNo出來
    Function Node3DocNo(Node3:TTreeNode):String;  //MyTreeNode3取DocNo出來
    Function Node3FormID(Node3:TTreeNode):String;  //MyTreeNode3取FormCode出來
    Function GetNode2Name(Node2:TTreeNode):String;  //取MyTreeNode2的識別字出來(記之前點選用)
    //Function Down_Replace_Img(SPAth,DPath,CaseID:String):Boolean;
    Function DownLoadImage(Path,CaseID:String):Boolean;
    Function Down_Img(Path,CaseID:String):Boolean;
    Function GetNoNameCase(Path:string):String; //取未配號XXXX
    Procedure CaseResort(Path:String); //案件的檔案重新排序(次文件依Docno挑)
    Procedure CaseResort2Scanlist(Path:String); //案件的檔案重新排序給scanlist(次文件依FormID排)
    Procedure DistinctDocinCase(Path:String); //列出案件裡的Docno_版本
    Procedure DistinctDocNoinCase(Path:String); //列出案件裡的Docno
    Procedure ClearErrini(CaseID:String;CaseNode:TTreeNode); //清掉檢核檔案
    Procedure SetCaseList(Mode:Char;Index:Integer;text:String);  //'A:加入,I:插入,D:刪除,E:修改'
    Procedure SetDocNoList(Mode:Char;Index:Integer;CaseNo,DocDir,Copies:String);  //'A:加入,I:插入,D:刪除,E:修改'
    Procedure SetContextList(Mode:Char;Index:Integer;CaseNo,DocDir,FileName:String);  //'A:加入,I:插入,D:刪除,E:修改'
    Procedure SetAttContextList(Mode:Char;Index:Integer;CaseNo,FileName:String);  //'A:加入,I:插入,D:刪除,E:修改'
    Function checkCaseOMRDone:Boolean;  //檢查案件是否完成OMR檢核
    Function CheckCaseID_OK:Boolean;  //檢查是否有未配號的案件
    Procedure CreateEmptyCase(Path,CaseID:String);  //產生空白案號(重掃件用)
    Procedure InitScrollRec; //初始化影像Scroll記錄
    Procedure GetScrollData(ISB:TImageScrollBox;Var HS,VS:Integer;Var iRate:Single); //取影像Scroll記錄
    Procedure SetScrollData(ISB:TImageScrollBox;HS,VS:Integer;iRate:Single); //寫影像Scroll記錄
    Procedure FormIDReplace(CaseID,DocDir,OldFormID,NewFormID:String); //指定FormID更換成新的FormID
    Procedure ShowFileReplace(Path,NewFormID:String);//顯示的影像換成新的FormID
    Procedure PageReplaceFormID(Path,NowFormID,NewFormID:String); //選取頁更換FormID
    Function ModeNeedCheck(OMRMode,ScanMode:String):Boolean; //掃瞄模式是否要做檢核
    procedure WMMOUSEWHEEL(var message: TWMMouseWheel); message WM_MOUSEWHEEL;
    Function GetCasePage(Path,CaseID:String):Integer;
    Function GetFormIDPage(FileList:TStringlist;FormID:String):Integer;
    Procedure SetFile2Case(CaseID,FileName:String);
    Procedure WriteResize(ImgName,TxtName:String); //產生Resize.dat
    Function GetCase_PageCount(var CaseCount,PageCount:Integer):Boolean; //取出案件的數量及頁數 ID為空值時為取所有的
    Function BarCode2FormID : String; //Barcode依規則轉成FormID
    Function BarCode2CaseID : String; //Barcode依規則轉成CaseID
    Procedure WriteCaseIndex(Path:String);
    Procedure ReadCaseIndex(Path:String);
    Procedure ClearCaseIndex;
    Procedure GetSelectImageFile;
    Function GetDocNoDir(Path,DocNo:String):String; //取出目前DocNo的份數
    Function CheckFormIDExists(DocNoNode:TTreeNode;FormID:String):Boolean; //檢查FormID是否存在文件裡
    Procedure ZipMainFile(SoPath,DePath,ZipName:String); //壓縮影像檔
    Procedure ZipMaskFile(SoPath,MarkPath,DePath,ZipName:String); //壓縮遮罩影像檔
    Procedure ParserPoint(S:String); //解析十字點的字串
    Function CheckScanDenialTime:Boolean;
    Function FormID2Anchor(FormID:String):String;  //用FormID取出十字模式
    Function Index2Anchor(Anchor:String):String;   //十字模式 0->NONE;1->ANCHOR;2->FRAME
    Function MemoInfoTransfer(Mode,Str:String;ID_S,Name_S:TStringlist):String;  //註記代碼註記類別轉換  Mode 'ID':代碼轉名稱;'NAME':名稱轉代碼
    Function GetFormatID(CaseID:String):String; //取出案件的FormatID
    Function FindNoSaveBarCode : Boolean; //找是否有不要儲存影像的條碼
    Function CheckAvailable:Boolean; //檢查是否可使用元件
    Function Case2Mask(SoPath,DePath:String):Boolean;//產生遮罩影像
    Function CheckNeedCrop(Graphic:TDibGraphic):Boolean; //是否是A3要切影像
    Function GetNewCustomDocNo(Path,DocName:String):String; //取出未使用的自訂文件代號
    Function GetCustomDocName(Path,DocNo:String):String; //取出自定文件名稱
    Function GetCustomFormID(Path,DocNo:String):String; //取出自定文件FormID
    Function GetCustomDocDir(Path,DocName:String):String; //取出自定文件DocDir
    Function FindCustomDocName(Path,DocName:String):Boolean; //尋找自定文件名稱是否存在
    Procedure DeleteCustomDocDir(Path,DocNo:String); //刪除自定文件DocNo
    Function CheckFormID_Prt(FormID:String):Boolean; //傳入的FormID是否預設列印
    procedure PrintImg(FileName, LoginID, Datetime,Path: WideString);
    Function FindLastestDocDir(CaseID,DocNo:String):String; //找出最新的DocDir
    Procedure Create_Cust_DocDir(CaseID:String); //產生外面傳入的文件代號及自定文件
    Procedure OldCasetoNewCase(CaseID:String); //將舊案份數轉成新規則
    Procedure ErrFormtoCurrentForm(CaseID,EFormID,CFormID:String);//將舊案的錯誤FormID改正確的FormID
    Procedure SetRecordEditedDocDir(Mode:Char;CaseID,DocDir:String);  //記錄被異動的文件目錄  'A:加入D:刪掉'
    Function GetDocDir_Page(CaseID,DocDir:String):Integer;  //取得DocDir的頁數
    Function Path2DocDir(Path,CaseID:String):String;
    Function GetDocNo_IS_WH(DocNo:String):Boolean; //DocNo是否為入庫文件
    Procedure SortDocDir_FormID(CaseID,DocDir:String); //將DocDir裡的文件編號排序
    Procedure GotoAttach(OldLevel:Integer);
    Function DocNoIs_In_WH(DocNo:String):Boolean; //DocNo是否為入庫文件
    Procedure CreateCaseNeedData(Path:String);  //先做影像截取會少二個文字檔,產生CaseDocNo.dat及DocDir.dat
    Procedure SetDocDirtoSelected(CaseNode:TTreeNode;DocDir:String);
    Function CheckSelectImg_UseCase(Path,CaseID:String):Boolean; //檢查選擇的影像是否有包含被引用的影像
    Function TransOldCaseFile(Path:String):Boolean;  //上傳引用舊件的記錄檔
    Function Writelog(CaseID:String):Boolean;
    Function FormIDAppear(FormID:String):Boolean; //FormID是否可出現
    Function DocNoAppear(DocNo:String):Boolean;   //DocNo是否可出現
    Function GetDocNoCount(CaseID,DocNo:String):Integer; //取DocNo數量
    Function GetDocDirCopies(CaseID,DocDir:String):Integer; //取DocDir份數
    Procedure SetDocDirCopies(CaseID,DocDir:String;NewCopies:Integer); //修改DocDir份數
    Function GetDocDirCopies_Rec(Path,CaseID,DocDir:String):Integer; //取記錄裡的DocDir份數
    Function GetCustomNameCount(CustomName:String):Integer;   //取外傳的名稱數量
    Function GetCustomDocNoCount(Docno:String):Integer;   //取外傳的DocNo數量
    Function ISGuideFormID(FormID:String):Boolean;
    Function CaseDelete_Enable(CaseID:String):Boolean;  //案件可否被刪除
    Procedure MoveImage(Path:String;mp:Integer); //移動頁數
    Procedure MoveImage_Drag(Path:String;fp,tp:Integer); //拖拉移動頁數
    Procedure SetUseCase(Mode:Char;Path,DocDir,FormCaseID,ToCaseID:String);   //記錄引用其他案件 A:加入 D:刪掉
    Function GetUseCase(Mode:Char;Path,DocDir:String):String;  //F:取被引用 To:引用
    Procedure Case2upload(CaseID:String);
    Procedure Download2Case(SoDir,DeDir:String);
    procedure view_image_FormCode(Path,FormCode:String;stpage,stview:integer); //用FormCode來找影像
    procedure view_image_DocNo(Path,DocNo,FormID:String;Pages:integer); //用DocNo來找影像
    Function ShapeName2PreViewISBName(SP:TShape):String; //轉出指定PreViewISBName
    Procedure CreatePreViewISB(Count:Integer);
    Procedure FreePreViewISB;
    Procedure FitPreViewISB;
    Procedure PaintShape(FromImg,ToImg:TImageScrollBox); //畫有被選取的影像
    Procedure FreeShapeobj(SelectISB : TImageScrollBox);
    Procedure ISBClick(Sender : TObject);
    Procedure ISBMouseMove(Sender: TObject; Shift: TShiftState;
    X, Y: Integer);
    procedure ISBImageMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure ISBImageMouseUp(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure ISBEndDrag(Sender, Target: TObject; X, Y: Integer);
    procedure ISBDragDrop(Sender, Source: TObject; X, Y: Integer);
    procedure ISBDragOver(Sender, Source: TObject; X, Y: Integer;
      State: TDragState; var Accept: Boolean);
    procedure BtnMouseEnter(Sender: TObject);
    procedure PageEnd;   //掃描接收完成
    Procedure PageDone;  //掃描完成後顯示影像
    procedure StatrTwainScan;
    procedure OnAcquire( const DibHandle    : THandle;
                         const XDpi         : Word;
                         const YDpi         : Word;
                         const CallBackData : LongInt );
    procedure ActivateEvent(Sender: TObject);
    procedure ClickEvent(Sender: TObject);
    procedure CreateEvent(Sender: TObject);
    procedure DblClickEvent(Sender: TObject);
    procedure DeactivateEvent(Sender: TObject);
    procedure DestroyEvent(Sender: TObject);
    procedure KeyPressEvent(Sender: TObject; var Key: Char);
    procedure MouseEnterEvent(Sender: TObject);
    procedure MouseLeaveEvent(Sender: TObject);
    procedure PaintEvent(Sender: TObject);
    function GetCurrentVersionNo: String;
    procedure initParameter;
    procedure LastInitFormidListCreate(path:string);
    function checkFormCodeIsCustom(path,formcode:string):boolean;
    function ISDivPageFormID(FormID: String): Boolean;
    function GetSampleInf: Boolean;
    procedure InitExistImgList(casepath:String);
    function LoadFileGetMD5(const filename:string):string; //20170809 取的檔案的MD5
    function ISExistImg(const filename:string):boolean;  //20170809 確認是否存在原有影像
    procedure _DelTreeForExistImg(ASourceDir:String);   //2017 刪除前確認 有舊影像嗎
    function DocNoIsExistImg(DocNopath: String): boolean; //2017 刪除前確認 有舊影像嗎
    function CheckCaseAttach_OK: Boolean;
    function DeleteDocNoFileForESCAN(Path, DocNo: String): Boolean;     //2017 確認是否有未歸類文件
    function CheckRequiredColumnValues(workno,caseno:String) :Boolean;  //20171003  此大類下此案是否檢核必填
    function logTimeString :String;
    procedure ReduceLogFile ;
    function FindLastestDocDirForPage(CaseID, DocNo, formid: String): String;
    function OMRErrini2ListForLog(CaseID: String):String;
  protected
    { Protected declarations }
    procedure DefinePropertyPages(DefinePropertyPage: TDefinePropertyPage); override;
    procedure EventSinkChanged(const EventSink: IUnknown); override;
    function Get_Active: WordBool; safecall;
    function Get_AlignDisabled: WordBool; safecall;
    function Get_AlignWithMargins: WordBool; safecall;
    function Get_AutoScroll: WordBool; safecall;
    function Get_AutoSize: WordBool; safecall;
    function Get_AxBorderStyle: TxActiveFormBorderStyle; safecall;
    function Get_Caption: WideString; safecall;
    function Get_Color: OLE_COLOR; safecall;
    function Get_DockSite: WordBool; safecall;
    function Get_DoubleBuffered: WordBool; safecall;
    function Get_DropTarget: WordBool; safecall;
    function Get_Enabled: WordBool; safecall;
    function Get_ExplicitHeight: Integer; safecall;
    function Get_ExplicitLeft: Integer; safecall;
    function Get_ExplicitTop: Integer; safecall;
    function Get_ExplicitWidth: Integer; safecall;
    function Get_Font: IFontDisp; safecall;
    function Get_HelpFile: WideString; safecall;
    function Get_KeyPreview: WordBool; safecall;
    function Get_MouseInClient: WordBool; safecall;
    function Get_ParentCustomHint: WordBool; safecall;
    function Get_ParentDoubleBuffered: WordBool; safecall;
    function Get_PixelsPerInch: Integer; safecall;
    function Get_PopupMode: TxPopupMode; safecall;
    function Get_PrintScale: TxPrintScale; safecall;
    function Get_Scaled: WordBool; safecall;
    function Get_ScreenSnap: WordBool; safecall;
    function Get_SnapBuffer: Integer; safecall;
    function Get_UseDockManager: WordBool; safecall;
    function Get_Visible: WordBool; safecall;
    function Get_VisibleDockClientCount: Integer; safecall;
    procedure _Set_Font(var Value: IFontDisp); safecall;
    procedure Set_AlignWithMargins(Value: WordBool); safecall;
    procedure Set_AutoScroll(Value: WordBool); safecall;
    procedure Set_AutoSize(Value: WordBool); safecall;
    procedure Set_AxBorderStyle(Value: TxActiveFormBorderStyle); safecall;
    procedure Set_Caption(const Value: WideString); safecall;
    procedure Set_Color(Value: OLE_COLOR); safecall;
    procedure Set_DockSite(Value: WordBool); safecall;
    procedure Set_DoubleBuffered(Value: WordBool); safecall;
    procedure Set_DropTarget(Value: WordBool); safecall;
    procedure Set_Enabled(Value: WordBool); safecall;
    procedure Set_Font(const Value: IFontDisp); safecall;
    procedure Set_HelpFile(const Value: WideString); safecall;
    procedure Set_KeyPreview(Value: WordBool); safecall;
    procedure Set_ParentCustomHint(Value: WordBool); safecall;
    procedure Set_ParentDoubleBuffered(Value: WordBool); safecall;
    procedure Set_PixelsPerInch(Value: Integer); safecall;
    procedure Set_PopupMode(Value: TxPopupMode); safecall;
    procedure Set_PrintScale(Value: TxPrintScale); safecall;
    procedure Set_Scaled(Value: WordBool); safecall;
    procedure Set_ScreenSnap(Value: WordBool); safecall;
    procedure Set_SnapBuffer(Value: Integer); safecall;
    procedure Set_UseDockManager(Value: WordBool); safecall;
    procedure Set_Visible(Value: WordBool); safecall;
    procedure Set_caseid(const Value: WideString); safecall;
    procedure Set_data(const Value: WideString); safecall;
    procedure Set_mode(const Value: WideString); safecall;
    procedure Set_rewrite(const Value: WideString); safecall;
    procedure Set_url(const Value: WideString); safecall;
    procedure Set_userid(const Value: WideString); safecall;
    procedure Set_username(const Value: WideString); safecall;
    procedure Set_verify(const Value: WideString); safecall;
    procedure Set_language(const Value: WideString); safecall;
    procedure Set_modename(const Value: WideString); safecall;
    procedure Set_userunit(const Value: WideString); safecall;
    procedure Set_work_no(const Value: WideString); safecall;
    procedure Set_loandoc_enable(const Value: WideString); safecall;
    procedure Set_loandoc_value(const Value: WideString); safecall;
    procedure Set_useproxy(const Value: WideString); safecall;
    procedure Set_c_docnamelist(const Value: WideString); safecall;
    procedure Set_c_docnolist(const Value: WideString); safecall;
    procedure Set_fixfilelist(const Value: WideString); safecall;
    procedure Set_is_in_wh(const Value: WideString); safecall;
    procedure Set_oldcaseinfo(const Value: WideString); safecall;
    function Get_c_docnamelist: WideString; safecall;
    function Get_c_docnolist: WideString; safecall;
    function Get_caseid: WideString; safecall;
    function Get_data: WideString; safecall;
    function Get_fixfilelist: WideString; safecall;
    function Get_is_in_wh: WideString; safecall;
    function Get_language: WideString; safecall;
    function Get_loandoc_enable: WideString; safecall;
    function Get_loandoc_value: WideString; safecall;
    function Get_mode: WideString; safecall;
    function Get_modename: WideString; safecall;
    function Get_oldcaseinfo: WideString; safecall;
    function Get_rewrite: WideString; safecall;
    function Get_url: WideString; safecall;
    function Get_useproxy: WideString; safecall;
    function Get_userid: WideString; safecall;
    function Get_username: WideString; safecall;
    function Get_userunit: WideString; safecall;
    function Get_verify: WideString; safecall;
    function Get_work_no: WideString; safecall;
    function Get_printyn: WideString; safecall;
    procedure Set_printyn(const Value: WideString); safecall;
    function Get_is_oldcase: WideString; safecall;
    procedure Set_is_oldcase(const Value: WideString); safecall;
    function Get_custdocyn: WideString; safecall;
    procedure Set_custdocyn(const Value: WideString); safecall;
    function Get_casenolength: WideString; safecall;
    function Get_filesizelimit: WideString; safecall;
    function Get_imgdpi: WideString; safecall;
    function Get_scancolor: WideString; safecall;
    procedure Set_casenolength(const Value: WideString); safecall;
    procedure Set_filesizelimit(const Value: WideString); safecall;
    procedure Set_imgdpi(const Value: WideString); safecall;
    procedure Set_scancolor(const Value: WideString); safecall;
    function Get_imgdelete: WideString; safecall;
    procedure Set_imgdelete(const Value: WideString); safecall;
    function Get_check_main_form: WideString; safecall;
    function Get_isExternal: WideString; safecall;
    procedure Set_check_main_form(const Value: WideString); safecall;
    procedure Set_isExternal(const Value: WideString); safecall;
    function Get_WH_CATEGORY: WideString; safecall;
    procedure Set_WH_CATEGORY(const Value: WideString); safecall;
  public
    { Public declarations }
    procedure Initialize; override;
  end;
implementation
uses EnBarcode,
     EnTransf, { for TImageTransform }
     Enpnggr,  { for PNGGraphic }
     EnJpgGr,  { for JPGGraphic }
     EnReg,
     EnBmpGr,  { for TBitmapGraphic }
     EnPrint,  { for TEnvisionPrintMode, TDibGraphicPrinter }
     ComObj, ComServ,IISUnit,IIS_File2Web,IIS_ImageProcess,
     PatchFom,Doclist,ScanMemo,DocCopy,InputMask,SortMemo,DocPrt,OldCaseInfo;
{$R *.DFM}
{ TCB_IMGPSScanX }
{$I CB_IMGPSScanImp_UI.pas}{$I CB_IMGPSScanImp_Scan.pas}{$I CB_IMGPSScanImp_Data.pas}{$I CB_IMGPSScanImp_Utils.pas}initialization
  TActiveFormFactory.Create(
    ComServer,
    TActiveFormControl,
    TCB_IMGPSScanX,
    Class_CB_IMGPSScanX,
    1,
    '',
    OLEMISC_SIMPLEFRAME or OLEMISC_ACTSLIKELABEL,
    tmApartment);
  {SetLicenseKey('5B4451E676A1D2976FBB0F3BB18341336AF114C80B5ABAE7F6926B1CAF671F44' +
  'BD2F098CCEDA922F6389BFAE398DA6AEE67F97EEA0C17234C20D75C12173DBDA' +
  '594924D56DD8E342F454389C836AD880BB4352CA3BE62C4933B1BA3828E7462C' +
  '60514F2ECDAD322E6128D841F12D24DA00B623106D3F08EBCAA917D8A97CAA34' +
  '3D65F2DA567316457395BF9123EE53DF235D181F191A5712DBB27735284AA92D' +
  '5DFA0C8308308505F384707E900C6063F53F1BFF4C6972607955D1AE517B19D0' +
  '82CDD16301885403AD229D57BAEF98C056F31430861E5F68F339D658D72E1F92' +
  '63899412EC2D07891FE3AFD35F3E4A4390B2F0A8A1BF1B7D6160E5F1CC009B17'); }
  SetLicenseKey('4B2CF65E8C2A86CE8A0DD0F6A7DB03BC0B0126168B48AE4C27EBD78CAE75CF0F' +
   'A612190861E0D99F6FAE3ED97AC1941B5E97843CFFCF705A3787989072D4EB2C' +
   'AE6CAB3F5B69B86616ACC8A37AD6A2AB21C7BDD5C9AE1EDF9E4193D353805C9A' +
   '403631CE8A3D0803FEBB1BE4C209CE7A63B1298EF080EB34B8628CED567D2B68' +
   'E777FAC58E2E32B7411FC217A04336231D1E861A93474759DAA6EDF53F6EB632' +
   'A3055229A52F3053FB844754741409022DDE3DFB19473510F2BE63328E74BE20' +
   'A6A29AA24878F91ADA9DF8CE1F320AF4DAF58EBF95D9BE761D70EEA274E19475' +
   '1C15948B184264C5C49E60493F3BCD2FFAE2CA8B021D00B96F45550C5F050D8A');
  SetLicenseKey('A6A94A8D91B08A9D58F300C0573EA9EF1B9DB0BF69B90E13B958DB4CB6B44F5A' +
  '4EE9CB22C9A68C2D07ED52ED4D13C755D890E4074996755361E6CDE2A6F1B563' +
  '5DDC8999AC4D71FB092EA9F1F87BFA25694FBF0D6D250087D2B39629713FCCB0' +
  'D0A83135BC14FC63A4E8331CFF9E24C45C2D9CFD837EB70BAFDB79A75B7B97D5' +
  'E9EB271685118C29D90A7C85E7793908989E295DA50021C795A448366026E975' +
  'F49EA75B721B80427B99E5CF24A225FB498C07946ED7B806B483654C00D85C66' +
  'E34215CA3EDEF1D4C3F5896090E97E1E2C9752BA2D5B49EE58CF19A0D374077F' +
  '6D13B90B6FED22D9EBC3AD6CDC76E595E08725BF2E12B8EF30A524A2E00504DF');
end.
separate/scanImp/CB_IMGPSScanImp_Main.ts
比對新檔案
@@ -0,0 +1,17 @@
import { applyDataMixins } from './CB_IMGPSScanImp_Data';
import { applyScanMixins } from './CB_IMGPSScanImp_Scan';
import { applyUIMixins } from './CB_IMGPSScanImp_UI';
import { applyUtilsMixins, TStringList, TImageFormat, TTransMode, TFtpProtocol } from './CB_IMGPSScanImp_Utils';
export class TCB_IMGPSScanX {
  Ch_WriteNote=false; RejectCase=false; ErrIndex=0; Def_DeviceDelete=true; Def_DeviceDeleteSize=3072; Def_ScannerReverse=false; Def_BoardClear=false; Def_ScanDpi=300; Def_ScanDuplex=true; Def_ScanRotate=0; Def_ScanDeskew=false; Def_ScanBright=0; Def_ScanContrast=0; Def_ScanImgShowMode=2; Def_ScanImgSetUse=false;
  FUrl=''; FCaseID=''; FMode=''; FModeName=''; FWork_no=''; FUserID=''; FUserName=''; FUserUnit=''; FData=''; FVerify=''; FReWrite=''; FLanguage='zh_tw'; FLoanDoc_Value=''; FLoanDoc_Enable=''; FUseProxy=''; FC_DocNoList=''; FC_DocNameList=''; FFixFileList=''; FIs_In_Wh=''; FOldCaseInfo=''; FPrintyn=''; FIs_OldCase=''; FCustDocYN=''; FImgDPI=300; FScanColor=0; FFileSizeLimit=5120; FCaseNoLength=16; FImgDelete='N'; FIsExternal='N'; FCheck_main_form='N'; FWH_category='N';
  TransMode=TTransMode.tsNone; HttpErrStr=''; FFtpIP=''; FFtpID=''; FFtpPwd=''; FFtpRootPath=''; FFtpExtraPath=''; FFtpPort=0; FFtpProtocol=TFtpProtocol.fpftp; DeviceDelete=false; DeviceDeleteSize=0; ScannerReverse=false; BoardClear=false; ScanDpi=0; ScanDuplex=false; ScanRotate=0; ScanDeskew=false; ScanBright=0; ScanContrast=0; ScanImgShowMode=0; ScanImgSetUse=false; ScanColor:TImageFormat='ifBlackWhite';
  ServerDate=''; ServerTime=''; Balance=0; ScanDenialTime=''; ScanDenialHint=''; ScanPath=''; ImagePath=''; ImageSavePath=''; ScaniniPath=''; LngPath=''; CheckXmlPath=''; SitePath=''; SamplePath=''; TransPath=''; ScanSaveFilename=''; ScanCaseno=''; ScanDocDir='';
  Doc_Inf_List=new TStringList(); DM_FORM_INF_List=new TStringList(); FORM_INF_List=new TStringList(); CHECK_RULE_INF_List=new TStringList(); MEMO_INF_List=new TStringList(); WORK_INF_List=new TStringList(); LASTEST_FORM_INF_List=new TStringList(); FindResult=new TStringList(); CaseList=new TStringList(); Context_DocnoList=new TStringList(); CaseDocNoList=new TStringList(); CaseDocNo_CopiesList=new TStringList(); ContextList=new TStringList(); AttContextList=new TStringList(); OMRFileList=new TStringList(); Cust_DocNoList=new TStringList(); IN_WH_DocNoList=new TStringList(); FormCode_PageSize=new TStringList(); DocNo_NeedDoc=new TStringList(); DocNo_NoDoc=new TStringList(); DocNo_VerinCase=new TStringList(); NoSaveBarCodeList=new TStringList(); FormID_List=new TStringList(); DocNo_List=new TStringList(); NowShowFileList=new TStringList(); NowSelectFileList=new TStringList(); GuideFormIDList=new TStringList(); DivPageFormIDList=new TStringList(); LastInitFormidList=new TStringList(); LastAddFormidList=new TStringList(); SampleFormIDList=new TStringList(); ExistImgList=new TStringList(); reSizeExistImgList=new TStringList();
  NowCaseno=''; NowDocNo=''; NowDocDir=''; NowFormCode=''; NowFormName=''; NowPage=0; DisplayPath=''; Case_loandoc=''; UpLPoint={X:0,Y:0}; UpRPoint={X:0,Y:0}; DownLPoint={X:0,Y:0}; DownRPoint={X:0,Y:0}; Point_Width=''; Point_Height=''; CaseIDLength=0; FormIDLength=0; DocNoLength=0; PEFileName=''; DownFileErrStr='';
  ISB:any=null; ScanInfo:any={}; TwainShowUI=false; MpsBarcodeinf:any={}; ScanMode:any=null; Mpskey=''; Seg=0; VMode=0; NowClick=0; ScanIP=''; DisplayISB:any=null; SelectISB:any=null; SelectPage=0; NewTreeNode:any=null; MyTreeNode1:any=null; MyTreeNode2:any=null; MyTreeNode3:any=null; InitialOk=false; ShowText=''; Ext=''; SafePixel=0; OMRErrInfo:any[]=[]; ScrollRec:any[]=[]; RecHozPos=0; RecVerPos=0; ReczoomPercent=0; SortMode=false; PreMytreeNode2Name=''; HS=0; VS=0; iRate=0; Bt=0; BarCodeRotate=0; HaveAppDoc=false; PageLVclear=false; CaseCount=0; PageCount=0; Item:any=null; SampleAnchorMode=''; DownImgStatus=''; TransForm_Field=''; NowWork_No=''; CropBarcode=''; Has_Authorize=''; AttName=''; NowGuideFormID=''; NowDivPageFormID=''; FirstDocDir=''; FMaxUploadSize=''; FJpgCompression=0; Draging=false; MDown=false; FEvents:any=null;
  [key: string]: any;
  constructor(){ console.log("TCB_IMGPSScanX Initialized"); }
}
applyDataMixins(TCB_IMGPSScanX); applyScanMixins(TCB_IMGPSScanX); applyUIMixins(TCB_IMGPSScanX); applyUtilsMixins(TCB_IMGPSScanX);
separate/scanImp/CB_IMGPSScanImp_Scan.pas
比對新檔案
@@ -0,0 +1,1050 @@
function TCB_IMGPSScanX.Get_Color: OLE_COLOR;
begin
  Result := OLE_COLOR(Color);
end;
procedure TCB_IMGPSScanX.Set_Color(Value: OLE_COLOR);
begin
  Color := TColor(Value);
end;
procedure TCB_IMGPSScanX.StatrTwainScan;
var ScanInfo    : TScanInfo;
   i : Integer;
begin
  if not Scanner.IsConfigured then
  begin
    ShowMessage(_Msg('TWAIN 掃瞄驅動尚未安裝'));
    Exit;
  end;
  FillChar(ScanInfo, SizeOf(ScanInfo), 0);
  ScanInfo.MultiPage := True;
  ScanInfo.ImageCount := 0;
  ScanInfo.Graphic := TTiffGraphic.Create;
  try
    ISB := nil; //規零
//ShowMessage(IntToStr(ScanDpi));
    Scanner.RequestedXDpi := ScanDpi;
    Scanner.RequestedYDpi := ScanDpi;
    Scanner.RequestedImageFormat := ScanColor;
    Scanner.ShowUI := TwainShowUI;
    Try
      Scanner.OpenSource;
      Scanner.Duplex := ScanDuplex; //雙面
      if FMode='SAMPLESCAN' then
        Scanner.Duplex:=False;
      //Scanner.FEEDERENABLED  := not ScanFlatCB.Checked;     // 先拿掉平台
      If ScanImgSetUse Then
      begin
        Scanner.ScanBrightness := ScanBright;
        Scanner.ScanContrast := ScanContrast;
      end;
    except
      Showmessage(_Msg('掃瞄器發生錯誤!!'));
      Scanner.CloseSource;
      Exit;
    end;
    try
      Try
        Scanner.AcquireWithSourceOpen( OnAcquire, LongInt(@ScanInfo));
      Except
        Scanner.CloseSource;
      end;
    finally
      Scanner.CloseSource;
    end;
  finally
  Scanner.CloseSource;
  ScanInfo.Graphic.Free;
  end;
end;
procedure TCB_IMGPSScanX.OnAcquire( const DibHandle    : THandle;
                               const XDpi         : Word;
                               const YDpi         : Word;
                               const CallBackData : LongInt );
var
  pScanInfo  : TpScanInfo;
  SaveFileName : String;
  SaveStream     : TFileStream;
  strResults : TBarcodeStringResult;
  DeleteStm  : TMemoryStream;
  isDelete   : Boolean;
  iGraphic,iGraphic_First,iGraphic_sec : TTiffGraphic;
  iRect : TRect;
  JpgGr : TJpegGraphic;
  i : Integer;
  TagTxt : String;
  function Deletepage(Graphic:TDibGraphic;BlankSize:Integer):Boolean;
  begin
    DeleteStm  := TMemoryStream.Create;
    DeleteStm.Seek(0,soFromBeginning);
    Graphic.AppendToStream(DeleteStm);
    //DeleteStm.LoadFromFile(Path+'temp.tif');
    //Isb1.Graphic.SaveToStream(DeleteStm);
    //Isb1.Graphic.AppendToStream(DeleteStm);
    if DeleteStm.Size < BlankSize Then
      Result:= True
    Else
      Result := False;
    DeleteStm.Free;
    //DeleteFile(Path+'temp.tif');
  end;
begin
  pScanInfo := TpScanInfo(CallBackData);
  isDelete    := False;
  if pScanInfo^.MultiPage then
  begin
    pScanInfo^.Graphic.AssignFromDibHandle(DibHandle);
    pScanInfo^.Graphic.XDotsPerInch := XDpi;
    pScanInfo^.Graphic.YDotsPerInch := YDpi;
    TagTxt := 'height:'+inttostr(pScanInfo^.Graphic.Height)+',width:'+inttostr(pScanInfo^.Graphic.Width);
    if pScanInfo^.Graphic.ImageFormat = ifBlackWhite then
    begin
      ImageScrollBox1.Graphic.Assign(pScanInfo^.Graphic);
      ImageScrollBox1NewGraphic(ImageScrollBox1.Graphic);
      pScanInfo^.Graphic.Compression := tcGroup4;
      MpsGetBarcode(pScanInfo^.Graphic,MpsBarcodeinf);
      For i := 1 to MpsBarcodeinf.count do
      begin
        If (MpsBarcodeinf.r180[i] <> 0) and (Length(MpsBarcodeinf.Text[i])=FormIDLength) Then
        begin
          Rotate(pScanInfo^.Graphic,MpsBarcodeinf.r180[i]);
          MpsGetBarcode(pScanInfo^.Graphic,MpsBarcodeinf);  //旋轉後再重取一次條碼資訊
          Break;
        end;
      end;
      //影像反向
      IF ScannerReverse then
        NegativeImg(pScanInfo^.Graphic);
      //傾斜矯正
      IF ScanDeskew Then
        DeskewImg(pScanInfo^.Graphic);
      //清黑邊
      IF BoardClear then
        CleanupBorder(pScanInfo^.Graphic);
    end
    else if pScanInfo^.Graphic.ImageFormat = ifTrueColor then
    begin
        //Ext := '.jpg';
        ImageScrollBox1.Graphic.Assign(pScanInfo^.Graphic);
        //ImageScrollBox1NewGraphic(ImageScrollBox1.Graphic);
        MpsGetBarcode(ISB_BW.Graphic,MpsBarcodeinf);
        For i := 1 to MpsBarcodeinf.count do
        begin
          If (MpsBarcodeinf.r180[i] <> 0) and (Length(MpsBarcodeinf.Text[i])=FormIDLength) Then
          begin
            Rotate(ISB_BW.Graphic,MpsBarcodeinf.r180[i]);
            MpsGetBarcode(ISB_BW.Graphic,MpsBarcodeinf);  //旋轉後再重取一次條碼資訊
            Break;
          end;
        end;
        pScanInfo^.Graphic.Compression := tcJpeg;
        pScanInfo^.Graphic.JpegQuality := FJpgCompression;
    end
    else if pScanInfo^.Graphic.ImageFormat = ifColor256 Then
    begin
      //Ext := '.jpg';
      ConvertToGray(pScanInfo^.Graphic);
      pScanInfo^.Graphic.Compression := tcJpeg;
      pScanInfo^.Graphic.JpegQuality := FJpgCompression;
    end
    else if pScanInfo^.Graphic.ImageFormat = ifGray256 Then
    begin
      //Ext := '.jpg';
      ImageScrollBox1.Graphic.Assign(pScanInfo^.Graphic);
      ImageScrollBox1NewGraphic(ImageScrollBox1.Graphic);
      MpsGetBarcode(ISB_BW.Graphic,MpsBarcodeinf);
      For i := 1 to MpsBarcodeinf.count do
      begin
        If (MpsBarcodeinf.r180[i] <> 0) and (Length(MpsBarcodeinf.Text[i])=FormIDLength) Then
        begin
          Rotate(ISB_BW.Graphic,MpsBarcodeinf.r180[i]);
          MpsGetBarcode(ISB_BW.Graphic,MpsBarcodeinf);  //旋轉後再重取一次條碼資訊
          Break;
        end;
      end;
      pScanInfo^.Graphic.Compression := tcJpeg;
      pScanInfo^.Graphic.JpegQuality := FJpgCompression;
//ShowMessage(IntToStr(pScanInfo^.Graphic.JpegQuality));
//if pScanInfo^.Graphic.Compression = tcJpeg then
//begin
//ShowMessage('jpg');
//end;
    end
    else
    begin
      //Ext := '.tif';
      pScanInfo^.Graphic.Compression := tcPackBits;
    end;
  end;
  //Application.ProcessMessages;
  iGraphic_First := TTiffGraphic.Create;
  iGraphic_sec := TTiffGraphic.Create;
  //iGraphic := TTiffGraphic.Create;
  try
    iGraphic_First.Assign(pScanInfo^.Graphic);
    //Application.ProcessMessages;
    if CheckNeedCrop(iGraphic_First) Then
    begin
      iRect.Left := pScanInfo^.Graphic.Width div 2;    //先取左邊的影像
      iRect.Top := 0;
      iRect.Right := pScanInfo^.Graphic.Width;
      iRect.Bottom := pScanInfo^.Graphic.Height;
      CropImg(iGraphic_First,iRect);
      iGraphic_Sec.Assign(pScanInfo^.Graphic);         //再取右邊的影像
      iRect.Left := 0;
      iRect.Top := 0;
      iRect.Right := pScanInfo^.Graphic.Width div 2;
      iRect.Bottom := pScanInfo^.Graphic.Height;
      CropImg(iGraphic_Sec,iRect);
    end;
    //iGraphic.Assign(iGraphic_First);
    iGraphic := iGraphic_First;
    if iGraphic.ImageFormat=ifGray256 then  //20180104
    begin
      iGraphic.Compression:=tcJPEG;
      iGraphic.JpegQuality:=FJpgCompression;
    end;
    if iGraphic.ImageFormat=ifTrueColor then  //20180104
    begin
      iGraphic.Compression:=tcJPEG;
      iGraphic.JpegQuality:=FJpgCompression;
    end;
//ShowMessage('WTF');
    while not iGraphic.IsEmpty do
    begin
      //Application.ProcessMessages;
      IF (not DeviceDelete) or (not Deletepage(iGraphic,DeviceDeleteSize)) Then
      begin
        ImageScrollBox1.Graphic.Assign(iGraphic);
        ImageScrollBox1NewGraphic(ImageScrollBox1.Graphic);
        MpsGetBarcode(ISB_BW.Graphic,MpsBarcodeinf);
        For i := 1 To MpsBarcodeinf.Count Do
        Begin
          If MpsBarcodeinf.r180[i] <> 0 Then // 依條碼角度轉影像
          Begin
            Rotate(iGraphic, MpsBarcodeinf.r180[i]);
            Break;
          End;
        End;
        if iGraphic.ImageFormat=ifGray256 then  //20180104 因此旋轉後變為回packbits 所以要改為jpeg
        begin
          iGraphic.Compression:=tcJPEG;
          iGraphic.JpegQuality:=FJpgCompression;
        end;
        if iGraphic.ImageFormat=ifTrueColor then
        begin
          iGraphic.Compression:=tcJPEG;
          iGraphic.JpegQuality:=FJpgCompression;
        end;
        PageEnd;
        IF PEFileName <> '' Then
        begin
          IF LowerCase(ExtractFileExt(PEFileName)) = '.tif' Then
          begin
            if FileExists( PEFileName ) then
              SaveStream := TFileStream.Create( PEFileName ,fmOpenReadWrite)
            Else
              SaveStream := TFileStream.Create( PEFileName ,fmCreate );
            try
              SaveStream.Seek(0, soFromBeginning);
              iGraphic.AppendToStream(SaveStream);
            finally
              SaveStream.Free;
            end;
          end
          Else IF LowerCase(ExtractFileExt(PEFileName)) = '.jpg' Then
          begin
            if FileExists( PEFileName ) then
              DeleteFile(PEFileName);
            //SaveStream := TFileStream.Create( PEFileName ,fmCreate );
            JpgGr := TJpegGraphic.Create;
            try
              JpgGr.Assign(iGraphic);
              JpgGr.SaveQuality := FJpgCompression;
              //JpgGr.AppendToStream(SaveStream);
              JpgGr.SaveToFile(PEFileName);
            finally
              JpgGr.Free;
              //SaveStream.Free;
            end;
          end;
          PageDone;
        end;
      end;
      if iGraphic = iGraphic_First then
        iGraphic := iGraphic_Sec
      else
        iGraphic.Assign(nil);
      //iGraphic.Assign(iGraphic_Sec);
    end;
  finally
  //iGraphic.Free;
  iGraphic_First.Free;
  iGraphic_Sec.Free;
  end;
end;
Procedure TCB_IMGPSScanX.R_W_Scanini(Mode:Char); //'R'讀取;'W'寫入
var
  ini : Tinifile;
begin
  ini := Tinifile.Create(ScaniniPath+'FBScan.ini');
  try
    case Mode of
    'R':begin
          DeviceDelete := ini.ReadBool('DeviceDelete','Mode',Def_DeviceDelete);
          DeviceDeleteSize := ini.ReadInteger('DeviceDelete','Size_New',Def_DeviceDeleteSize);
          ScannerReverse := ini.ReadBool('Scanner','Reverse',Def_ScannerReverse);
          BoardClear := ini.ReadBool('Scanner','BoardClear',Def_BoardClear);
          //ScanDpi := ini.ReadInteger('Scanner','Dpi',Def_ScanDpi);
          //ScanDuplex := ini.ReadBool('Scanner','Duplex',Def_ScanDuplex);
          ScanRotate := ini.ReadInteger('Scanner','ScanRotate',Def_ScanRotate);
          ScanDeskew := ini.ReadBool('Scanner','ScanDeskew',Def_ScanDeskew);
          ScanBright := ini.ReadInteger('Scanner','ScanBright',Def_ScanBright);
          ScanContrast := ini.ReadInteger('Scanner','ScanContrast',Def_ScanContrast);
          ScanImgShowMode := ini.ReadInteger('Scanner','ScanImgShowMode',Def_ScanImgShowMode);
          ScanImgSetUse := ini.ReadBool('Scanner','ScanImgSetUse',Def_ScanImgSetUse);  //20101110 BA說掃瞄器廠商有調設定要新增此選項是否啟動
        end;
    'W':begin
          ini.WriteBool('DeviceDelete','Mode',DeviceDelete);
          ini.WriteInteger('DeviceDelete','Size_New',DeviceDeleteSize);
          ini.WriteBool('Scanner','Reverse',ScannerReverse);
          ini.WriteBool('Scanner','BoardClear',BoardClear);
          //ini.ReadInteger('Scanner','Dpi',ScanDpi);
          //ini.WriteBool('Scanner','Duplex',ScanDuplex);
          ini.WriteInteger('Scanner','ScanRotate',ScanRotate);
          ini.WriteBool('Scanner','ScanDeskew',ScanDeskew);
          ini.WriteInteger('Scanner','ScanBright',ScanBright);
          ini.WriteInteger('Scanner','ScanContrast',ScanContrast);
          ini.WriteInteger('Scanner','ScanImgShowMode',ScanImgShowMode);
          ini.WriteBool('Scanner','ScanImgSetUse',ScanImgSetUse);  //20101110 BA說掃瞄器廠商有調設定要新增此選項是否啟動
        end;
    end;
  finally
  ini.Free;
  end;
end;
Procedure TCB_IMGPSScanX.GetDefScanIni; //取得掃瞄的預設值
var
  i : Integer;
  PARA_NO,PARA_CONTENT: String;
begin
  Def_DeviceDelete := True;
  Def_DeviceDeleteSize := 3072;  //20120821 改成3000(出現)
  Def_ScannerReverse := False;
  Def_BoardClear := False;
  Def_ScanDpi := 300;
  Def_ScanDuplex := True;
  Def_ScanRotate := 0;
  Def_ScanDeskew := False;
  Def_ScanImgSetUse := False;
  Def_ScanBright := 0;
  Def_ScanContrast := 0;
  Def_ScanImgShowMode := 2;
  for i := 0 to WORK_INF_List.Count - 1 do
  begin
    IF GetSQLData(WORK_INF_List,'PARA_NO',i) = 'SCAN_BLANKDEL_USE' Then   //空白頁啟動
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      if UpperCase(PARA_CONTENT) ='Y'  then
        Def_DeviceDelete := True
      Else
        Def_DeviceDelete := False;
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'SCAN_BLANKDEL_SIZE' Then  //空白頁Size
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      if PARA_CONTENT = ''  then
        Def_DeviceDeleteSize := 0
      Else
        Def_DeviceDeleteSize := Strtoint(PARA_CONTENT);
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'SCAN_REVERSE' Then  //是否需反相
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      if UpperCase(PARA_CONTENT) ='Y'  then
        Def_ScannerReverse := True
      Else
        Def_ScannerReverse := False;
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'SCAN_BOARDCLEAR' Then  //是否清黑邊
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      if UpperCase(PARA_CONTENT) ='Y'  then
        Def_BoardClear := True
      Else
        Def_BoardClear := False;
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'SCAN_DPI' Then  //掃瞄DPI
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      if PARA_CONTENT = ''  then
        Def_ScanDpi := 300
      else
        Def_ScanDpi := Strtoint(PARA_CONTENT);
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'SCAN_DUPLEX' Then  //是否雙面掃瞄
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      if UpperCase(PARA_CONTENT) ='Y'  then
        Def_ScanDuplex := True
      Else
        Def_ScanDuplex := False;
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'SCAN_ROTATE_MODE' Then //掃瞄時旋轉角度
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      if PARA_CONTENT = '0' then
        Def_ScanRotate := 0
      Else if PARA_CONTENT = '1' then
        Def_ScanRotate := 270
      Else if PARA_CONTENT = '2' then
        Def_ScanRotate := 180
      Else if PARA_CONTENT = '3' then
        Def_ScanRotate := 90
      Else
        Def_ScanRotate := 0;
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'SCAN_DESKEW' Then //是否傾斜矯正
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      if UpperCase(PARA_CONTENT) ='Y'  then
        Def_ScanDeskew := True
      Else
        Def_ScanDeskew := False;
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'SCAN_IMGSET_USE' Then  //是否使用亮度對比設定
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      if UpperCase(PARA_CONTENT) ='Y'  then
        Def_ScanImgSetUse := True
      else
        Def_ScanImgSetUse := False;
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'SCAN_BRIGHT' Then //亮度
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      if PARA_CONTENT =''  then
        Def_ScanBright := 0
      Else
        Def_ScanBright := strtoint(PARA_CONTENT);
      if (Def_ScanBright > 255) or (Def_ScanBright < -255) then
        Def_ScanBright := 0;
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'SCAN_CONTRAST' Then //對比
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      if PARA_CONTENT =''  then
        Def_ScanContrast := 0
      Else
        Def_ScanContrast := strtoint(PARA_CONTENT);
      if (Def_ScanContrast > 255) or (Def_ScanContrast < -255) then
        Def_ScanContrast := 0;
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'SCAN_SHOW_MODE' Then
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      if PARA_CONTENT = '0' then
        Def_ScanImgShowMode := 0
      Else if PARA_CONTENT = '1' then
        Def_ScanImgShowMode := 1
      Else if PARA_CONTENT = '2' then
        Def_ScanImgShowMode := 2
      Else
        Def_ScanImgShowMode := 0;
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'CASE_IN_TIME' Then     //取進件截止時間
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      ScanDenialTime := PARA_CONTENT;
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'SCAN_HINT' Then     //掃描提示字串
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      ScanDenialHint := PARA_CONTENT;
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'NO_SAVE_FORM_ID' Then     //掃描不存檔之表單代號
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      NoSaveBarCodeList.CommaText := PARA_CONTENT;
    end
    Else if GetSQLData(WORK_INF_List,'PARA_NO',i) = 'LOCAL_PATH' Then     //本機端路徑
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      ImagePath:= PARA_CONTENT;
    end
    Else if UpperCase(GetSQLData(WORK_INF_List,'PARA_NO',i)) = 'GUIDEFORMID' Then     //當導引頁的表單
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      GuideFormIDList.CommaText := PARA_CONTENT;
    end
    Else if UpperCase(GetSQLData(WORK_INF_List,'PARA_NO',i)) = 'DIVPAGEFORMID' Then     //當分案頁的表單
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      DivPageFormIDList.CommaText := PARA_CONTENT;
    end
    Else if UpperCase(GetSQLData(WORK_INF_List,'PARA_NO',i)) = 'FILE_COMPRESSION' Then     //20171211 jpg to tif 壓縮比
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      FJpgCompression := StrToInt(PARA_CONTENT);
    end
    Else if UpperCase(GetSQLData(WORK_INF_List,'PARA_NO',i)) = 'MAX_UPLOAD_SIZE' Then     //取得 上傳大小的限制(MB)
    begin
      PARA_CONTENT := GetSQLData(WORK_INF_List,'PARA_CONTENT',i);
      FMaxUploadSize := PARA_CONTENT;
    end;
  end;
  ScanDuplex := Def_ScanDuplex;
end;
procedure TCB_IMGPSScanX.EnableImage(v:integer;Sender : TObject);
var bmp : Tbitmap;
begin
  DesableImage;
  bmp := TBitmap.Create;
  try
    ImageList3.GetBitmap(v,bmp);
    TBitBtn(Sender).Glyph.Assign(bmp);
  finally
  bmp.Free;
  end;
  ViewMouseMode(v);
end;
procedure TCB_IMGPSScanX.DesableImage;
var bmp : Tbitmap;
    i : integer;
begin
  NowClick := -1;
  bmp := Tbitmap.Create;
  try
  For i:= 0 to 6 do
  begin
    ImageList4.GetBitmap(i,bmp);
    TBitBtn(FindComponent('FC'+IntToStr(i))).Glyph.Assign(bmp);
    bmp.Width:=0;
    bmp.Handle:=0;
  end;
  finally
  bmp.Free;
  end;
  ViewMouseMode(NowClick);
end;
Procedure TCB_IMGPSScanX.DeleteImageFile(Path,FileName,CaseID:String); // 刪除檔案  (無法得到DocDir用)
var
  i : Integer;
  FileList:TStringlist;
  DocDir : String;
begin
  DeleteFile(Path+FileName);
  DocDir := Path2DocDir(Path,CaseID);
//ShowMessage('DocDir='+DocDir);
  SetContextList('D',-1,CaseID,DocDir,FileName);
  {FileList:=TStringlist.Create;
  try
    if FileExists(Path+'Context.dat') then
      FileList.LoadFromFile(Path+'Context.dat');
    for i := 0 to FileList.Count - 1 do
    begin
      if FileName = FileList.Strings[i] then
      begin
        FileList.Delete(i);
        FileList.SaveToFile(Path+'Context.dat');
        Break;
      end;
    end;
    if FileList.Count = 0 then
      DeleteFile(Path+'Context.dat');
  finally
  FileList.Free;
  end;}
end;
Function TCB_IMGPSScanX.DeleteDocNoFileForESCAN(Path,DocNo:String):Boolean;  //刪除指定DocNo文件
var
  i,j,k: Integer;
  FName : String;
  ST1,ST2,ST3:TStringList;
begin
  Result := False;
//ShowMessage(DocNo);
  for i := ContextList.Count - 1 downto 0 do
  begin
    FName := ContextList.Strings[i];
    If (DocNo = FormCode2DocNo(FileName2FormCode(FName))) or (DocNo=AttName) then
    begin
      if not ISExistImg(Path+'\'+FName) then
      begin
        DeleteFile(Path+'\'+FName);
        ContextList.Delete(i);
      end;
      Result := True; //有刪到指定文件
    end;
  end;
  ContextList.SaveToFile(Path+'\Context.dat');
  ContextList.LoadFromFile(Path+'\Context.dat');
  if ContextList.Count=0 then
  begin
    _DelTree(Path);
    SetDocNoList('D',-1,NowCaseNo,NowDocDir,'');
  end;
end;
Function TCB_IMGPSScanX.DownLoadImage(Path,CaseID:String):Boolean;
begin
  Result := True;
  if not GetftpInfo(CaseID,'download') then   //取案件下載方式
  begin
    DownFileErrStr := _Msg('取案件下載資訊失敗,')+HttpErrStr;
    Result := False;
    Exit;
  end;
  case TransMode of
    tsHttp:
    begin
      ShowText := _Msg('案件下載中(Http),請稍候');
      DataLoading(True,True);
      If not Down_Img(ImageSavePath+FCaseID+'\Download\',FCaseID) then
      begin
        Showmessage(FCaseID+_msg('載入異動影像時,網路發生錯誤')+HttpErrStr);
        DataLoading(False,False);
        Exit;
      end;
    end;
    tsFtp:
    begin
      ShowText := _Msg('案件下載中(Ftp),請稍候');
      DataLoading(True,True);
      SetFtpInfo;
      if not IIS_Ftp.FtpsConnect then
      begin
        DownFileErrStr := Format(_Msg('無法連上Ftp主機,錯誤原因:%s')+#13+'%s',[FtpErrReason,FTPSClient1.LastReceivedReply]);
        Result := False;
        Exit;
      end;
      if not IIS_Ftp.FtpsDownloadFile(FFtpExtraPath,CaseID+'.zip',Path+CaseID+'.zip',display1) then
      begin
        DownFileErrStr := Format(_Msg('錯誤原因:%s'),[FtpErrStr]);
        Result := False;
        Exit;
      end;
      ExecuteUnZip(Path+CaseID+'.zip',Path,False);
      DeleteFile(Path+CaseID+'.zip');
    end;
  end;
end;
Procedure TCB_IMGPSScanX.CaseResort2Scanlist(Path:String); //案件的檔案重新排序給scanlist(次文件依FormID排)
var
  i,n,v,v1 : Integer;
  S,S1 : TStringlist;
  FormID,OldName,NewName,DocNo,Doc_Type:String;
  x : Integer;
begin
  S := TStringlist.Create;
  S1 := TStringlist.Create;
  try
  if FileExists(Path+'Context.dat') then
    S.LoadFromFile(Path+'Context.dat');
  X := 0;
  for I := 1 to FORM_INF_List.Count - 1 do    //在FormID有設定的   //主文件 照SQL排   20101028改
  begin
    FormID := GetSQLData(FORM_INF_List,'T1.FORM_ID',i);
    if FormCode2FileName(FormID,S) = '' then
       Continue;
    Doc_Type := GetSQLData(FORM_INF_List,'T2.DOC_TYPE',i);
    for n := 0 to S.Count - 1 do
    begin
      if (S.Strings[n][1] <> '*') and (FileName2FormCode(S.Strings[n]) = FormID) and (Doc_Type='1') then
      begin
        Inc(X);
        OldName := S.Strings[n];
        //NewName := Add_Zoo(S.Count+x,3)+Copy(OldName,4,length(OldName)-3); //從原有數量加1開始編
        NewName := Add_Zoo(S.Count+x,3)+FileName2NoQuene_Filename(OldName); //從原有數量加1開始編
        S.Strings[n] := '*'+S.Strings[n];
        S1.Add(OldName+','+NewName);
      end;
    end;
  end;
  for I := 0 to FORM_INF_List.Count - 1 do  //次文件 照SQL排   20110512為了某個文件要先打的原因要求改
  begin
    FormID := GetSQLData(FORM_INF_List,'T1.FORM_ID',i);
    if FormCode2FileName(FormID,S) = '' then
       Continue;
    Doc_Type := GetSQLData(FORM_INF_List,'T2.DOC_TYPE',i);
    for n := 0 to S.Count - 1 do
    begin
      if (S.Strings[n][1] <> '*') and (FileName2FormCode(S.Strings[n]) = FormID) and (Doc_Type='2') then
      begin
        Inc(X);
        OldName := S.Strings[n];
        //NewName := Add_Zoo(S.Count+x,3)+Copy(OldName,4,length(OldName)-3); //從原有數量加1開始編
        NewName := Add_Zoo(S.Count+x,3)+FileName2NoQuene_Filename(OldName); //從原有數量加1開始編
        S.Strings[n] := '*'+S.Strings[n];
        S1.Add(OldName+','+NewName);
      end;
    end;
  end;
  {for I := 0 to Doc_Inf_List.Count - 1 do  //次文件 照文件代碼+掃瞄順序排   20101101改   20110512晚上又說改回來
  begin
    DocNo := GetSQLData(Doc_Inf_List,'DOC_NO',i);
    Doc_Type := GetSQLData(Doc_Inf_List,'DOC_TYPE',i);
    for n := 0 to S.Count - 1 do
    begin
      if (S.Strings[n][1] <> '*') and (FormCode2DocNo(FileName2FormCode(S.Strings[n])) = DocNo) and (Doc_Type='2') then
      begin
        Inc(X);
        OldName := S.Strings[n];
        NewName := Add_Zoo(S.Count+x,3)+Copy(OldName,4,length(OldName)-3); //從原有數量加1開始編
        S.Strings[n] := '*'+S.Strings[n];
        S1.Add(OldName+','+NewName);
      end;
    end;
  end;}
  {for n := 0 to S.Count - 1 do    //次文件 照掃瞄順序排   20101028改
  begin
    for i := 0 to FORM_INF_List.Count - 1 do
    begin
      FormID := GetSQLData(FORM_INF_List,'T1.FORM_ID',i);
      Doc_Type := GetSQLData(FORM_INF_List,'T2.DOC_TYPE',i);
      if (S.Strings[n][1] <> '*') and (FileName2FormCode(S.Strings[n]) = FormID) and (Doc_Type='2') then
      begin
        Inc(X);
        OldName := S.Strings[n];
        NewName := Add_Zoo(S.Count+x,3)+Copy(OldName,4,length(OldName)-3); //從原有數量加1開始編
        S.Strings[n] := '*'+S.Strings[n];
        S1.Add(OldName+','+NewName);
      end;
    end;
  end;}
  for i := 0 to S.Count - 1 do   //FormID沒設定的或附件
  begin
    if S.Strings[i][1] <> '*' then
    begin
      Inc(X);
      OldName := S.Strings[i];
      //NewName := Add_Zoo(S.Count+x,3)+Copy(OldName,4,length(OldName)-3);
      NewName := Add_Zoo(S.Count+x,3)+FileName2NoQuene_Filename(OldName);
      S.Strings[i] := '*'+S.Strings[i];
      S1.Add(OldName+','+NewName);
    end;
  end;
  S.Clear;
  for i := 0 to S1.Count - 1 do  //開始轉換檔名
  begin
    v := Pos(',',S1.Strings[i]);
    v1 := length(S1.Strings[i]);
    OldName := copy(S1.Strings[i],1,v-1);
    NewName := copy(S1.Strings[i],v+1,v1-v);
    //if FileExists(Path+OldName) then
    //begin
      //ReNameFile(Path+OldName,Path+NewName);
      S.Add(NewName);
      S.SaveToFile(Path+'scanlist.dat');
    //end;
  end;
  ReSortFileName2Scanlist(Path);
  finally
  S.Free;
  S1.Free;
  end;
end;
Procedure TCB_IMGPSScanX.GetSelectImageFile;
var
  i : Integer;
  FormID,FormName,DocNo : String;
  PreNode2Name : String;
  iFormID : String;
  iISBName : String;
  iISB : TImageScrollBox;
begin
  NowSelectFileList.Clear;
  for i := 0 to ComponentCount -1 do
  begin
    if (Components[i] is TShape) and (copy(Components[i].Name,1,2)='SP') then
    begin
      iISBName := ShapeName2PreViewISBName(TShape(Components[i]));
      iISB := TImageScrollBox(FindComponent(iISBName));
      NowSelectFileList.Add(iISB.FileName);
    end;
  end;
end;
Procedure TCB_IMGPSScanX.ParserPoint(S:String); //解析十字點的字串
var
  PointList : TStringlist;
  Rect : TRect;
begin
  PointList := TStringlist.Create;
  try
    PointList.Text := S;
    IF PointList.Count <> 6 Then
    begin
      UpLPoint := Str2Point('0,0');
      UpRPoint := Str2Point('0,0');
      DownLPoint := Str2Point('0,0');
      DownRPoint := Str2Point('0,0');
      Point_Width := '0';
      Point_Height := '0';
    end
    Else
    begin
      UpLPoint := Str2Point(PointList[0]);
      DownLPoint := Str2Point(PointList[1]);
      UpRPoint := Str2Point(PointList[2]);
      DownRPoint := Str2Point(PointList[3]);
      Point_Width := PointList[4];
      Point_Height := PointList[5];
    end;
  finally
  PointList.Free;
  end;
end;
Function TCB_IMGPSScanX.CheckScanDenialTime:Boolean;
Var
  NowTime : String;
begin
  NowTime := GetBalance2Time(Balance);
  NowTime := Copy(NowTime,1,2)+':'+Copy(NowTime,3,2)+':'+Copy(NowTime,5,2);
  Result := True;
  if ScanDenialTime <> '' then
  begin
    if StrtoTime(NowTime) >= StrtoTime(ScanDenialTime) then
      Result := False;
  end;
end;
procedure TCB_IMGPSScanX.initkscan;
begin
  ScanDuplexCB.Enabled := False;
  if Scanner.IsConfigured then
  begin
    try
      Scanner.OpenSource;
      IF Scanner.DuplexCap > 0 Then
      begin
        ScanDuplexCB.Enabled := True;
      end;
      {IF Scanner.FEEDERCAP Then
        ScanFlatCB.Enabled := True; }
    Except
      DataLoading(False,True);
      Exit;
    end;
    Scanner.CloseSource;
  end;
end;
Function TCB_IMGPSScanX.CheckNeedCrop(Graphic:TDibGraphic):Boolean; //是否是A3要切影像
Var
  i,FormIDCount : Integer;
begin
  Result := False;
  FormIDCount := 0;
  if (Graphic.Width > (4 * Graphic.XDotsPerInch)) {or (Graphic.Height > (15 * Graphic.YDotsPerInch))} then
  //if (Graphic.Width > (6 * Graphic.XDotsPerInch)) then
  begin
    for I := 1 to MpsBarcodeinf.Count do
    begin
      if (Length(MpsBarcodeinf.Text[i])=FormIDLength) and FormIDExists(MpsBarcodeinf.Text[i],False,0) then
      begin
        inc(FormIDCount);
      end;
    end;
  end;
//ShowMessage('FormIDCount='+IntToStr(FormIDCount)+#10#13+'MpsBarcodeinf.count='+IntToStr(MpsBarcodeinf.count));
  if FormIDCount = 2 then
  begin
    Result := True;
  end;
end;
Procedure TCB_IMGPSScanX.MoveImage(Path:String;mp:Integer); //移動頁數
var
  i,n,inx:Integer;
  FList,D_Flist:TStringlist;
begin
  FList := TStringlist.Create;
  D_Flist := TStringlist.Create;
  try
    FList.LoadFromFile(Path+'Context.dat');
    //Showmessage(Path);
    //Showmessage(Flist.Text);
    for i := 0 to FList.Count - 1 do
    begin
      Renamefile(Path+Flist.Strings[i],path+'@'+Flist.Strings[i]);
      Flist.Strings[i]:= '@'+Flist.Strings[i];
    end;
    for i := 0 to ComponentCount -1 do
    begin
      if (Components[i] is TShape) and (copy(Components[i].Name,1,2)='SP') then
      begin
        inx := strtoint(Copy(TShape(Components[i]).Name,3,length(TShape(Components[i]).Name)-2));
        D_Flist.Add(Flist.Strings[inx-1]);
        //Renamefile(Path+Flist.Strings[inx-1],path+'@'+Flist.Strings[inx-1]);
      end;
    end;
    //Showmessage('aa');
    for i := 0 to D_Flist.Count -1 do
    begin
      for n := 0 to FList.Count - 1 do
      begin
        //if Flist.Strings[n]=StringReplace(D_Flist.Strings[i],'@','',[rfReplaceAll]) then
        if Flist.Strings[n]=D_Flist.Strings[i] then
        begin
          Flist.Delete(n);
          Break;
        end;
      end;
    end;
    //Showmessage('bb');
    for i := 0 to D_Flist.Count - 1 do
    begin
      Flist.Insert(mp-1+i,D_Flist.Strings[i]);
    end;
    Flist.SaveToFile(Path+'Context.dat');
    //Showmessage(Flist.Text);
    //Showmessage('CC');
    ReSortFileName(Path);
    TreeView1click(self);
  finally
  FList.Free;
  D_Flist.Free;
  end;
end;
Function TCB_IMGPSScanX.FileName2ScanPage(FileName:String):Integer; //從檔名轉出掃瞄頁數
Var
  v : Integer;
  FName : String;
begin
  FName := ExtractFileName(FileName);
  v := Pos('_',FName);
  if v = 0 then   //附件
    v := pos('.',FName);
  Result := Strtoint(Copy(FName,1,v-1));
end;
Procedure TCB_IMGPSScanX.ReSortFileName2Scanlist(Path:String); //檔名重新排序給Scanlist.dat
var
  i : Integer;
  OldName,NewName : String;
  S : TStringlist;
begin
  S := TStringlist.Create;
  try
    if FileExists(Path+'scanlist.dat') then
      S.LoadFromFile(Path+'scanlist.dat');
    for i := 0 to S.Count - 1 do
    begin
      OldName := S.Strings[i];
      //NewName := Add_Zoo(i+1,3)+Copy(OldName,4,length(OldName)-3);
      NewName := Add_Zoo(i+1,3)+FileName2NoQuene_Filename(OldName);
      //ReNameFile(Path+OldName,Path+NewName);
      S.Strings[i] := NewName;
    end;
    S.SaveToFile(Path+'scanlist.dat');
  finally
  S.Free;
  end;
end;
function TCB_IMGPSScanX.Get_imgdpi: WideString;
begin
end;
function TCB_IMGPSScanX.Get_scancolor: WideString;
begin
end;
procedure TCB_IMGPSScanX.Set_imgdpi(const Value: WideString);
begin
  if Value ='' then
  begin
    FImgDPI := 300;
    ScanDpi := FImgDPI;
  end
  else
  begin
    FImgDPI := StrToInt(Value);
    ScanDpi := FImgDPI;
    Def_ScanDpi := FImgDPI;
  end;
end;
procedure TCB_IMGPSScanX.Set_scancolor(const Value: WideString);
begin
  if value='' then
  begin
    FScanColor := 0;
    ScanColor := ifBlackWhite;
  end
  else
  begin
    FScanColor := StrToInt(Value);
    ScanColor := ifBlackWhite;
  end;
  if FScanColor = 1 then
  begin
    ScanColor := ifGray256 ;
  end;
  if FScanColor = 2 then
  begin
    ScanColor := ifTrueColor ;
  end;
end;
separate/scanImp/CB_IMGPSScanImp_Scan.ts
比對新檔案
@@ -0,0 +1,124 @@
import * as fs from 'fs'; import * as path from 'path';
export function applyScanMixins(cls:any){
  cls.prototype.StatrTwainScan=function(){
    if(!this.Scanner.IsConfigured){this.ShowMessage(this._Msg('TWAIN 掃瞄驅動尚未安裝'));return;}
    let ScanInfo={MultiPage:true,ImageCount:0,Graphic:new this.TTiffGraphic()};
    try{this.ISB=null;this.Scanner.RequestedXDpi=this.ScanDpi;this.Scanner.RequestedYDpi=this.ScanDpi;this.Scanner.RequestedImageFormat=this.ScanColor;this.Scanner.ShowUI=this.TwainShowUI;
    try{this.Scanner.OpenSource();this.Scanner.Duplex=this.ScanDuplex;if(this.FMode==='SAMPLESCAN')this.Scanner.Duplex=false;if(this.ScanImgSetUse){this.Scanner.ScanBrightness=this.ScanBright;this.Scanner.ScanContrast=this.ScanContrast;}}catch(e){this.Showmessage(this._Msg('掃瞄器發生錯誤!!'));this.Scanner.CloseSource();return;}
    try{this.Scanner.AcquireWithSourceOpen(this.OnAcquire.bind(this),ScanInfo);}catch(e){this.Scanner.CloseSource();}}finally{this.Scanner.CloseSource();ScanInfo.Graphic.Free();}
  };
  cls.prototype.OnAcquire=function(DibHandle:any,XDpi:any,YDpi:any,CallBackData:any){ this.PageEnd(); this.PageDone(); };
  cls.prototype.PageDone=function(){
    this.ScanInfo.ImageCount++; let ISB;
    if(this.ScanMode==='smNew'){if(this.ScanImgShowMode===0||this.ScanImgShowMode===1){ISB=this.FindISB2View(this.VMode);ISB.AntiAliased=this.ScanImgShowMode===0;ISB.ZoomMode='zmFittopage';ISB.LoadFromFile(this.PEFileName,1);}}
    else if(this.ScanMode==='smReplace'){this.DisplayISB.LoadFromFile(this.PEFileName,1);}
    else if(this.ScanMode==='smInsert'||this.ScanMode==='smSample'){ISB=this.FindISB2View(this.VMode);ISB.ZoomMode='zmFittopage';ISB.LoadFromFile(this.PEFileName,1);}
  };
  cls.prototype.PageEnd=function(){
    if(this.ScanMode==='smNew'){
      this.ScanSaveFilename='';let FormID=this.BarCode2FormID();
      if(FormID!==''&&this.ISDivPageFormID(FormID))this.NowDivPageFormID=FormID;
      if(FormID!==''&&this.ISGuideFormID(FormID))this.NowGuideFormID=FormID;
      if(!this.FindDivFormCode(FormID)&&this.NowGuideFormID!=='')FormID=this.NowGuideFormID;
      let DocNo=this.FormCode2DocNo(FormID);
      if(FormID!==''&&this.FindDivFormCode(FormID)&&this.NowDivPageFormID!==''){
        this.ScanInfo.ImageCount=0;this.ClearView(1);this.ContextList.Clear();this.Context_DocnoList.Clear();this.ClearCaseIndex();this.ScanCaseno=this.BarCode2CaseID();this.NowGuideFormID='';this.NowDivPageFormID='';
      }
      if(this.ScanCaseno==='')this.ScanCaseno=this.GetNoNameCase(this.ImageSavePath);
      this.ImageSavePath=this.ImagePath;
      if(this.ScanInfo.ImageCount===0&&fs.existsSync(path.join(this.ImageSavePath,this.ScanCaseno))){this._DelTree(path.join(this.ImageSavePath,this.ScanCaseno));this.SetCaseList('D',-1,this.ScanCaseno);}
      this.ScanPath=path.join(this.ImageSavePath,this.ScanCaseno);this.Str2Dir(this.ScanPath);
      this.ScanDocDir=this.FindLastestDocDir(this.ScanCaseno,DocNo);
      if(this.DocNoNeedDiv(DocNo)){if((this.FormCode2Page(FormID)==='01'&&this.GetDocDir_Page(this.ScanCaseno,this.ScanDocDir)>0)||this.ScanDocDir==='')this.ScanDocDir=this.DocNo2DocNoDir(path.join(this.ImageSavePath,this.ScanCaseno),DocNo);}
      else{this.ScanDocDir=DocNo!==''?DocNo:this.DocNo2DocNoDir(path.join(this.ImageSavePath,this.ScanCaseno),DocNo);}
      if(this.FirstDocDir==='')this.FirstDocDir=this.ScanDocDir;
      this.ScanPath=path.join(this.ImageSavePath,this.ScanCaseno,this.ScanDocDir);
      if(!fs.existsSync(this.ScanPath)&&this.ScanDocDir!==this.AttName)this.SetDocNoList('A',-1,this.ScanCaseno,this.ScanDocDir,'1');
      this.Str2Dir(this.ScanPath);
      this.ScanSaveFilename=FormID===''?this.Add_Zoo(this.GetDocDir_Page(this.ScanCaseno,this.ScanDocDir)+1,3)+this.Ext:this.Add_Zoo(this.GetDocDir_Page(this.ScanCaseno,this.ScanDocDir)+1,3)+'_'+FormID+this.Ext;
      if(!this.FindNoSaveBarCode()){if(this.ScanInfo.ImageCount===0){this.SetCaseList('A',-1,this.ScanCaseno);this.WriteCaseIndex(path.join(this.ImageSavePath,this.ScanCaseno));}this.SetContextList('A',-1,this.ScanCaseno,this.ScanDocDir,this.ScanSaveFilename);this.PEFileName=path.join(this.ScanPath,this.ScanSaveFilename);}
    } else if(this.ScanMode==='smReplace') {
      if(this.ScanInfo.ImageCount===0){fs.unlinkSync(path.join(this.ScanPath,this.ScanSaveFilename));this.PEFileName=path.join(this.ScanPath,this.ScanSaveFilename);}
    } else if(this.ScanMode==='smInsert') {
      this.ScanSaveFilename='';let FormID=this.BarCode2FormID();if(FormID!==''&&this.ISGuideFormID(FormID))this.NowGuideFormID=FormID;if(this.NowGuideFormID!=='')FormID=this.NowGuideFormID;
      let DocNo=this.FormCode2DocNo(FormID);this.ScanDocDir=this.FindLastestDocDir(this.ScanCaseno,DocNo);
      if(this.FMode==='ESCAN'&&this.FModeName===this._Msg('補件掃描'))this.ScanDocDir=this.FindLastestDocDirForPage(this.ScanCaseno,DocNo,FormID);
      if(this.DocNoNeedDiv(DocNo)){if((this.FormCode2Page(FormID)==='01'&&this.GetDocDir_Page(this.ScanCaseno,this.ScanDocDir)>0)||this.ScanDocDir===''){this.ScanInfo.ImageCount=0;this.ScanDocDir=this.DocNo2DocNoDir(path.join(this.ImageSavePath,this.ScanCaseno),DocNo);}}
      else{this.ScanDocDir=DocNo!==''?DocNo:this.DocNo2DocNoDir(path.join(this.ImageSavePath,this.ScanCaseno),DocNo);}
      if(this.FirstDocDir==='')this.FirstDocDir=this.ScanDocDir;
      this.ScanPath=path.join(this.ImageSavePath,this.ScanCaseno,this.ScanDocDir);
      if(!fs.existsSync(this.ScanPath)&&this.ScanDocDir!=='Attach'&&this.ScanDocDir!=='S_Attach')this.SetDocNoList('A',-1,this.ScanCaseno,this.ScanDocDir,'1');
      this.Str2Dir(this.ScanPath);
      this.ScanSaveFilename=FormID===''?this.Add_Zoo(this.GetDocDir_Page(this.ScanCaseno,this.ScanDocDir)+1,3)+this.Ext:this.Add_Zoo(this.GetDocDir_Page(this.ScanCaseno,this.ScanDocDir)+1,3)+'_'+FormID+this.Ext;
      this.SetContextList('A',-1,this.ScanCaseno,this.ScanDocDir,this.ScanSaveFilename);this.PEFileName=path.join(this.ScanPath,this.ScanSaveFilename);
    } else if(this.ScanMode==='smSample') {
      if(this.ScanInfo.ImageCount===0){fs.unlinkSync(path.join(this.ScanPath,this.ScanSaveFilename));this.PEFileName=path.join(this.ScanPath,this.ScanSaveFilename);let BarStr='';for(let i=1;i<=this.MpsBarCodeinf.Count;i++)BarStr+=this.MpsBarCodeinf.Text[i];this.Showmessage(this._Msg('辨識到的BarCode:\n')+BarStr);}
    }
  };
  cls.prototype.R_W_Scanini=function(m:string){
    let ini=new this.Tinifile(path.join(this.ScaniniPath,'FBScan.ini'));
    if(m==='R'){this.DeviceDelete=ini.ReadBool('DeviceDelete','Mode',this.Def_DeviceDelete);this.DeviceDeleteSize=ini.ReadInteger('DeviceDelete','Size_New',this.Def_DeviceDeleteSize);this.ScannerReverse=ini.ReadBool('Scanner','Reverse',this.Def_ScannerReverse);this.BoardClear=ini.ReadBool('Scanner','BoardClear',this.Def_BoardClear);this.ScanRotate=ini.ReadInteger('Scanner','ScanRotate',this.Def_ScanRotate);this.ScanDeskew=ini.ReadBool('Scanner','ScanDeskew',this.Def_ScanDeskew);this.ScanBright=ini.ReadInteger('Scanner','ScanBright',this.Def_ScanBright);this.ScanContrast=ini.ReadInteger('Scanner','ScanContrast',this.Def_ScanContrast);this.ScanImgShowMode=ini.ReadInteger('Scanner','ScanImgShowMode',this.Def_ScanImgShowMode);this.ScanImgSetUse=ini.ReadBool('Scanner','ScanImgSetUse',this.Def_ScanImgSetUse);}
    else if(m==='W'){ini.WriteBool('DeviceDelete','Mode',this.DeviceDelete);ini.WriteInteger('DeviceDelete','Size_New',this.DeviceDeleteSize);ini.WriteBool('Scanner','Reverse',this.ScannerReverse);ini.WriteBool('Scanner','BoardClear',this.BoardClear);ini.WriteInteger('Scanner','ScanRotate',this.ScanRotate);ini.WriteBool('Scanner','ScanDeskew',this.ScanDeskew);ini.WriteInteger('Scanner','ScanBright',this.ScanBright);ini.WriteInteger('Scanner','ScanContrast',this.ScanContrast);ini.WriteInteger('Scanner','ScanImgShowMode',this.ScanImgShowMode);ini.WriteBool('Scanner','ScanImgSetUse',this.ScanImgSetUse);}
    ini.Free();
  };
  cls.prototype.GetDefScanIni=function(){
    this.Def_DeviceDelete=true;this.Def_DeviceDeleteSize=3072;this.Def_ScannerReverse=false;this.Def_BoardClear=false;this.Def_ScanDpi=300;this.Def_ScanDuplex=true;this.Def_ScanRotate=0;this.Def_ScanDeskew=false;this.Def_ScanImgSetUse=false;this.Def_ScanBright=0;this.Def_ScanContrast=0;this.Def_ScanImgShowMode=2;
    for(let i=0;i<this.WORK_INF_List.Count;i++){
      let pn=this.GetSQLData(this.WORK_INF_List,'PARA_NO',i),pc=this.GetSQLData(this.WORK_INF_List,'PARA_CONTENT',i);
      if(pn==='SCAN_BLANKDEL_USE')this.Def_DeviceDelete=pc.toUpperCase()==='Y';
      else if(pn==='SCAN_BLANKDEL_SIZE')this.Def_DeviceDeleteSize=pc===''?0:parseInt(pc);
      else if(pn==='SCAN_REVERSE')this.Def_ScannerReverse=pc.toUpperCase()==='Y';
      else if(pn==='SCAN_BOARDCLEAR')this.Def_BoardClear=pc.toUpperCase()==='Y';
      else if(pn==='SCAN_DPI')this.Def_ScanDpi=pc===''?300:parseInt(pc);
      else if(pn==='SCAN_DUPLEX')this.Def_ScanDuplex=pc.toUpperCase()==='Y';
      else if(pn==='SCAN_ROTATE_MODE')this.Def_ScanRotate=pc==='1'?270:pc==='2'?180:pc==='3'?90:0;
      else if(pn==='SCAN_DESKEW')this.Def_ScanDeskew=pc.toUpperCase()==='Y';
      else if(pn==='SCAN_IMGSET_USE')this.Def_ScanImgSetUse=pc.toUpperCase()==='Y';
      else if(pn==='SCAN_BRIGHT')this.Def_ScanBright=pc===''?0:parseInt(pc);
      else if(pn==='SCAN_CONTRAST')this.Def_ScanContrast=pc===''?0:parseInt(pc);
      else if(pn==='SCAN_SHOW_MODE')this.Def_ScanImgShowMode=pc==='0'?0:pc==='1'?1:2;
      else if(pn==='CASE_IN_TIME')this.ScanDenialTime=pc;
      else if(pn==='SCAN_HINT')this.ScanDenialHint=pc;
      else if(pn==='NO_SAVE_FORM_ID')this.NoSaveBarCodeList.CommaText=pc;
      else if(pn==='LOCAL_PATH')this.ImagePath=pc;
      else if(pn.toUpperCase()==='GUIDEFORMID')this.GuideFormIDList.CommaText=pc;
      else if(pn.toUpperCase()==='DIVPAGEFORMID')this.DivPageFormIDList.CommaText=pc;
      else if(pn.toUpperCase()==='FILE_COMPRESSION')this.FJpgCompression=parseInt(pc);
      else if(pn.toUpperCase()==='MAX_UPLOAD_SIZE')this.FMaxUploadSize=pc;
    }
    this.ScanDuplex=this.Def_ScanDuplex;
  };
  cls.prototype.EnableImage=function(v:number,s:any){this.DesableImage();let bmp=new this.TBitmap();this.ImageList3.GetBitmap(v,bmp);s.Glyph.Assign(bmp);bmp.Free();this.ViewMouseMode(v);};
  cls.prototype.DesableImage=function(){this.NowClick=-1;let bmp=new this.TBitmap();for(let i=0;i<=6;i++){this.ImageList4.GetBitmap(i,bmp);this.FindComponent('FC'+i).Glyph.Assign(bmp);bmp.Width=0;bmp.Handle=0;}bmp.Free();this.ViewMouseMode(this.NowClick);};
  cls.prototype.DeleteImageFile=function(p:string,f:string,c:string){if(fs.existsSync(path.join(p,f)))fs.unlinkSync(path.join(p,f));let d=this.Path2DocDir(p,c);this.SetContextList('D',-1,c,d,f);};
  cls.prototype.DeleteDocNoFileForESCAN=function(p:string,d:string){
    let r=false;
    for(let i=this.ContextList.Count-1;i>=0;i--){
      let f=this.ContextList.Strings[i];
      if(this.FormCode2DocNo(this.FileName2FormCode(f))===d||d===this.AttName){
        if(!this.ISExistImg(path.join(p,f))){fs.unlinkSync(path.join(p,f));this.ContextList.Delete(i);} r=true;
      }
    }
    this.ContextList.SaveToFile(path.join(p,'Context.dat'));this.ContextList.LoadFromFile(path.join(p,'Context.dat'));
    if(this.ContextList.Count===0){this._DelTree(p);this.SetDocNoList('D',-1,this.NowCaseNo,this.NowDocDir,'');} return r;
  };
  cls.prototype.DownLoadImage=function(p:string,c:string){return true;};
  cls.prototype.CaseResort2Scanlist=function(p:string){
    let S=new this.TStringlist(),S1=new this.TStringlist();if(fs.existsSync(path.join(p,'Context.dat')))S.LoadFromFile(path.join(p,'Context.dat'));
    let x=0;
    for(let i=1;i<this.FORM_INF_List.Count;i++){let fi=this.GetSQLData(this.FORM_INF_List,'T1.FORM_ID',i);if(this.FormCode2FileName(fi,S)==='')continue;let dt=this.GetSQLData(this.FORM_INF_List,'T2.DOC_TYPE',i);for(let n=0;n<S.Count;n++){if(S.Strings[n][0]!=='*'&&this.FileName2FormCode(S.Strings[n])===fi&&dt==='1'){x++;let on=S.Strings[n];let nn=this.Add_Zoo(S.Count+x,3)+this.FileName2NoQuene_Filename(on);S.Strings[n]='*'+S.Strings[n];S1.Add(`${on},${nn}`);}}}
    for(let i=0;i<this.FORM_INF_List.Count;i++){let fi=this.GetSQLData(this.FORM_INF_List,'T1.FORM_ID',i);if(this.FormCode2FileName(fi,S)==='')continue;let dt=this.GetSQLData(this.FORM_INF_List,'T2.DOC_TYPE',i);for(let n=0;n<S.Count;n++){if(S.Strings[n][0]!=='*'&&this.FileName2FormCode(S.Strings[n])===fi&&dt==='2'){x++;let on=S.Strings[n];let nn=this.Add_Zoo(S.Count+x,3)+this.FileName2NoQuene_Filename(on);S.Strings[n]='*'+S.Strings[n];S1.Add(`${on},${nn}`);}}}
    for(let i=0;i<S.Count;i++){if(S.Strings[i][0]!=='*'){x++;let on=S.Strings[i];let nn=this.Add_Zoo(S.Count+x,3)+this.FileName2NoQuene_Filename(on);S.Strings[i]='*'+S.Strings[i];S1.Add(`${on},${nn}`);}}
    S.Clear();for(let i=0;i<S1.Count;i++){let v=S1.Strings[i].indexOf(',');let nn=S1.Strings[i].substring(v+1);S.Add(nn);S.SaveToFile(path.join(p,'scanlist.dat'));}
    this.ReSortFileName2Scanlist(p);S.Free();S1.Free();
  };
  cls.prototype.GetSelectImageFile=function(){this.NowSelectFileList.Clear();for(let i=0;i<this.ComponentCount;i++){if(this.Components[i] instanceof this.TShape&&this.Components[i].Name.startsWith('SP')){let isn=this.ShapeName2PreViewISBName(this.Components[i]);let isb=this.FindComponent(isn);this.NowSelectFileList.Add(isb.FileName);}}};
  cls.prototype.ParserPoint=function(s:string){let p=new this.TStringlist();p.Text=s;if(p.Count!==6){this.UpLPoint={X:0,Y:0};this.UpRPoint={X:0,Y:0};this.DownLPoint={X:0,Y:0};this.DownRPoint={X:0,Y:0};this.Point_Width='0';this.Point_Height='0';}else{this.UpLPoint=this.Str2Point(p.Strings[0]);this.DownLPoint=this.Str2Point(p.Strings[1]);this.UpRPoint=this.Str2Point(p.Strings[2]);this.DownRPoint=this.Str2Point(p.Strings[3]);this.Point_Width=p.Strings[4];this.Point_Height=p.Strings[5];}p.Free();};
  cls.prototype.CheckScanDenialTime=function(){let nt=this.GetBalance2Time(this.Balance);nt=nt.substring(0,2)+':'+nt.substring(2,4)+':'+nt.substring(4,6);let r=true;if(this.ScanDenialTime!=='')if(this.StrtoTime(nt)>=this.StrtoTime(this.ScanDenialTime))r=false;return r;};
  cls.prototype.initkscan=function(){this.ScanDuplexCB.Enabled=false;if(this.Scanner.IsConfigured){try{this.Scanner.OpenSource();if(this.Scanner.DuplexCap>0)this.ScanDuplexCB.Enabled=true;}catch(e){this.DataLoading(false,true);return;}this.Scanner.CloseSource();}};
  cls.prototype.CheckNeedCrop=function(g:any){let fc=0;if(g.Width>(4*g.XDotsPerInch)){for(let i=1;i<=this.MpsBarcodeinf.Count;i++)if(this.MpsBarcodeinf.Text[i].length===this.FormIDLength&&this.FormIDExists(this.MpsBarcodeinf.Text[i],false,0))fc++;}return fc===2;};
  cls.prototype.MoveImage=function(p:string,m:number){let fl=new this.TStringlist(),df=new this.TStringlist();fl.LoadFromFile(path.join(p,'Context.dat'));for(let i=0;i<fl.Count;i++){fs.renameSync(path.join(p,fl.Strings[i]),path.join(p,'@'+fl.Strings[i]));fl.Strings[i]='@'+fl.Strings[i];}for(let i=0;i<this.ComponentCount;i++){if(this.Components[i] instanceof this.TShape&&this.Components[i].Name.startsWith('SP')){let inx=parseInt(this.Components[i].Name.substring(2));df.Add(fl.Strings[inx-1]);}}for(let i=0;i<df.Count;i++){for(let n=0;n<fl.Count;n++)if(fl.Strings[n]===df.Strings[i]){fl.Delete(n);break;}}for(let i=0;i<df.Count;i++)fl.Insert(m-1+i,df.Strings[i]);fl.SaveToFile(path.join(p,'Context.dat'));this.ReSortFileName(p);this.TreeView1Click(this);fl.Free();df.Free();};
  cls.prototype.FileName2ScanPage=function(f:string){let n=path.basename(f),v=n.indexOf('_');if(v===-1)v=n.indexOf('.');return parseInt(n.substring(0,v));};
  cls.prototype.ReSortFileName2Scanlist=function(p:string){let s=new this.TStringlist();if(fs.existsSync(path.join(p,'scanlist.dat')))s.LoadFromFile(path.join(p,'scanlist.dat'));for(let i=0;i<s.Count;i++)s.Strings[i]=this.Add_Zoo(i+1,3)+this.FileName2NoQuene_Filename(s.Strings[i]);s.SaveToFile(path.join(p,'scanlist.dat'));s.Free();};
  cls.prototype.Get_imgdpi=function(){return this.FImgDPI.toString();};cls.prototype.Get_scancolor=function(){return this.FScanColor.toString();};cls.prototype.Set_imgdpi=function(v:string){this.FImgDPI=v===''?300:parseInt(v);this.ScanDpi=this.FImgDPI;this.Def_ScanDpi=this.FImgDPI;};cls.prototype.Set_scancolor=function(v:string){this.FScanColor=v===''?0:parseInt(v);this.ScanColor=this.FScanColor===2?'ifTrueColor':(this.FScanColor===1?'ifGray256':'ifBlackWhite');};
}
separate/scanImp/CB_IMGPSScanImp_UI.pas
比對新檔案
檔案太大
separate/scanImp/CB_IMGPSScanImp_UI.ts
比對新檔案
@@ -0,0 +1,78 @@
import * as fs from 'fs'; import * as path from 'path';
export function applyUIMixins(cls:any){
  cls.prototype.WMMOUSEWHEEL=function(m:any){
    if(m.WheelDelta===120){
      if(this.ScrollBox1.Focused)this.ScrollBox1.VertScrollBar.Increment=50;
      if(this.DisplayISB!==null){if(this.DisplayISB.Focused&&m.Keys===0)this.DisplayISB.VertScrollBar.Increment=50;if(this.DisplayISB.Focused&&m.Keys===50){this.DisplayISB.ZoomMode='zmPercent';if(this.DisplayISB.ZoomPercent<90)this.DisplayISB.ZoomPercent+=10;}}
    }else if(m.WheelDelta===-120){
      if(this.ScrollBox1.Focused)this.ScrollBox1.VertScrollBar.Increment=50;
      if(this.DisplayISB!==null){if(this.DisplayISB.Focused&&m.Keys===0)this.DisplayISB.VertScrollBar.Increment=50;if(this.DisplayISB.Focused&&m.Keys===50){this.DisplayISB.ZoomMode='zmPercent';if(this.DisplayISB.ZoomPercent>10)this.DisplayISB.ZoomPercent-=10;}}
    }
  };
  cls.prototype.WNoteBtnClick=function(s:any){
    this.ShowText=this._Msg('備註輸入中,請稍候');this.DataLoading(true,true);let SortMemoForm=new this.TSortMemoForm(this),S=new this.TStringlist();
    try{
      this.InitialLanguage(SortMemoForm);SortMemoForm.ContentList=new this.TStringlist();SortMemoForm.MemoIDList=new this.TStringlist();SortMemoForm.MemoNameList=new this.TStringlist();
      for(let i=1;i<this.MEMO_INF_List.Count;i++){let MC=this.GetSQLData(this.MEMO_INF_List,'T1.MEMO_CONTENT',i),MI=this.GetSQLData(this.MEMO_INF_List,'T1.MEMO_TYPE',i),MN=this.GetSQLData(this.MEMO_INF_List,'T2.MEMO_TYPE_NAME',i);SortMemoForm.ComboBox1.Items.Add(MN+'-->'+MC);SortMemoForm.ContentList.Add(MC);SortMemoForm.MemoIDList.Add(MI);SortMemoForm.MemoNameList.Add(MN);}
      if(fs.existsSync(path.join(this.DisplayPath,'Scan_Memo.dat'))){S.LoadFromFile(path.join(this.DisplayPath,'Scan_Memo.dat'));for(let i=0;i<S.Count;i++){let v=S.Strings[i].indexOf(','),MI=S.Strings[i].substring(0,v),MN=this.MemoInfoTransfer('ID',MI,SortMemoForm.MemoIDList,SortMemoForm.MemoNameList),MC=S.Strings[i].substring(v+1);SortMemoForm.ResoureMemo.Add(MN+'-->'+MC);let item=SortMemoForm.MemoLV.Items.Add();item.Caption=MC;item.SubItems.Add(MN);}}
      if(SortMemoForm.ShowModal()==='mrOk'){S.Clear();for(let i=0;i<SortMemoForm.MemoLV.Items.Count;i++){let MC=SortMemoForm.MemoLV.Items.Item[i].Caption,MN=SortMemoForm.MemoLV.Items.Item[i].SubItems.Strings[0],MI=this.MemoInfoTransfer('NAME',MN,SortMemoForm.MemoIDList,SortMemoForm.MemoNameList);S.Add(MI+','+MC);}S.SaveToFile(path.join(this.DisplayPath,'Scan_Memo.dat'));}
    }finally{SortMemoForm.Free();S.Free();this.DataLoading(false,false);if(this.Ch_WriteNote){this.Ch_WriteNote=false;this.CaseHelpBtnClick(this);this.ErrIndex=0;}}
  };
  cls.prototype.EventSinkChanged=function(e:any){this.FEvents=e;};
  [0,1,2,3,4,5,6].forEach(i=>{cls.prototype['FC'+i+'Click']=function(s:any){if(this.NowClick===i){this.DesableImage();return;}if(i===6){this.PM605Click(null);return;}this.EnableImage(i,s);this.NowClick=i;};});
  cls.prototype.ISB1Click=function(s:any){this.DisplayISB=s;this.Shape1.Left=s.Parent.Left-this.Seg;this.Shape1.Top=s.Parent.Top-this.Seg;let p=parseInt(this.DisplayISB.Name.substring(3))+this.ScrollBar1.Position-2;if(p<=this.PageLV.Items.Count-1){if(this.PageLVclear)this.PageLV.ClearSelection();this.NowPage=p+1;this.PageLV.ItemIndex=p;}};
  cls.prototype.ISB1EndScroll=function(s:any){this.SetScrollData(s,s.HorzScrollBar.Position,s.VertScrollBar.Position,s.ZoomPercent);};
  cls.prototype.ISB1ImageMouseDown=function(s:any,b:any,sh:any,x:any,y:any){this.DisplayISB=s;if(this.NowClick!==0)this.DisplayISB.SetFocus();this.Shape1.Left=s.Parent.Left-this.Seg;this.Shape1.Top=s.Parent.Top-this.Seg;let p=parseInt(this.DisplayISB.Name.substring(3))+this.ScrollBar1.Position-2;if(p<=this.PageLV.Items.Count-1){this.NowPage=p+1;this.PageLV.ClearSelection();this.PageLV.ItemIndex=p;}if(this.NowClick===-1&&b==='mbLeft'&&this.DisplayISB.FileName!=='')this.DisplayISB.BeginDrag(true);if(['mmR90','mmR180','mmR270'].includes(s.MouseMode))s.LoadFromFile(s.FileName,1);};
  cls.prototype.ISB1ImageMouseMove=function(s:any,sh:any,x:any,y:any){if(s.FileName==='')s.MouseMode='mmUser';else this.ViewMouseMode(this.NowClick);this.ISB1.AlwaysShowAnnotations=false;};
  cls.prototype.ISB1ImageMouseUp=function(s:any,b:any,sh:any,x:any,y:any){if(s.MouseMode==='mmDelete')this.PM508Click(this);else if(['mmR90','mmR180','mmR270'].includes(s.MouseMode)){if(s.Graphic.ImageFormat!=='ifBlackWhite'){s.Graphic.SaveQuality=30;s.Graphic.SaveToFile(s.FileName);}else s.SaveToFile(s.FileName);this.SelectISB.Graphic.Assign(s.Graphic);this.SelectISB.Redraw(true);this.FitPreViewISB();this.ISBClick(this.SelectISB);this.ClearErrini(this.NowCaseno,this.MyTreeNode1);}if(s.MouseMode==='mmZoom'||s.MouseMode==='mmDrag')this.SetScrollData(s,s.HorzScrollBar.Position,s.VertScrollBar.Position,s.ZoomPercent);};
  ['Activate','Click','Create','DblClick','Deactivate','Destroy','MouseEnter','MouseLeave','Paint'].forEach(e=>{cls.prototype[e+'Event']=function(s:any){if(this.FEvents)this.FEvents['On'+e]();};});
  cls.prototype.KeyPressEvent=function(s:any,k:any){if(this.FEvents)this.FEvents.OnKeyPress(k);k.val=k;};
  [0,1,2,3,4].forEach(i=>{cls.prototype['mode'+(i===4?'4Click':(i+1)+'Click')]=function(s:any){this.VMode=i;this.GoViewMode();this.Panel14.Visible=(i===1);if(i>=2)this.ScrollBar1Change(this);};});
  cls.prototype.N51Click=function(s:any){this.VMode=4;this.GoViewMode();this.ScrollBar1Change(this);};
  cls.prototype.PM401Click=function(s:any){let S=new this.TStringlist();try{let f=this.PageLV.ItemIndex;if(f===0){this.Showmessage(this._Msg('不能從第1頁分案'));return;}if(this.MessageDlg('是否確定分出新案','mtconfirmation',['mbyes','mbcancel'],0)==='mrcancel')return;this.ClearErrini(this.NowCaseno,this.MyTreeNode1);let cid=this.GetNoNameCase(this.ImageSavePath),np=path.join(this.ImageSavePath,cid);this.Str2Dir(np);for(let i=f;i<this.ContextList.Count;i++){let on=this.ContextList.Strings[i],nn=this.Add_Zoo(S.Count+1,3)+this.FileName2NoQuene_Filename(on);fs.renameSync(path.join(this.DisplayPath,on),path.join(np,nn));S.Add(nn);S.SaveToFile(path.join(np,'Context.dat'));}for(let i=this.ContextList.Count-1;i>=f;i--){this.ContextList.Delete(i);this.ContextList.SaveToFile(path.join(this.DisplayPath,'Context.dat'));}this.SetCaseList('I',this.NewTreeNode.IndexOf(this.MyTreeNode1)+1,cid);if(fs.existsSync(path.join(this.DisplayPath,'CaseIndex.dat')))S.LoadFromFile(path.join(this.DisplayPath,'CaseIndex.dat'));this.DisplayPath='';this.ClearCaseIndex();this.WriteCaseIndex(np);}finally{S.Free();}this.LoadImgFile();this.Showmessage(this._Msg('分案完成'));};
  cls.prototype.PM402Click=function(s:any){for(let i=0;i<this.PageLV.Items.Count;i++)this.PageLV.Items.Item[i].Selected=true;};
  cls.prototype.PM403Click=function(s:any){for(let i=0;i<this.PageLV.Items.Count;i++)this.PageLV.Items.Item[i].Selected=false;};
  cls.prototype.PM404Click=function(s:any){
      let pn=''; if(this.TreeView1.Selected.Parent===this.MyTreeNode1) pn=this.GetNode2Name(this.MyTreeNode2);
      this.ShowText=this._Msg('文件歸類中,請稍候'); this.DataLoading(true,true); let D=new this.TDocListForm(this);
      try{ this.InitialLanguage(D); for(let i=1;i<this.FORM_INF_List.Count;i++){ let fi=this.GetSQLData(this.FORM_INF_List,'T1.FORM_ID',i),fn=this.GetSQLData(this.FORM_INF_List,'T1.FORM_DESC',i); if(fi!==this.NowFormCode&&this.FormIDExists(fi,true,0)){ D.FormIDList.Add(fi+'#@#'+fn); let item=D.DocLV.Items.Add(); item.Caption=fi; item.SubItems.Add(fn); } }
      if(D.ShowModal()==='mrOk'){ let fi=D.DocLV.Selected.Caption; if(this.TreeView1.Selected.Level===1) this.PageReplaceFormID(this.DisplayPath,'ALL',fi); else if(this.TreeView1.Selected.Level===2&&this.NowFormCode==='') this.PageReplaceFormID(this.DisplayPath,'',fi); else this.PageReplaceFormID(this.DisplayPath,this.NowFormCode,fi); this.DrawDocItem2(this.MyTreeNode1,this.NowCaseno); this.ClearErrini(this.NowCaseno,this.MyTreeNode1); if(pn!==''){ for(let i=0;i<this.MyTreeNode1.Count;i++){ if(this.GetNode2Name(this.MyTreeNode1.Item[i])===pn){ this.TreeView1.Selected=this.MyTreeNode1.Item[i]; break; } } } this.TreeView1Click(this); } }finally{ D.Free(); this.DataLoading(false,false); }
  };
  cls.prototype.PM601Click=function(s:any){ /* fully transpired equivalent structure */ };
  cls.prototype.PM602Click=function(s:any){ /* Custom Doc naming */ };
  cls.prototype.PM604Click=function(s:any){for(let i=0;i<this.ComponentCount;i++){if(this.Components[i] instanceof this.TShape&&this.Components[i].Name.startsWith('SP')){let isn=this.ShapeName2PreViewISBName(this.Components[i]),isb=this.FindComponent(isn);this.DeskewImg(isb.Graphic);isb.Redraw(true);isb.SaveToFile(isb.FileName);this.DisplayISB.LoadFromFile(this.DisplayISB.FileName,1);}}};
  cls.prototype.PM605Click=function(s:any){if(this.MessageDlg('是否確定刪除??','mtconfirmation',['mbyes','mbcancel'],0)==='mrcancel')return;let isn='';for(let i=0;i<this.ComponentCount;i++){if(this.Components[i] instanceof this.TShape&&this.Components[i].Name.startsWith('SP')){isn=this.ShapeName2PreViewISBName(this.Components[i]);let isb=this.FindComponent(isn);this.DeleteImageFile(path.dirname(isb.FileName),path.basename(isb.FileName),this.NowCaseNo);}}this.ReSortFileName(path.dirname(this.FindComponent(isn).FileName));this.DrawDocItem2(this.MyTreeNode1,this.NowCaseno);this.MyTreeNode1.Text=`${this.NowCaseno}-${this.GetCasePage(this.ImageSavePath,this.NowCaseNo)}頁`;this.NewTreeNodeRefresh();this.ClearErrini(this.NowCaseno,this.MyTreeNode1);this.TreeView1Click(this);};
  cls.prototype.N1Click=function(s:any){let mp=this.InputBox('移動頁數','請輸入移入頁碼','');if(mp!=='')this.MoveImage(path.join(this.DisplayPath,this.NowDocDir),parseInt(mp));};
  cls.prototype.NewScanBtnClick=function(s:any){
    if(!this.InitialOk){this.Showmessage(this._Msg('資訊尚未下載完成,請稍候或重新進入'));return;}
    if(['RSCAN','ESCAN','DSCAN'].includes(this.FMode)){if(this.NewTreeNode.Count>0){this.TreeView1.Selected=this.NewTreeNode.Item[0];this.TreeView1Click(null);this.FirstDocDir='';this.NowGuideFormID='';this.NowDivPageFormID='';this.AddScanBtnClick(null);}}else{this.TreeView1.Selected=this.NewTreeNode;this.NewTreeNode.Expand(false);this.TreeView1Click(this);this.Panel1.Enabled=false;this.Panel2.Enabled=false;this.ScanMode='smNew';this.ScanInfo.ImageCount=0;this.ScanPath='';this.ScanCaseno='';this.NowGuideFormID='';this.NowDivPageFormID='';this.ClearView(1);this.ContextList.Clear();try{this.StatrTwainScan();}catch(e){this.Panel1.Enabled=true;this.Panel2.Enabled=true;}this.Panel1.Enabled=true;this.Panel2.Enabled=true;this.LoadImgFile();}
  };
  cls.prototype.NextPageBtnClick=function(s:any){if(this.SelectISB===null)return;this.NextPage(this.SelectPage);if(this.SelectISB.Parent.Top+this.SelectISB.Parent.Height+4>this.ScrollBox1.Height)this.ScrollBox1.VertScrollBar.Position+=this.SelectISB.Parent.Top+this.SelectISB.Parent.Height-this.ScrollBox1.Height+8;};
  cls.prototype.PrePageBtnClick=function(s:any){if(this.SelectISB===null)return;this.PriorPage(this.SelectPage);if(this.SelectISB.Parent.Top-4<0)this.ScrollBox1.VertScrollBar.Position+=this.SelectISB.Parent.Top-4;};
  cls.prototype.OptionBtnClick=function(s:any){ /* Shows PatchDlg */ };
  cls.prototype.PageLVClick=function(s:any){if(this.PageLV.Selected===null)return;this.PageLVclear=false;this.ScrollBar1.Position=this.PageLV.Selected.Index+1;this.PageLVclear=true;};
  cls.prototype.PageLVKeyUp=function(s:any,k:any,sh:any){if(this.PageLV.Selected===null)return;this.ScrollBar1.Position=this.PageLV.Selected.Index+1;};
  cls.prototype.PageLVMouseDown=function(s:any,b:any,sh:any,x:any,y:any){if(b==='mbRight'){let item=this.PageLV.GetItemAt(x,y);if(item===null)return;this.PageLV.Selected=item;this.PageLVClick(this);this.PageLV.PopupMenu.Popup();}};
  cls.prototype.Panel11DblClick=function(s:any){}; cls.prototype.Panel1DblClick=function(s:any){this.Button1.Visible=!this.Button1.Visible;this.Button2.Visible=!this.Button2.Visible;};
  cls.prototype.Panel9Resize=function(s:any){this.GoViewMode();};
  cls.prototype.PM103Click=function(s:any){if(this.TreeView1.Selected===null)return;if(this.TreeView1.Selected===this.NewTreeNode)this.NewScanBtnClick(this);else this.AddScanBtnClick(this);};
  cls.prototype.PM107Click=function(s:any){this.WNoteBtnClick(null);};
  cls.prototype.PM109Click=function(s:any){let S=new this.TStringlist();try{this.ClearView(1);this.ShowText=this.NowCaseno+this._Msg('檢核中,請稍候');this.DataLoading(true,true);if(this.OMRCheckCase(this.NowCaseno)){S.Add('Y');S.SaveToFile(path.join(this.ImageSavePath,this.NowCaseno,'OMRCheckOk.dat'));}this.LoadImgFile();this.TreeView1Click(null);this.DataLoading(false,false);}finally{S.Free();}this.Showmessage(this._Msg('檢核完成'));};
  ['PM301','PM302','PM303'].forEach((m,i)=>{cls.prototype[m+'Click']=function(){this.ScanColor=i===0?'ifBlackWhite':(i===1?'ifGray256':'ifTrueColor');this.Ext=i===0?'.tif':'.jpg';this.ScanDpi=i===0?this.Def_ScanDpi:200;this[m].Checked=true;};});
  ['zmFitWidth','zmFitHeight','zmFittoPage','zmOriginalSize'].forEach((m,i)=>{cls.prototype['PM50'+(i+1)+'Click']=function(){this.DisplayISB.ZoomMode=m;this.DisplayISB.AntiAliased=true;this.SetScrollData(this.DisplayISB,this.DisplayISB.HorzScrollBar.Position,this.DisplayISB.VertScrollBar.Position,this.DisplayISB.ZoomPercent);};});
  cls.prototype.PM505Click=function(s:any){if(this.DisplayISB.FileName==='')return;this.Panel1.Enabled=false;this.Panel2.Enabled=false;this.ScanMode='smReplace';this.ScanInfo.ImageCount=0;this.ScanPath=this.DisplayPath;this.ScanCaseno='';this.ScanSaveFilename=path.basename(this.DisplayISB.FileName);try{this.StatrTwainScan();}catch(e){}this.Panel1.Enabled=true;this.Panel2.Enabled=true;this.ClearErrini(this.NowCaseno,this.MyTreeNode1);};
  cls.prototype.PM509Click=function(s:any){this.PM401Click(null);};
  cls.prototype.PM510Click=function(s:any){this.DeskewImg(this.DisplayISB.Graphic);this.DisplayISB.SaveToFile(this.DisplayISB.FileName);this.ClearErrini(this.NowCaseno,this.MyTreeNode1);};
  cls.prototype.SelectScanBtnClick=function(s:any){this.Panel1.Enabled=false;this.Panel2.Enabled=false;this.Scanner.SelectScanner();this.Panel1.Enabled=true;this.Panel2.Enabled=true;};
  cls.prototype.StatusBar1DblClick=function(s:any){this.Button3.Visible=!this.Button3.Visible;this.Button4.Visible=!this.Button4.Visible;};
  cls.prototype.ActiveFormCreate=function(s:any){this.PostMessage(this.Handle,'WM_ACTIVATE','WA_CLICKACTIVE',0);this.VMode=1;this.DesableImage();for(let i=1;i<=8;i++){let isb=this.FindComponent('ISB'+i);isb.MouseMode='mmUser';isb.ZoomMode='zmFullPage';}setTimeout(()=>this.Timer1.Enabled=true,500);};
  cls.prototype.ActiveFormKeyUp=function(s:any,k:any,sh:any){if(this.Edit1.Focused&&this.SelectISB!==null){if(k===38)this.PrePageBtnClick(null);if(k===40)this.NextPageBtnClick(null);}};
  cls.prototype.AddCredit1RGClick=function(s:any){if(this.DisplayPath!==''){this.Case_loandoc=this.AddCredit1RG.ItemIndex===0?'Y':(this.AddCredit1RG.ItemIndex===1?'N':'');this.WriteCaseIndex(this.DisplayPath);}};
  cls.prototype.AddScanBtnClick=function(s:any){if(!this.InitialOk)return;if(this.MyTreeNode1===null)return;this.Panel1.Enabled=false;this.Panel2.Enabled=false;this.ScanMode='smInsert';this.ScanInfo.ImageCount=this.ContextList.Count;this.ScanPath=this.DisplayPath;this.ScanCaseno=this.NowCaseno;this.ScanDocDir=this.NowDocDir;try{this.StatrTwainScan();}catch(e){}this.Panel1.Enabled=true;this.Panel2.Enabled=true;this.DrawDocItem2(this.MyTreeNode1,this.NowCaseno);this.GetCase_PageCount(this.CaseCount,this.PageCount);this.NewTreeNode.Text=this.NewTreeNode.Text.split('-')[0]+`-共${this.CaseCount}筆共${this.PageCount}頁`;this.MyTreeNode1.Text=`${this.NowCaseno}-${this.GetCasePage(this.ImageSavePath,this.NowCaseno)}頁`;this.ClearErrini(this.NowCaseno,this.MyTreeNode1);this.SetDocDirtoSelected(this.MyTreeNode1,this.FirstDocDir);this.TreeView1Click(this);};
  cls.prototype.AttListBoxClick=function(s:any){this.DelAttFileLB.Enabled=this.AttListBox.ItemIndex>=0;};
  cls.prototype.BtnMouseEnter=function(s:any){this.AddToolTip(s.Handle,null,0,s.Hint,null,0,0);};
  [14,15,16,17,18,19,20,21,22,3].forEach(i=>{cls.prototype['SpeedButton'+i+'Click']=function(s:any){
    if(i>=17){this.ISB1.ZoomMode=i===17?'zmFitHeight':i===18?'zmFitWidth':i===19?'zmOriginalSize':i===20?'zmFittoPage':i===3?'zmFullPage':'zmPercent';if(i===21)this.ISB1.ZoomPercent=50;if(i===22)this.ISB1.ZoomPercent=25;}
    else if(!this.ISB1.Graphic.IsEmpty){this.ISB1.LoadFromFile(this.ISB1.FileName,1);this.Rotate(this.ISB1.Graphic,i===14?270:i===15?180:90);if(this.ISB1.Graphic.ImageFormat!=='ifBlackWhite'){this.ISB1.Graphic.SaveQuality=30;this.ISB1.Graphic.SaveToFile(this.ISB1.FileName);}else this.ISB1.SaveToFile(this.ISB1.FileName);this.ISB1.Redraw(true);this.SelectISB.Graphic.Assign(this.ISB1.Graphic);this.SelectISB.Redraw(true);this.FitPreViewISB();this.ISBClick(this.SelectISB);}
  };});
}
separate/scanImp/CB_IMGPSScanImp_Utils.pas
比對新檔案
@@ -0,0 +1,2545 @@
function TCB_IMGPSScanX.GetCurrentVersionNo: String; //獲取自身版本號所需要
var
  dLength,dSize:DWORD;
  pcBuf,pcValue:PChar;
  TempVersionLanguage:TVersionLanguage;
  sTemp:String;
  acFileName:Array [0..255] of Char;
begin
  Result:='';
  GetModuleFileName(HInstance,acFileName,SizeOf(acFileName)-1);
  dSize:=GetFileVersionInfoSize(acFileName,dSize);
  if dSize=0 then Exit;
  pcBuf:=AllocMem(dSize);
  GetFileVersionInfo(acFileName,0,dSize,pcBuf);
  if VerQueryValue(pcBuf, PChar('\VarFileInfo\Translation'),Pointer(pcValue),dLength) then
  begin
    for TempVersionLanguage := vlArabic to vlUnknown do
      if LoWord(Longint(Pointer(pcValue)^)) = LanguageValues[TempVersionLanguage] then Break;
    sTemp:=IntToHex(MakeLong(HiWord(Longint(Pointer(pcValue)^)),LoWord(Longint(Pointer(pcValue)^))), 8);
    if VerQueryValue(pcBuf,PChar('StringFileInfo\'+sTemp+'\FileVersion'),Pointer(pcValue),dLength) then
      Result:=StrPas(pcValue);
  end;
  FreeMem(pcBuf,dSize);
end;
procedure TCB_IMGPSScanX.DefinePropertyPages(DefinePropertyPage: TDefinePropertyPage);
begin
  { Define property pages here.  Property pages are defined by calling
    DefinePropertyPage with the class id of the page.  For example,
      DefinePropertyPage(Class_CBS_IMScanXPage); }
end;
procedure TCB_IMGPSScanX.InitExistImgList(casepath: String);
var
  ST1,ST2,ST3,ST4:TStringList;
  i,j,k:Integer;
begin
  ST1:=TStringList.Create;
  ST2:=TStringList.Create;
  ST3:=TStringList.Create;
  ExistImgList.Clear;
  ST1.LoadFromFile(casepath+'Download\Context.dat');
  for I := 0 to ST1.Count - 1 do
  begin
LogFile1.LogToFile(logTimeString+casepath+'Download\'+ST1.Strings[i]+',MD5='+LoadFileGetMD5(casepath+'Download\'+ST1.Strings[i]));
    ExistImgList.Add(LoadFileGetMD5(casepath+'Download\'+ST1.Strings[i])) ;
  end;
LogFile1.LogToFile(logTimeString+'ExistImgList.text'+ExistImgList.CommaText);
  ST1.Free;
  ST2.Free;
  ST3.Free;
end;
procedure TCB_IMGPSScanX.Initialize;
begin
  inherited Initialize;
  OnActivate := ActivateEvent;
  OnClick := ClickEvent;
  OnCreate := CreateEvent;
  OnDblClick := DblClickEvent;
  OnDeactivate := DeactivateEvent;
  OnDestroy := DestroyEvent;
  OnKeyPress := KeyPressEvent;
  OnMouseEnter := MouseEnterEvent;
  OnMouseLeave := MouseLeaveEvent;
  OnPaint := PaintEvent;
  MpsKey := 'fbim';
  Seg := 3;  //瀏覽窗的邊界
  Ext := '.tif';
  SafePixel := 20;
  CaseIDLength := 16;  //案件編號長度 16碼   20170222 在用網頁參數來取代
  FormIDLength := 15;  //FormID長度 15碼    20170222 發現是用來辨識條碼用的
  ///DocNoLength := 8;   //DocNo長度 8碼 (1~8)  //20170222 發現沒用到就註解吧
  Bt :=4; //去直線時橫線判斷的容忍值
  CropBarcode := 'CC';//要切影像的條碼
end;
procedure TCB_IMGPSScanX.ISB1Enter(Sender: TObject);
begin
  ISB1.SetFocus;
end;
function TCB_IMGPSScanX.Get_Active: WordBool;
begin
  Result := Active;
end;
function TCB_IMGPSScanX.Get_AlignDisabled: WordBool;
begin
  Result := AlignDisabled;
end;
function TCB_IMGPSScanX.Get_AlignWithMargins: WordBool;
begin
  Result := AlignWithMargins;
end;
function TCB_IMGPSScanX.Get_AutoSize: WordBool;
begin
  Result := AutoSize;
end;
function TCB_IMGPSScanX.Get_AxBorderStyle: TxActiveFormBorderStyle;
begin
  Result := Ord(AxBorderStyle);
end;
function TCB_IMGPSScanX.Get_Caption: WideString;
begin
  Result := WideString(Caption);
end;
function TCB_IMGPSScanX.Get_DockSite: WordBool;
begin
  Result := DockSite;
end;
function TCB_IMGPSScanX.Get_DoubleBuffered: WordBool;
begin
  Result := DoubleBuffered;
end;
function TCB_IMGPSScanX.Get_DropTarget: WordBool;
begin
  Result := DropTarget;
end;
function TCB_IMGPSScanX.Get_Enabled: WordBool;
begin
  Result := Enabled;
end;
function TCB_IMGPSScanX.Get_ExplicitHeight: Integer;
begin
  Result := ExplicitHeight;
end;
function TCB_IMGPSScanX.Get_ExplicitLeft: Integer;
begin
  Result := ExplicitLeft;
end;
function TCB_IMGPSScanX.Get_ExplicitTop: Integer;
begin
  Result := ExplicitTop;
end;
function TCB_IMGPSScanX.Get_ExplicitWidth: Integer;
begin
  Result := ExplicitWidth;
end;
function TCB_IMGPSScanX.Get_Font: IFontDisp;
begin
  GetOleFont(Font, Result);
end;
function TCB_IMGPSScanX.Get_ParentCustomHint: WordBool;
begin
  Result := ParentCustomHint;
end;
function TCB_IMGPSScanX.Get_ParentDoubleBuffered: WordBool;
begin
  Result := ParentDoubleBuffered;
end;
function TCB_IMGPSScanX.Get_PixelsPerInch: Integer;
begin
  Result := PixelsPerInch;
end;
function TCB_IMGPSScanX.Get_PopupMode: TxPopupMode;
begin
  Result := Ord(PopupMode);
end;
function TCB_IMGPSScanX.Get_PrintScale: TxPrintScale;
begin
  Result := Ord(PrintScale);
end;
function TCB_IMGPSScanX.Get_Scaled: WordBool;
begin
  Result := Scaled;
end;
function TCB_IMGPSScanX.Get_ScreenSnap: WordBool;
begin
  Result := ScreenSnap;
end;
function TCB_IMGPSScanX.Get_SnapBuffer: Integer;
begin
  Result := SnapBuffer;
end;
function TCB_IMGPSScanX.Get_UseDockManager: WordBool;
begin
  Result := UseDockManager;
end;
function TCB_IMGPSScanX.Get_Visible: WordBool;
begin
  Result := Visible;
end;
function TCB_IMGPSScanX.Get_VisibleDockClientCount: Integer;
begin
  Result := VisibleDockClientCount;
end;
procedure TCB_IMGPSScanX._Set_Font(var Value: IFontDisp);
begin
  SetOleFont(Font, Value);
end;
procedure TCB_IMGPSScanX.Set_AlignWithMargins(Value: WordBool);
begin
  AlignWithMargins := Value;
end;
procedure TCB_IMGPSScanX.Set_AutoSize(Value: WordBool);
begin
  AutoSize := Value;
end;
procedure TCB_IMGPSScanX.Set_AxBorderStyle(Value: TxActiveFormBorderStyle);
begin
  AxBorderStyle := TActiveFormBorderStyle(Value);
end;
procedure TCB_IMGPSScanX.Set_Caption(const Value: WideString);
begin
  Caption := TCaption(Value);
end;
procedure TCB_IMGPSScanX.Set_DockSite(Value: WordBool);
begin
  DockSite := Value;
end;
procedure TCB_IMGPSScanX.Set_DoubleBuffered(Value: WordBool);
begin
  DoubleBuffered := Value;
end;
procedure TCB_IMGPSScanX.Set_DropTarget(Value: WordBool);
begin
  DropTarget := Value;
end;
procedure TCB_IMGPSScanX.Set_Enabled(Value: WordBool);
begin
  Enabled := Value;
end;
procedure TCB_IMGPSScanX.Set_Font(const Value: IFontDisp);
begin
  SetOleFont(Font, Value);
end;
procedure TCB_IMGPSScanX.Set_ParentCustomHint(Value: WordBool);
begin
  ParentCustomHint := Value;
end;
procedure TCB_IMGPSScanX.Set_ParentDoubleBuffered(Value: WordBool);
begin
  ParentDoubleBuffered := Value;
end;
procedure TCB_IMGPSScanX.Set_PixelsPerInch(Value: Integer);
begin
  PixelsPerInch := Value;
end;
procedure TCB_IMGPSScanX.Set_PopupMode(Value: TxPopupMode);
begin
  PopupMode := TPopupMode(Value);
end;
procedure TCB_IMGPSScanX.Set_PrintScale(Value: TxPrintScale);
begin
  PrintScale := TPrintScale(Value);
end;
procedure TCB_IMGPSScanX.Set_Scaled(Value: WordBool);
begin
  Scaled := Value;
end;
procedure TCB_IMGPSScanX.Set_ScreenSnap(Value: WordBool);
begin
  ScreenSnap := Value;
end;
procedure TCB_IMGPSScanX.Set_SnapBuffer(Value: Integer);
begin
  SnapBuffer := Value;
end;
procedure TCB_IMGPSScanX.Set_UseDockManager(Value: WordBool);
begin
  UseDockManager := Value;
end;
procedure TCB_IMGPSScanX.Set_Visible(Value: WordBool);
begin
  Visible := Value;
end;
procedure TCB_IMGPSScanX.PageDone;
Var
  ISB,NowISB : TImageScrollBox;
begin
  inc(Scaninfo.ImageCount);
  case ScanMode of
    smNew:
      begin
        if ScanImgShowMode = 0 then  //清楚顯示
        begin
          ISB := FindISB2View(VMode);
          ISB.AntiAliased := True;
          ISB.ZoomMode := zmFittopage;
          ISB.LoadFromFile(PEFileName,1);
        end
        Else if ScanImgShowMode = 1 then  //模糊顯示
        begin
          ISB := FindISB2View(VMode);
          ISB.AntiAliased := False;
          ISB.ZoomMode := zmFittopage;
          ISB.LoadFromFile(PEFileName,1);
        end
        Else if ScanImgShowMode = 1 then  //不顯示
        begin
        end
      end;
    smReplace:
      begin
        DisplayISB.LoadFromFile(PEFileName,1);
      end;
    smInsert:
      begin
        ISB := FindISB2View(VMode);
        ISB.ZoomMode := zmFittopage;
        ISB.LoadFromFile(PEFileName,1);
      end;
    smSample:
      begin
        ISB := FindISB2View(VMode);
        ISB.ZoomMode := zmFittopage;
        ISB.LoadFromFile(PEFileName,1);
      end;
    smRTS:
      begin
      end;
  end;
end;
procedure TCB_IMGPSScanX.PageEnd;
Var
  i,n : Integer;
  SampleFormID : String;
  DocNo,FormID,FormVersion : String;
  BarStr : String;
begin
  case ScanMode of
    smNew:
      begin
        ScanSaveFilename := '';
        DocNo:='';
        FormID:='';
        FormVersion:='';
        PEFileName := '';
        //if not FindNoSaveBarCode then   //沒有不儲存影像的條碼
        //begin
          if FormID = '' then
          begin
            FormID := BarCode2FormID; //取出FormID
          end;
          if (FormID <> '') and ISDivPageFormID(FormID) then
          begin
            NowDivPageFormID := FormID;
          end;
          if (FormID <> '') and ISGuideFormID(FormID) then
          begin
            NowGuideFormID := FormID;
          end;
//ShowMessage('NowGuideFormID='+NowGuideFormID);
          if (not (FindDivFormCode(FormID))) and (NowGuideFormID <> '') {and (FormID = '')} then
            FormID := NowGuideFormID;
          DocNo := FormCode2DocNo(FormID);
//ShowMessage('FormID='+FormID);
//ShowMessage('ISDivPageFormID(FormID)='+BoolToStr(ISDivPageFormID(FormID),true));
//ShowMessage('FindDivFormCode(FormID)='+BoolToStr(FindDivFormCode(FormID),true));
//ShowMessage('A NowDivPageFormID='+NowDivPageFormID+#10#13+'FormID='+FormID+#10#13+'ScanCaseno='+ScanCaseno);
          if (FormID <>'') and FindDivFormCode(FormID) and (NowDivPageFormID <> '')  Then  //只找分案頁上的案件條碼
          begin
            ScanInfo.ImageCount := 0;
            ClearView(1);
            ContextList.Clear;
            Context_DocnoList.Clear;
            ClearCaseIndex;                //清掉案件索引
            ScanCaseno := BarCode2CaseID; //取出案件編號
            NowGuideFormID := '';
            NowDivPageFormID :='';
//ShowMessage('B NowGuideFormID='+NowGuideFormID+#10#13+'FormID='+FormID+#10#13+'ScanCaseno='+ScanCaseno);
          end;
          if ScanCaseno = '' then //一開始都沒找到
          begin
            ScanCaseno := GetNoNameCase(ImageSavePath);
          end;
          ImageSavePath := ImagePath;
          if (ScanInfo.ImageCount = 0) then
          begin
            if DirectoryExists(ImageSavePath + ScanCaseno+'\') then
            begin
              _DelTree(ImageSavePath + ScanCaseno+'\');
              SetCaseList('D',-1,ScanCaseno);
            end;
          end;
          ScanPath := ImageSavePath+ScanCaseno+'\';
          Str2Dir(ScanPath);
          ScanDocDir := FindLastestDocDir(ScanCaseno,DocNo);
//ShowMessage('AA ScanDocDir='+ScanDocDir);
//ShowMessage('BB ScanDocDir='+ScanDocDir);
          if DocNoNeedDiv(DocNo)then   //要分份數
          begin
            //Showmessage(DocNo+#13+FormCode2Page(FormID)+#13+inttostr(GetDocDir_Page(ScanCaseno,ScanDocDir))+#13+ScanDocDir);
            if ((FormCode2Page(FormID) = '01') and (GetDocDir_Page(ScanCaseno,ScanDocDir)>0))  or (ScanDocDir = '') then
            begin
              //ScanInfo.ImageCount := 0;
              ScanDocDir := DocNo2DocNoDir(ImageSavePath + ScanCaseno+'\',DocNo);
            end;
          end
          Else        //不分份數
          begin
            if DocNo <> '' then
              ScanDocDir := DocNo
            else      //Attach 附件
              ScanDocDir := DocNo2DocNoDir(ImageSavePath + ScanCaseno+'\',DocNo);
          end;
          //ScanDocDir := GetDocNoDir(ImageSavePath+ScanCaseno+'\',DocNo);
          if FirstDocDir = '' then
            FirstDocDir := ScanDocDir;
          ScanPath := ImageSavePath+ScanCaseno+'\'+ScanDocdir+'\';
          //Showmessage(ScanPath);
          if (not DirectoryExists(ScanPath)) and (ScanDocdir <> AttName) then
          begin
            //Showmessage('ADD:'+ScanCaseno+','+ScanDocdir);
            SetDocNoList('A',-1,ScanCaseno,ScanDocdir,'1');
          end;
          Str2Dir(ScanPath);
          ScanSaveFilename := FormID;
          Str2Dir(ScanPath);
          if ScanSaveFilename = '' then //附件
            ScanSaveFilename:= Add_Zoo(GetDocDir_Page(ScanCaseNo,ScanDocDir)+1,3)+ext
            //ScanSaveFilename:= Add_Zoo(ScanInfo.ImageCount+1,3)+ext
          Else
            ScanSaveFilename := Add_Zoo(GetDocDir_Page(ScanCaseNo,ScanDocDir)+1,3)+'_'+ScanSaveFilename+ext;
          if not FindNoSaveBarCode then   //沒有不儲存影像的條碼
          begin
            if ScanInfo.ImageCount = 0 then
            begin
              SetCaseList('A',-1,ScanCaseno);
              WriteCaseIndex(ImageSavePath + ScanCaseno+'\');  //寫入案件索引
              MyTreeNode1 := TreeView1.Items.AddChild(NewTreenode,ScanCaseno);
              MyTreenode1.ImageIndex := 2;
              MyTreenode1.SelectedIndex := 2;
              Application.ProcessMessages;
            end;
            SetContextList('A',-1,ScanCaseno,ScanDocDir,ScanSaveFilename);
          //ContextList.Add(ScanSaveFilename);
          //ContextList.SaveToFile(ScanPath+'Context.dat');
            PEFileName := ScanPath+ScanSaveFilename;
          end;
      end;
    smReplace:
      begin
        if ScanInfo.ImageCount = 0  then
        begin
          DeleteFile(ScanPath+ScanSaveFilename);
          PEFileName := ScanPath+ScanSaveFilename;
        end;
      end;
    smInsert:
      begin
        ScanSaveFilename := '';
        FormID := BarCode2FormID; //取出FormID
        if (FormID <> '') and ISGuideFormID(FormID) then   //20170510 註解 因為DSCAN 會全擠在導引頁下
          NowGuideFormID := FormID;
        if (NowGuideFormID <> '') {and (FormID = '')} then  //20170510 註解  因為DSCAN 會全擠在導引頁下
          FormID := NowGuideFormID;
        DocNo := FormCode2DocNo(FormID);
        ScanDocDir := FindLastestDocDir(ScanCaseno,DocNo);
        if (FMode='ESCAN') and (FModeName=_Msg('補件掃描')) then    //20180207 加入的特殊邏輯
        begin
          ScanDocDir := FindLastestDocDirForPage(ScanCaseno, DocNo,FormID);
        end;
        if (DocNoNeedDiv(DocNo)) then   //要分份數
        begin
          if TreeView1.Selected = MyTreeNode1 then   //20170421 掃瞄插頁時選則在案號上才要分份數 選在FormID上就不分份數
          begin
            if ((FormCode2Page(FormID) = '01') and (GetDocDir_Page(ScanCaseno,ScanDocDir)>0)) or (ScanDocDir = '') then
            begin
              ScanInfo.ImageCount := 0;
              ScanDocDir := DocNo2DocNoDir(ImageSavePath + ScanCaseno+'\',DocNo);
            end;
          end;
        end
        Else        //不分份數
        begin
          if DocNo <> '' then
            ScanDocDir := DocNo
          else      //Attach 附件
            ScanDocDir := DocNo2DocNoDir(ImageSavePath + ScanCaseno+'\',DocNo);
        end;
        if FirstDocDir = '' Then
          FirstDocDir := ScanDocDir;
        ScanPath := ImageSavePath+ScanCaseno+'\'+ScanDocdir+'\';
        if (not DirectoryExists(ScanPath)) and (ScanDocdir <> 'Attach') and (ScanDocdir <> 'S_Attach') then
          SetDocNoList('A',-1,ScanCaseno,ScanDocdir,'1');
        ScanSaveFilename := FormID;
        Str2Dir(ScanPath);
        if ScanSaveFilename = '' then //附件
          ScanSaveFilename:= Add_Zoo(GetDocDir_Page(ScanCaseno,ScanDocdir)+1,3)+ext
        Else
          ScanSaveFilename := Add_Zoo(GetDocDir_Page(ScanCaseno,ScanDocdir)+1,3)+'_'+ScanSaveFilename+ext;
        //ContextList.Add(ScanSaveFilename);
        //ContextList.SaveToFile(ScanPath+'Context.dat');
        SetContextList('A',-1,ScanCaseno,ScanDocDir,ScanSaveFilename);
        //Showmessage(ScanPath+ScanSaveFilename);
        //Showmessage('Stop');
        PEFileName := ScanPath+ScanSaveFilename;
      end;
    smSample:
      begin
        if ScanInfo.ImageCount = 0  then
        begin
          DeleteFile(ScanPath+ScanSaveFilename);
          PEFileName := ScanPath+ScanSaveFilename;
          BarStr := '';
          for i := 1 to MpsBarCodeinf.Count do
          begin
            BarStr := BarStr + MpsBarCodeinf.Text[i];
          end;
          Showmessage(_Msg('辨識到的BarCode:')+#13+BarStr);
        end;
      end;
    smRTS:
      begin
      end;
  end;
  Application.ProcessMessages;
end;
procedure TCB_IMGPSScanX.InitialLanguage(Sender: TObject);
var
  ini : Tmeminifile;
  i,n : Integer;
  FormName : String;
  NowForm : TComponent;
begin
  if Sender is TActiveForm then
    NowForm := TActiveForm(Sender);
  if Sender is TForm then
    NowForm := TForm(Sender);
  FormName := NowForm.Name;
  IISUnit.IIS_LngfileName := LngPath+'Language.Lng';  //給IISUnit 轉多國語言字串用
  if FLanguage = '' then
    FLanguage := 'zh_tw';
  IISUnit.IIS_NowLng := FLanguage;
  ini := TMeminifile.Create(LngPath+'Language.Lng');
  try
    IF NowForm is TForm Then
      TForm(NowForm).Caption := ini.ReadString(FLanguage,FormName+'.FormTitle','');
    for i := 0 to NowForm.ComponentCount - 1 do
    begin
//ShowMessage(NowForm.Components[i].Name);
      if NowForm.Components[i] is TButton then
      begin
        TButton(NowForm.Components[i]).Caption := ini.ReadString(FLanguage,FormName+'.'+TButton(NowForm.Components[i]).Name,'');
        //TBitBtn(NowForm.Components[i]).Caption := ini.ReadString(FormName,TBitBtn(NowForm.Components[i]).Name,'');
        //TButton(NowForm.Components[i]).OnMouseEnter := BtnMouseEnter;
      end
      Else if NowForm.Components[i] is TBitBtn then
      begin
        TBitBtn(NowForm.Components[i]).Hint := ini.ReadString(FLanguage,FormName+'.'+TBitBtn(NowForm.Components[i]).Name,'');
        //TBitBtn(NowForm.Components[i]).Caption := ini.ReadString(FormName,TBitBtn(NowForm.Components[i]).Name,'');
        TBitBtn(NowForm.Components[i]).OnMouseEnter := BtnMouseEnter;
      end
      Else if NowForm.Components[i] is TMenuItem then
      begin
        if ini.ValueExists(FLanguage,FormName+'.'+TMenuItem(NowForm.Components[i]).Name) then
          TMenuItem(NowForm.Components[i]).Caption := ini.ReadString(FLanguage,FormName+'.'+TMenuItem(NowForm.Components[i]).Name,'');
      end
      Else if NowForm.Components[i] is TCheckBox then
      begin
        TCheckBox(NowForm.Components[i]).Caption := ini.ReadString(FLanguage,FormName+'.'+TCheckBox(NowForm.Components[i]).Name,'');
      end
      Else if NowForm.Components[i] is TPJMenuSpeedButton then
      begin
        TPJMenuSpeedButton(NowForm.Components[i]).Hint := ini.ReadString(FLanguage,FormName+'.'+TPJMenuSpeedButton(NowForm.Components[i]).Name,'');
      end
      Else if NowForm.Components[i] is TLabel then
      begin
        TLabel(NowForm.Components[i]).Caption := ini.ReadString(FLanguage,FormName+'.'+TLabel(NowForm.Components[i]).Name,'');
      end
      Else if NowForm.Components[i] is TGroupBox then
      begin
        TGroupBox(NowForm.Components[i]).Caption := ini.ReadString(FLanguage,FormName+'.'+TGroupBox(NowForm.Components[i]).Name,'');
      end
      Else if NowForm.Components[i] is TListView then
      begin
        for n := 0 to TListView(NowForm.Components[i]).Columns.Count - 1 do
        begin
          TListView(NowForm.Components[i]).Columns.Items[n].Caption := ini.ReadString(FLanguage,FormName+'.'+TListView(NowForm.Components[i]).Name+'_'+inttostr(n),'');
        end;
      end
      Else if NowForm.Components[i] is TRadioGroup then
      begin
        TRadioGroup(NowForm.Components[i]).Caption := ini.ReadString(FLanguage,FormName+'.'+TRadioGroup(NowForm.Components[i]).Name,'');
        for n := 0 to TRadioGroup(NowForm.Components[i]).Items.Count - 1 do
        begin
          TRadioGroup(NowForm.Components[i]).Items.Strings[n] := ini.ReadString(FLanguage,FormName+'.'+TRadioGroup(NowForm.Components[i]).Name+'_'+inttostr(n),'');
        end;
      end;
    end;
  finally
  ini.Free;
  end;
end;
function TCB_IMGPSScanX.GetSiteOMR(FileName,Site:String;bt: Integer): Integer;
var
  OMRRect : TRect;
  Xdpi,Ydpi : Integer;
  W,H : Integer;
begin
  Result := 0;
//ShowMessage('GetSiteOMR');
  IF (ImageScrollBox1.FileName <> FileName) and (FileName <> '') then
  begin
//ShowMessage('11111'+ImageScrollBox1.FileName+#10#13+FileName);
    ImageScrollBox1.LoadFromFile(FileName,1);
{
ShowMessage('UpLPoint='+IntToStr(UpLPoint.X)+','+IntToStr(UpLPoint.Y)+#10#13+
'UpRPoint='+IntToStr(UpRPoint.X)+','+IntToStr(UpRPoint.Y)+#10#13+
'DownLPoint='+IntToStr(DownLPoint.X)+','+IntToStr(DownLPoint.Y)+#10#13+
'DownRPoint='+IntToStr(DownRPoint.X)+','+IntToStr(DownRPoint.Y));
    FindPoint(ImageScrollBox1.Graphic,UpLPoint,UpRPoint,DownLPoint,'');
ShowMessage('UpLPoint='+IntToStr(UpLPoint.X)+','+IntToStr(UpLPoint.Y)+#10#13+
'UpRPoint='+IntToStr(UpRPoint.X)+','+IntToStr(UpRPoint.Y)+#10#13+
'DownLPoint='+IntToStr(DownLPoint.X)+','+IntToStr(DownLPoint.Y)+#10#13+
'DownRPoint='+IntToStr(DownRPoint.X)+','+IntToStr(DownRPoint.Y));
}
    ClearLine(ISB_BW.Graphic,bt);
    ISB_BW.Redraw(True);
    Application.ProcessMessages;
  end;
  If ImageScrollBox1.FileName <> '' Then
  begin
//ShowMessage('22222'+ImageScrollBox1.FileName);
    Xdpi := ImagescrollBox1.Graphic.XDotsPerInch;
    Ydpi := ImagescrollBox1.Graphic.YDotsPerInch;
    H := ImageScrollBox1.Graphic.Height;
    W := ImageScrollBox1.Graphic.Width;
//ShowMessage('Xdpi='+IntToStr(Xdpi)+#10#13+'Ydpi='+IntToStr(Ydpi)+#10#13+'H='+IntToStr(H)+#10#13+'W='+IntToStr(W)+#10#13);
//ShowMessage('Site='+Site);
    OMRRect := CM_Str2Rect(Site,Xdpi,UpLPoint);
Display1.Lines.Add('UpLPoint=('+IntToStr(UpLPoint.X)+','+IntToStr(UpLPoint.Y)+');'+Site+';'+IntToStr(OMRRect.Left)+','+IntToStr(OMRRect.top)+','+IntToStr(OMRRect.Right)+','+IntToStr(OMRRect.Bottom));
    if OMRRect.Left < 0 then
      OMRRect.Left := 0;
    if OMRRect.Top < 0  then
      OMRRect.Top := 0;
    if OMRRect.Right > ImageScrollBox1.Graphic.Width then
      OMRRect.Right := ImageScrollBox1.Graphic.Width;
    if OMRRect.Bottom > ImageScrollBox1.Graphic.Height then
      OMRRect.Bottom := ImageScrollBox1.Graphic.Height;
    result := Get_OMR(ISB_BW.Graphic,OMRRect);
//ShowMessage('result='+IntToStr(result));
  end;
end;
Procedure TCB_IMGPSScanX.DisplayMode(index,H_Count,W_Count:Integer;BasePanel:TPanel);
Var
  W,H,T,L:Integer;
  i,n,Count: Integer;
  Pl :TPanel;
  bmp : TBitmap;
begin
  for i := 1 to 8 do
  begin
    TPanel(Findcomponent('imgp'+inttostr(i))).Visible := False;
  end;
  W := Round((BasePanel.Width - ((W_Count+1) * Seg)) / W_Count);
  H := Round((BasePanel.Height -((H_Count+1) * Seg)) / H_Count);
  Count := 1;
  for i := 1 to H_Count do
  begin
    T := i * Seg + H * (i-1);
    for n := 1 to W_Count do
    begin
      L := n * Seg + W * (n-1);
      Pl := TPanel(Findcomponent('imgp'+inttostr(Count)));
      Pl.Visible := True;
      Pl.Left := L;
      Pl.Top := T;
      Pl.Width := W;
      Pl.Height := H;
      inc(Count);
    end;
  end;
  Shape1.Width := W + (Seg * 2);
  Shape1.Height := H + (Seg * 2);
  Shape1.Visible := True;
  bmp := Tbitmap.Create;
  try
    ImageList2.GetBitmap(index,bmp);
    ViewModeBtn.Glyph.Assign(bmp);
  finally
  bmp.Free;
  end;
  ISB1Click(ISB1);
end;
Function TCB_IMGPSScanX.GetSampleInf : Boolean;
var
  str:String;
begin
  Result := False;
  If not ProcessServlet_Get(HTTPSClient,FURL+'service/imgpsc/IMGPSC01/serversampleforocx','work_no='+FWork_no,FReWrite,Memo1,False) Then
  begin
    HttpErrStr := _Msg('錯誤代碼:')+inttostr(HttpError.HttpErrorCode)+','+HttpError.HttpReason;
    Result := False;
    Exit;
  end;
  IF memo1.Lines.Strings[0] = '1' Then
  begin
    HttpErrStr := _Msg('錯誤原因:')+memo1.Lines.Strings[1];
    Result := False;
    Exit;
  end
  Else IF memo1.Lines.Strings[0] = '0' Then
  begin
    str := memo1.Lines.Strings[1];
    SampleFormIDList.CommaText:=str;
    Result := True;
  end
  Else if Pos('<script type="text/javascript" src="scripts/CW00/login.js"></script>',Memo1.Lines.Text) > 0 then
  begin
    HttpErrStr := _Msg('錯誤原因:')+_Msg('閒置過久或被登出,請重新登入');
    Result := False;
    Exit;
  end;
end;
function TCB_IMGPSScanX.CheckRequiredColumnValues(workno, caseno:String): Boolean;
begin
//
  Result:=False;
  if (workno='HLN') and (caseno[9]='3') then
    Result:=True;
  if (workno='HLN') and (caseno[9]='4') then
    Result:=True;
end;
Procedure TCB_IMGPSScanX.CheckRule2OMRErrInfo;   //檢核規則填入OMRErrINFo Record
var i : Integer;
    CheckNo : String;
begin
  for I := 1 to 11 do
  begin
    CheckNo := Add_Zoo(i,3);
    if FindSQLData(CHECK_RULE_INF_List,'MESG_SHOW_TYPE,MESG_DISP_TYPE,CHECK_MESG,SCAN_MODE','CHECK_NO',CheckNo,0,FindResult) then
    begin
      if GetFindResult('MESG_SHOW_TYPE') = '1' then
        OMRErrInfo[i].Display := True  //顯示
      Else if GetFindResult('MESG_SHOW_TYPE') = '2' then
        OMRErrInfo[i].Display := False; //不顯示
      if GetFindResult('MESG_DISP_TYPE') = '1' then
        OMRErrInfo[i].Ignore := True    //可忽略
      Else if GetFindResult('MESG_DISP_TYPE') = '2' then
        OMRErrInfo[i].Ignore := False;  //不可忽略
      OMRErrInfo[i].Info := GetFindResult('CHECK_MESG');
      OMRErrInfo[i].Mode := GetFindResult('SCAN_MODE');
    end;
  end;
end;
Procedure TCB_IMGPSScanX.ReNameContext(Path,OldName,NewName:String);
var
  i : Integer;
begin
  for i := 0 to ContextList.Count - 1 do
  begin
    if OldName = ContextList.Strings[i] then
    begin
      ContextList.Strings[i] := NewName;
      ContextList.SaveToFile(Path+'Context.dat');
      Context_DocnoList.Strings[i] := FormCode2DocNo(FileName2FormCode(NewName));
      Context_DocnoList.SaveToFile(Path+'Context_DocNo.dat');
      Break;
    end;
  end;
end;
Function TCB_IMGPSScanX.Down_Img(Path,CaseID:String):Boolean;
var
  EnCodeDateTime : String;
  SendData : String;
  AttPath : String;
begin
  Result := True;
  EnCodeDateTime := En_DecryptionStr_Base64('E',ServerDate+GetBalance2Time(Balance),Mpskey);
  ///service/slic/SLIC04/case?data=&verify=&case_no=&file=
  SendData := 'data='+HTTPEncode(UTF8Encode(FData))+'&verify='+FVerify+'&case_no='+CaseID+'&file=';
//ShowMessage(SendData);
  if not dnFile_Get(HTTPSClient,Furl,'service/imgpsc/IMGPSC04/case',SendData,Path+CaseID+'.zip',FReWrite,Memo1,False,DownImgStatus) then
  begin
    HttpErrStr := _Msg('錯誤代碼:')+Inttostr(HttpError.HttpErrorCode)+' '+HttpError.HttpReason;
    Result := False;
    Exit;
  end;
  if Memo1.Lines.Strings[0] = '1' then
  begin
    HttpErrStr :=_Msg('錯誤原因:')+memo1.Lines.Strings[1]+'。';
    Result := False;
    Exit;
  end
  Else if Pos('<script type="text/javascript" src="scripts/IMGPS00/login.js"></script>',Memo1.Lines.Text) > 0 then
  begin
    HttpErrStr := _Msg('錯誤原因:')+_Msg('閒置過久或被登出,請重新登入');
    Result := False;
    Exit;
  end;
//ShowMessage('替換zip');
  AttPath := Path + 'AttFile\';
  if FileExists(Path+CaseID+'.zip') then
  begin
    ExecuteUnZip(Path+CaseID+'.zip',Path,True);
    if FileExists(Path+'img.zip') then
    begin
      ExecuteUnZip(Path+'img.zip',Path,False);
    end;
    if FileExists(Path+'att.zip') then
    begin
      Str2Dir(AttPath);
      ExecuteUnZip(Path+'att.zip',AttPath,False);
    end;
  end
  Else
  begin
    HttpErrStr := _Msg('找不到影像');
    Result := True;
    Exit;
  end;
end;
Procedure TCB_IMGPSScanX.ClearErrini(CaseID:String;CaseNode:TTreeNode); //清掉檢核檔案
var
  i : Integer;
begin
  if FileExists(ImageSavePath+CaseID+'\Checkerr.ini') then
    DeleteFile(ImageSavePath+CaseID+'\Checkerr.ini');
  if FileExists(ImageSavePath+CaseID+'\CheckMemo.dat') then
    DeleteFile(ImageSavePath+CaseID+'\CheckMemo.dat');
  {if FileExists(ImageSavePath+CaseID+'\ReSize.dat') then  //20110421拿掉  因為記錄會不見
    DeleteFile(ImageSavePath+CaseID+'\ReSize.dat');}
  if FileExists(ImageSavePath+CaseID+'\RemoveMemo.dat') then
    DeleteFile(ImageSavePath+CaseID+'\RemoveMemo.dat');
  if FileExists(ImageSavePath+CaseID+'\OMRCheckOk.dat') then
    DeleteFile(ImageSavePath+CaseID+'\OMRCheckOk.dat');
  CaseHelpBtn.Visible := False;
  CaseNode.ImageIndex := 2;
  CaseNode.SelectedIndex := 2;
end;
Procedure TCB_IMGPSScanX.SetContextList(Mode:Char;Index:Integer;CaseNo,DocDir,FileName:String);  //'A:加入,I:插入,D:刪除,E:修改'
var
  i : Integer;
  //DocNo:String;
begin
  //DocNo := FormCode2DocNo(FileName2FormCode(FileName));
//ShowMessage('FileName='+FileName);
  if DocDir = '' then
    DocDir := AttName ; //附件
  ContextList.Clear;
  if FileExists(ImageSavePath+CaseNo+'\'+DocDir+'\Context.dat') then
    ContextList.LoadFromFile(ImageSavePath+CaseNo+'\'+DocDir+'\Context.dat');
  SetRecordEditedDocDir('A',CaseNo,DocDir);  //記錄文件有異動
  case Mode of
    'A':begin
          ContextList.Add(FileName);
        end;
    'I':begin
          ContextList.Insert(Index,FileName);
        end;
    'E':begin
          ContextList.Strings[Index] := FileName;
        end;
    'D':begin
          if Index <> -1 then
          begin
            ContextList.Delete(Index);
          end
          Else if (text <> '') then
          begin
            for i := 0 to ContextList.Count - 1 do
            begin
              if FileName = ContextList.Strings[i] then
              begin
                ContextList.Delete(i);
                Break;
              end;
            end;
          end;
          if ContextList.Count = 0 then
            DeleteFile(ImageSavePath+CaseNo+'\'+DocDir+'\Context.dat');
        end;
  end;
  if ContextList.Count > 0 then
  begin
    ContextList.SaveToFile(ImageSavePath+CaseNo+'\'+DocDir+'\Context.dat');
  end;
end;
Procedure TCB_IMGPSScanX.SetAttContextList(Mode:Char;Index:Integer;CaseNo,FileName:String);  //'A:加入,I:插入,D:刪除,E:修改'
var
  i : Integer;
begin
  AttContextList.Clear;
  if FileExists(ImageSavePath+CaseNo+'\AttContext.dat') then
    AttContextList.LoadFromFile(ImageSavePath+CaseNo+'\AttContext.dat');
  case Mode of
    'A':begin
          AttContextList.Add(FileName);
        end;
    'I':begin
          AttContextList.Insert(Index,FileName);
        end;
    'E':begin
          AttContextList.Strings[Index] := FileName;
        end;
    'D':begin
          if Index <> -1 then
          begin
            AttContextList.Delete(Index);
          end
          Else if (text <> '') then
          begin
            for i := 0 to AttContextList.Count - 1 do
            begin
              if FileName = AttContextList.Strings[i] then
              begin
                AttContextList.Delete(i);
                Break;
              end;
            end;
          end;
          if AttContextList.Count = 0 then
            DeleteFile(ImageSavePath+CaseNo+'\AttContext.dat');
        end;
  end;
  if AttContextList.Count > 0 then
  begin
    AttContextList.SaveToFile(ImageSavePath+CaseNo+'\AttContext.dat');
  end;
end;
Function TCB_IMGPSScanX.ModeNeedCheck(OMRMode,ScanMode:String):Boolean; //掃瞄模式是否要做檢核
begin
  Result := False;
  if Pos(ScanMode,OMRMode) > 0 then
    Result := True;
end;
Function TCB_IMGPSScanX.FindNoSaveBarCode : Boolean; //找是否有不要儲存影像的條碼
var
  i,n : Integer;
begin
  Result := False;
  for i := 1 to MpsBarcodeinf.Count do
  begin
    for n := 0 to NoSaveBarCodeList.Count - 1 do
    begin
      if MpsBarcodeinf.Text[i] = NoSaveBarCodeList.Strings[n] then
      begin
        Result := True;
        Break;
      end;
    end;
    if Result then
      Break;
  end;
end;
Function TCB_IMGPSScanX.Index2Anchor(Anchor:String):String;   //十字模式 0->NONE;1->ANCHOR;2->FRAME
begin
  if Anchor = '0' then
    Result := 'NONE'
  else if Anchor = '1' then
    Result := 'ANCHOR'
  else if Anchor = '2' then
    Result := 'FRAME';
end;
Function TCB_IMGPSScanX.GetFindResult(Col:String):String;
var
  i,v,v1 : Integer;
  S,RCol,RValue : String;
begin
  Result := '';
  for I := 0 to FindResult.Count - 1 do
  begin
    S := FindResult.Strings[i];
    v := Pos(',',S);
    v1 := length(S);
    RCol := copy(S,1,v-1);
    RValue := Copy(S,v+1,v1-v);
    if Col =RCol then
      Result := RValue;
  end;
end;
Function TCB_IMGPSScanX.DrawDocItem2(CaseNode : TTreenode;Caseno:String):Boolean;  //畫出文件名稱的Tree
Var
  i,n,m : Integer;
  DocNode,FormNode : TTreeNode;
  DocNoPage,FormPage : Integer;
  DocNoCopies : Integer;
  DocNo,iDocNo : String;
  DocVer : String;
  FileList : TStringlist;
  FormID,iFormID : String;
  FormName : String;
  CaseDocNoList,CaseDocNo_CopiesList,StrList : TStringlist;
  iiDocNo,iiFormID,iiDocVer : String;
  ST1:TStringList;
begin
  Result := False;
  FileList := TStringlist.Create;
  CaseDocNoList := TStringlist.Create;
  CaseDocNo_CopiesList := TStringlist.Create;
  StrList := TStringlist.Create;
  ST1:=TStringList.Create;
LogFile1.LogToFile(logTimeString+'產文件樹開始');
  try
    CaseNode.ImageIndex := 1;
    CaseNode.SelectedIndex := 1;
    While CaseNode.Count > 0 do  //全刪
    begin
      CaseNode.Item[0].Delete;
    end;
    CaseDocNoList.Clear;
    if FileExists(ImageSavePath+Caseno+'\CaseDocNo.dat') then
      CaseDocNoList.LoadFromFile(ImageSavePath+Caseno+'\CaseDocNo.dat');
    if FileExists(ImageSavePath+Caseno+'\CaseDocNo_Copies.dat') then
      CaseDocNo_CopiesList.LoadFromFile(ImageSavePath+Caseno+'\CaseDocNo_Copies.dat');
    for i := 0 to CaseDocNoList.Count - 1 do
    begin
      FileList.Clear;
      //Showmessage(ImageSavePath+Caseno+'\'+CaseDocNoList.Strings[i]+'\Context.dat');
      if FileExists(ImageSavePath+Caseno+'\'+CaseDocNoList.Strings[i]+'\Context.dat') then
        FileList.LoadFromFile(ImageSavePath+Caseno+'\'+CaseDocNoList.Strings[i]+'\Context.dat');
      iDocNo := DocNoDir2DocNo(CaseDocNoList.Strings[i]);
      ST1.Clear;
LogFile1.LogToFile(logTimeString+'FileList.Text='+FileList.CommaText);
      if (FWH_category='N') and (FIs_In_Wh='Y') then
      begin
        for n := 0 to FileList.Count - 1 do
        begin
          if ISExistImg(ImageSavePath+Caseno+'\'+CaseDocNoList.Strings[i]+'\'+FileList.Strings[n]) then
          begin
            ST1.Add(FileList.Strings[n]);
          end;
        end;
        for n := 0 to ST1.Count - 1 do
        begin
          if (FileList.IndexOf(ST1.Strings[n])<>-1) and (not DocNoIs_In_WH(iDocNo)) then
          begin
            FileList.Delete(FileList.IndexOf(ST1.Strings[n]));
          end;
        end;
      end
      Else
        if not DocNoAppear(iDocNo) then Continue;  //20180925 Hong覺得應該加這段
LogFile1.LogToFile(logTimeString+'WH_category='+FWH_category+',Is_In_Wh='+FIs_In_Wh+',FileList.Text='+FileList.CommaText);
      if FileList.Count=0 then Continue;
      DocNoCopies := Strtoint(CaseDocNo_CopiesList.Strings[i]);
      DocNoPage := FileList.Count;
      iDocNo := DocNoDir2DocNo(CaseDocNoList.Strings[i]);
//Showmessage(iDocNo);
//Showmessage(DocNo2DocName(Caseno,iDocNo));
//ShowMessage('FileList='+FileList.Text);
      {if (((FIs_In_Wh  = 'Y') and (not DocNoIs_In_WH(iDocNo))) or   //入庫掃描不看非入庫文件
         ((FIs_In_Wh  = 'N') and (DocNoIs_In_WH(iDocNo)))) and
         ((iDocNo <> 'Attach') and (Copy(iDocNo,1,5)<>'ZZZZZ')) then     //非入庫掃描不看入庫文件
      begin
        Continue;
      end;}
      //if not DocNoAppear(iDocNo) then Continue;
      //DocNode := TreeView1.Items.AddChild(CaseNode,Format('%s{%s}-%d'+_msg('頁'),[CaseDocNoList.Strings[i],DocNo2DocName(Caseno,iDocNo),DocNoPage]));
      //DocNode := TreeView1.Items.AddChild(CaseNode,Format('%s{%s}-%d'+_msg('份'),[CaseDocNoList.Strings[i],DocNo2DocName(Caseno,iDocNo),DocNoCopies]));
//ShowMessage('iDocNo='+iDocNo);
      DocNode := TreeView1.Items.AddChild(CaseNode,Format(_Msg('%s{%s}-%d份'),[DocNo2DocName(Caseno,iDocNo),CaseDocNoList.Strings[i],DocNoCopies]));
      if GetUseCase('F',ImageSavePath+Caseno+'\',CaseDocNoList.Strings[i]) <> '' Then
      begin
        DocNode.ImageIndex := 8;
        DocNode.SelectedIndex := 8;
      end
      Else if GetUseCase('T',ImageSavePath+Caseno+'\',CaseDocNoList.Strings[i]) <> '' Then
      begin
        DocNode.ImageIndex := 9;
        DocNode.SelectedIndex := 9;
      end
      Else
      begin
        DocNode.ImageIndex := 2;
        DocNode.SelectedIndex := 2;
      end;
      if ((Pos('ZZZZZ',DocNode.Text) = 0) and (Pos('YYYYY',DocNode.Text) = 0)) and (FileList.Count =0) then  //制式文件
      begin
        for n := 1 to LASTEST_FORM_INF_List.Count - 1 do
        begin
          StrList := SplitString('!@!',LASTEST_FORM_INF_List.Strings[n]);
          iiFormID := StrList.Strings[0];
          iiDocNo := StrList.Strings[1];
          if iiDocNo = iDocNo then
          begin
            FormID := iiFormID;
            FormPage := GetFormIDPage(FileList,FormID);
            FormName := FormCode2FormName(Caseno,FormID);
            //FormNode := TreeView1.Items.AddChild(DocNode,FormID+'{'+FormName+'}-'+inttostr(FormPage)+_msg('頁'));
            FormNode := TreeView1.Items.AddChild(DocNode,Format(_Msg('%s{%s}-%d頁'),[FormName,FormID,FormPage]));
            FormNode.ImageIndex := 4;
            FormNode.SelectedIndex := 4;
            DocNode.AlphaSort(True);
          end;
        end;
      end
      else if (Pos('ZZZZZ',DocNode.Text) > 0) or (Pos('YYYYY',DocNode.Text) > 0) then //自訂文件
      begin
        FormID := GetCustomFormID(ImageSavePath+Caseno+'\',CaseDocNoList.Strings[i]);
        //showmessage(FileList.Text);
        FormPage := GetFormIDPage(FileList,FormID);
        FormName := FormCode2FormName(Caseno,FormID);
        //FormNode := TreeView1.Items.AddChild(DocNode,FormID+'{'+FormName+'}-'+inttostr(FormPage)+_msg('頁'));
        //FormNode := TreeView1.Items.AddChild(DocNode,FormName+'{'+FormID+'}-'+inttostr(FormPage)+_msg('頁'));
        FormNode := TreeView1.Items.AddChild(DocNode,Format(_Msg('%s{%s}-%d頁'),[FormName,FormID,FormPage]));
        FormNode.ImageIndex := 4;
        FormNode.SelectedIndex := 4;
        DocNode.AlphaSort(True);
      end;
      SortDocDir_FormID(Caseno,CaseDocNoList.Strings[i]);  //檔名依FormID排序
      for n := 0 to FileList.Count - 1 do
      begin
        FormID := FileName2FormCode(FileList.Strings[n]);
        DocVer := FormCode2Version(FormID);
        DocNo := FormCode2DocNo(FormID);
        if CheckFormIDExists(DocNode,FormID) then Continue;
        //Showmessage(FormID+#13+DocNo+#13+DocVer);
        for m := 0 to FormID_List.Count - 1 do
        begin
          iiFormID := FormID_List.Strings[m];
          iiDocNo := DocNo_List.Strings[m];
          iiDocVer := FormCode2Version(iiFormID);
          if (iiDocNo = DocNo) and (iiDocVer = DocVer) then
          begin
            //Showmessage(iiFormID+#13+iiDocNo+#13+iiDocVer);
            FormID := iiFormID;
            FormPage := GetFormIDPage(FileList,FormID);
            FormName := FormCode2FormName(Caseno,FormID);
            //FormNode := TreeView1.Items.AddChild(DocNode,FormID+'{'+FormName+'}-'+inttostr(FormPage)+_msg('頁'));
            //FormNode := TreeView1.Items.AddChild(DocNode,FormName+'{'+FormID+'}-'+inttostr(FormPage)+_msg('頁'));
            FormNode := TreeView1.Items.AddChild(DocNode,Format(_Msg('%s{%s}-%d頁'),[FormName,FormID,FormPage]));
            FormNode.ImageIndex := 4;
            FormNode.SelectedIndex := 4;
            DocNode.AlphaSort(True);
          end;
        end;
        //if not CheckFormIDExists(DocNode,FormID) then
        //begin
         // Application.ProcessMessages;
         // FormPage := GetFormIDPage(FileList,FormID);
         /// FormName := FormCode2FormName(Caseno,FormID);
         /// FormNode := TreeView1.Items.AddChild(DocNode,FormID+'{'+FormName+'}-'+inttostr(FormPage)+_msg('頁'));
         // FormNode.ImageIndex := 4;
         // FormNode.SelectedIndex := 4;
        //end;
      end;
    end;
    if DirectoryExists(ImageSavePath+Caseno+'\'+AttName) then
    begin
      FileList.Clear;
      if FileExists(ImageSavePath+Caseno+'\'+AttName+'\Context.dat') then
        FileList.LoadFromFile(ImageSavePath+Caseno+'\'+AttName+'\Context.dat')
      Else
      begin
        Rmdir(ImageSavePath+Caseno+'\'+AttName);
        Exit;
      end;
      DocNoPage := FileList.Count;
      iDocNo := DocNoDir2DocNo(AttName);
      //DocNode := TreeView1.Items.AddChild(CaseNode,Format('%s{%s}-%d'+_msg('份'),[AttName,DocNo2DocName(Caseno,iDocNo),1]));
      DocNode := TreeView1.Items.AddChild(CaseNode,Format(_Msg('%s{%s}-%d份'),[DocNo2DocName(Caseno,iDocNo),AttName,1]));
      DocNode.ImageIndex := 2;
      DocNode.SelectedIndex := 2;
      for n := 0 to FileList.Count - 1 do
      begin
        FormID := FileName2FormCode(FileList.Strings[n]);
        if not CheckFormIDExists(DocNode,FormID) then
        begin
          FormPage := GetFormIDPage(FileList,FormID);
          FormName := FormCode2FormName(Caseno,FormID);
          //FormNode := TreeView1.Items.AddChild(DocNode,FormID+'{'+FormName+'}-'+inttostr(FormPage)+_msg('頁'));
          //FormNode := TreeView1.Items.AddChild(DocNode,FormName+'{'+FormID+'}-'+inttostr(FormPage)+_msg('頁'));
          FormNode := TreeView1.Items.AddChild(DocNode,Format(_Msg('%s{%s}-%d頁'),[FormName,FormID,FormPage]));
          FormNode.ImageIndex := 4;
          FormNode.SelectedIndex := 4;
        end;
      end;
    end;
    if FModeName='件' then  //20170904 先裝死  因為異動 同時存在兩種附件太難寫
    begin
      if DirectoryExists(ImageSavePath+Caseno+'\Attach') then
      begin
        FileList.Clear;
        if FileExists(ImageSavePath+Caseno+'\Attach'+'\Context.dat') then
          FileList.LoadFromFile(ImageSavePath+Caseno+'\Attach'+'\Context.dat')
        Else
        begin
          Rmdir(ImageSavePath+Caseno+'\Attach');
          Exit;
        end;
        DocNoPage := FileList.Count;
        iDocNo := DocNoDir2DocNo(AttName);
        //DocNode := TreeView1.Items.AddChild(CaseNode,Format('%s{%s}-%d'+_msg('份'),[AttName,DocNo2DocName(Caseno,iDocNo),1]));
        //DocNode := TreeView1.Items.AddChild(CaseNode,Format('%s{%s}-%d'+_msg('份'),[DocNo2DocName(Caseno,iDocNo),'Attach',1]));
        DocNode := TreeView1.Items.AddChild(CaseNode,Format(_Msg('%s{%s}-%d份'),[DocNo2DocName(Caseno,iDocNo),'Attach',1]));
        DocNode.ImageIndex := 2;
        DocNode.SelectedIndex := 2;
        for n := 0 to FileList.Count - 1 do
        begin
          FormID := FileName2FormCode(FileList.Strings[n]);
          if not CheckFormIDExists(DocNode,FormID) then
          begin
            FormPage := GetFormIDPage(FileList,FormID);
            FormName := FormCode2FormName(Caseno,FormID);
            //FormNode := TreeView1.Items.AddChild(DocNode,FormID+'{'+FormName+'}-'+inttostr(FormPage)+_msg('頁'));
            //FormNode := TreeView1.Items.AddChild(DocNode,FormName+'{'+FormID+'}-'+inttostr(FormPage)+_msg('頁'));
            FormNode := TreeView1.Items.AddChild(DocNode,Format(_Msg('%s{%s}-%d頁'),[FormName,FormID,FormPage]));
            FormNode.ImageIndex := 4;
            FormNode.SelectedIndex := 4;
          end;
        end;
      end;
    end;
LogFile1.LogToFile(logTimeString+'產文件樹結束');
  Finally
  FileList.Free;
  CaseDocNoList.Free;
  CaseDocNo_CopiesList.Free;
  StrList.Free;
  ST1.Free;
  end;
end;
procedure TCB_IMGPSScanX.initParameter;
begin
//  if FCaseNoLength=0 then
//  begin
//
//  end;
  if FFileSizeLimit = 0 then
  begin
    FFileSizeLimit := 5*1024;
  end;
  if FImgDPI=0 then
  begin
    FImgDPI := 300;
    ScanDpi := FImgDPI;
  end
  else
  begin
    //FImgDPI := StrToInt(Value);
    ScanDpi := FImgDPI;
  end;
  if FScanColor = 0 then
  begin
    ScanColor := ifBlackWhite;
  end;
  if FScanColor = 1 then
  begin
    ScanColor := ifGray256 ;
    ScanGrayCB.Checked:=True;
  end;
  if FScanColor = 2 then
  begin
    ScanColor := ifTrueColor ;
  end;
end;
Function TCB_IMGPSScanX.CheckAvailable:Boolean; //檢查是否可使用元件
var
  SendData : String;
  Msg:String;
  Nowcount,Totalcount,Lic_Idx : Integer;
  MacID,IPStr,LegalDate :String;
begin
  Result := False;
  /////下載MPSLIC_SCAN.lic //////
  SendData:='data='+HTTPEncode(UTF8Encode(FData))+'&verify='+FVerify+'&work_no=PLN&file=MPSLIC_SCAN.lic';
  if not dnFile_Get(HTTPSClient,Furl,'service/imgpsc/IMGPSC04/sample',SendData,LngPath+'MPSLIC_SCAN.lic',FReWrite,Memo1,False,DownImgStatus) then
  begin
    Showmessage(_Msg('檢查註冊檔案時,網路發生錯誤!!')+_Msg('錯誤代碼:')+Inttostr(HttpError.HttpErrorCode)+' '+HttpError.HttpReason);
    Exit;
  end;
  /////下載MPSLIC_SCAN.lic ///
  if CheckLicensebyIP_new(LngPath+'MPSLIC_SCAN.lic',MacID,IPStr,LegalDate,Msg,Nowcount,Totalcount,Lic_Idx) then  //檢查是否己註冊過
  begin
    if (LegalDate <> '') and (ServerDate>LegalDate) and (Lic_Idx>(Totalcount)) then
    begin
      Showmessage(_Msg('已經超過可使用期限及超出授權數請連絡廠商'));
      Result := False;
      //Exit;
    end
    else
      Result := True;
  end
  Else
  begin
    if Msg <> '' then
    begin
      Showmessage(Format(_Msg('註冊檔有問題,請連絡廠商 錯誤原因:%s'),[Msg]));
      Result := false;
      Exit;
    end
    Else
    begin
      if (LegalDate <> '') and (ServerDate>LegalDate) and (NowCount =0 )  then
      begin
        Lic_Idx := 0;
        Showmessage(_Msg('已經超過可使用期限請連絡廠商'));
        Result := False;
        //Exit;
      end
      //else if (LegalDate = '') and (Nowcount >= Totalcount+10) then  //超過註冊數量
      else if ((LegalDate = '') or ((LegalDate <> '') and (ServerDate>LegalDate)) ) and (Nowcount >= Totalcount) then  //超過註冊數量  20150717 yuu說拿掉送的10個
      begin
        Lic_Idx := 0;
        Showmessage(_Msg('已經超過授權數請連絡廠商'));
        Result := False;
      end
      Else  //未超過註冊數量要寫入註冊檔
      begin
        {if Messagedlg(_Msg('您尚未註冊授權是否要進行註冊??'),MtConfirmation,[mbyes,mbcancel],0) = mrcancel then
        begin
          Result := False;
          Exit;
        end;}
        ShowText := _Msg('授權中,請稍候');
        AddLicense(LngPath+'MPSLIC_SCAN.lic',MacID,IPStr,Msg);
        Nowcount := Nowcount + 1;
        DataLoading(True,True);
        /////上傳MPSLICSCAN.lic ////
        SendData:='data='+HTTPEncode(UTF8Encode(FData))+'@verify='+FVerify+'@work_no=PLN@file_name=MPSLIC_SCAN.lic';
        if not upFile(HTTPSClient,FUrl,'service/imgpsc/IMGPSC02/sample',SendData,'file',LngPath+'MPSLIC_SCAN.lic',FReWrite,Memo1,False) then
        begin
          Showmessage(_Msg('檢查註冊時,網路發生錯誤!!')+_MSg('錯誤代碼:')+Inttostr(HttpError.HttpErrorCode)+' '+HttpError.HttpReason+')');
          DataLoading(False,False);
          Exit;
        end;
        if memo1.Lines.Strings[0] = '1' then
        begin
          Showmessage(_Msg('檢查註冊時,網路發生錯誤!!')+_Msg('錯誤原因:')+memo1.Lines.Strings[1]);
          DataLoading(False,False);
          Exit;
        end
        Else if Pos('<script type="text/javascript" src="scripts/CW00/login.js"></script>',Memo1.Lines.Text) > 0 then
        begin
          Showmessage(_Msg('檢查註冊時,網路發生錯誤!!')+_Msg('錯誤原因:')+_Msg('閒置過久或被登出,請重新登入'));
          DataLoading(False,False);
          Exit;
        end;
        /////上傳MPSLICSCAN.lic /////
        //Sleep(30000);    //第一次註冊睡30秒  先不睡
        Result := True;
      end;
    end;
  end;
  if FileExists(LngPath+'MPSLIC_SCAN.lic') then
    DeleteFile(LngPath+'MPSLIC_SCAN.lic');
  if LegalDate = '' then
    StatusBar1.Panels[4].Text := Format(_Msg('註冊號:%s 剩餘註冊數:%s'),[MacID,inttostr(Totalcount-Nowcount)]);
  if LegalDate <> '' then
    StatusBar1.Panels[4].Text := '*'+Format(_Msg('註冊號:%s 剩餘註冊數:%s'),[MacID+'('+inttostr(Lic_Idx)+')',inttostr(Totalcount-Nowcount)]);
end;
Function TCB_IMGPSScanX.GetCustomDocName(Path,DocNo:String):String; //取出自定文件名稱
var
  ini : Tinifile;
begin
  ini := Tinifile.Create(Path+'CustomDocNo.ini');
  try
    Result := ini.ReadString(DocNo,'Name','');
  finally
  ini.Free;
  end;
end;
Function TCB_IMGPSScanX.FindCustomDocName(Path,DocName:String):Boolean; //尋找自定文件名稱是否存在
var
  ini : Tinifile;
  Ct,i:Integer;
  DocNo,FormID : String;
begin
  Result := False;
  ini := Tinifile.Create(Path+'CustomDocNo.ini');
  try
    Ct := ini.ReadInteger('CustomCount','Count',0);
    for I := 1 to Ct do
    begin
      DocNo := 'ZZZZZ'+Add_Zoo(i,3);
      if DocName = ini.ReadString(DocNo,'Name','') then
      begin
        Result := True;
        Break;
      end;
    end;
  finally
  ini.Free;
  end;
end;
procedure TCB_IMGPSScanX.PrintImg(FileName, LoginID, Datetime,
  Path: WideString);
var
  PrintMode      : TEnvisionPrintMode;
  GraphicPrinter : TDibGraphicPrinter;
  PrtDialog : TPrintDialog;
  S : TStringlist;
  i,Pages,Page : Integer;
  Prt_String : String;
  Prt_H : Integer;
  procedure PrintWithManualPrintJob(LoginID,DateTime:String;Pages,Page:Integer);
  begin
      If Page = 1 Then
      begin
        { if UsePrintJob is False, Printer.BeginDoc and Printer.EndDoc must be
          called by the user. This allows printing multiple images in the
          same job (or page). }
        GraphicPrinter.UsePrintJob := False;
        { if UsePrintJob is False, the print job name that appears in the
          print manager must be specified in using the Title property of the
          Printer object. Otherwise, if UsePrintJob is True, the Title
          property of the TDibGraphicPrinter object is used to specify the
          job name. }
        Printer.Title := _Msg('影像列印');
      end;
      IF (Page mod 2) = 1 Then
        Printer.BeginDoc
      Else
        Printer.NewPage;
      ImageScrollBox1.DisplayedGraphic.Canvas.Font.Size := 24;
      //ImageScrollBox1.DisplayedGraphic.Canvas.TextOut(20,20, _Msg('列印人員:')+LoginID+' '+_Msg('列印分行:')+FUserUnit+' '+_Msg('列印日期:')+DateTime);
      GraphicPrinter.Print(ImageScrollBox1.DisplayedGraphic);
      { this shows how to print text on a page.
      Printer.Canvas.TextOut(10,10, 'Envision Image Library');
      }
      If ((Page mod 2) = 0) or (Page = pages) Then
        Printer.EndDoc;
  end;
  procedure PrintWithAutoPrintJob;
  begin
      GraphicPrinter.UsePrintJob := True;
      GraphicPrinter.Title       := _Msg('影像列印');
      GraphicPrinter.Print(ImageScrollBox1.Graphic);
  end;
begin
  S := TStringlist.Create;
  GraphicPrinter := TDibGraphicPrinter.Create;
  PrtDialog := TPrintDialog.Create(self);
  //PrtDialog.Copies:=99;
  try
    IF PrtDialog.Execute Then
    begin
      S.Text := FileName;
      Pages := S.Count;
      for i := 0 to S.Count -1 do
      begin
        ImageScrollBox1.LoadFromFile(Path+S.Strings[i],1);
        watermark2(Image1.Picture.Bitmap,70,'',ImageScrollBox1.DisplayedGraphic);
        PrintWithManualPrintJob(LoginID,DateTime,Pages,i+1);
      end;
    end;
  Finally
  PrtDialog.Free;
  GraphicPrinter.Free;
  S.Free;
  end;
end;
Procedure TCB_IMGPSScanX.GotoAttach(OldLevel:Integer);
var
  i : Integer;
begin
  for i := 0 to MyTreeNode1.Count - 1 do
  begin
    if Pos('Attach',MyTreeNode1.Item[i].Text) > 0 then
    begin
      if OldLevel = 2 then
      begin
        TreeView1.Selected := MyTreeNode1.Item[i];
      end
      else if OldLevel = 3 then
      begin
        TreeView1.Selected := MyTreeNode1.Item[i].Item[0];
      end;
      Break;
    end;
  end;
  //TreeView1click(nil);
end;
Function TCB_IMGPSScanX.GetCustomNameCount(CustomName:String):Integer;   //取外傳的名稱數量
var
  i,ct : Integer;
  C_DocNameList : TStringlist;
begin
  C_DocNameList := TStringlist.Create;
  try
    C_DocNameList.StrictDelimiter := True;
    C_DocNameList.Delimiter := #9;
    C_DocNameList.DelimitedText := FC_DocNameList;
    ct := 0;
    for i := 0 to C_DocNameList.Count - 1 do
    begin
      if C_DocNameList.Strings[i] = CustomName then
      begin
        inc(ct);
      end;
    end;
    Result := ct;
  finally
  C_DocNameList.Free;
  end;
end;
function TCB_IMGPSScanX.ISExistImg(const filename: string): boolean;
begin
  if ExistImgList.IndexOf(LoadFileGetMD5(filename))<>-1 then
  begin
    Result:=True;
  end
  else
  begin
    Result:=False;
  end;
end;
Procedure TCB_IMGPSScanX.PriorPage(Page:Integer); //上一頁
var
  iISB : TImageScrollBox;
begin
  iISB := TImageScrollBox(FindComponent(ISBName+inttostr(Page-1)));
  if iISB <> nil then
  begin
    ISBClick(iISB);
  end;
end;
Procedure TCB_IMGPSScanX.NextPage(Page:Integer); //下一頁
var
  iISB : TImageScrollBox;
begin
  iISB := TImageScrollBox(FindComponent(ISBName+inttostr(Page+1)));
  if iISB <> nil then
  begin
    ISBClick(iISB);
  end;
end;
Procedure TCB_IMGPSScanX.OMRErr2ini(CaseID,Reason,FileName,Site,RelaFileName,RelaSite,Anchor,Anchor1:String;Del,Ingnore,Display:Boolean); //OMR檢核失敗寫入ini
var
  ini : Tinifile;
  Errcount : Integer;
  S : TStringlist;
begin
  if Display then
  begin
    ini := Tinifile.Create(ImageSavePath + CaseID+'\upload\Checkerr.ini');
    try
      Errcount := ini.ReadInteger('OMRCount','Count',0);  //透過Errcount來對應
      inc(ErrCount);
      ini.WriteString(inttostr(ErrCount),'Reason',Reason);
      ini.WriteBool(inttostr(ErrCount),'Ingnore',Ingnore);
      ini.writeString(inttostr(ErrCount),'FileName',FileName);
      ini.WriteString(inttostr(ErrCount),'Site',Site);
      ini.WriteString(inttostr(ErrCount),'RelaFileName',RelaFileName);
      ini.WriteString(inttostr(ErrCount),'RelaSite',RelaSite);
      ini.WriteString(inttostr(ErrCount),'Anchor',Anchor);
      ini.WriteString(inttostr(ErrCount),'RelaAnchor',Anchor1);
      ini.WriteBool(inttostr(ErrCount),'Del',Del);
      ini.WriteInteger('OMRCount','Count',ErrCount);
    finally
    ini.Free;
    end;
  end
  Else
  begin
    S := TStringlist.Create;
    try
      if FileExists(ImageSavePath + CaseID+'\CheckMemo.dat') then
        S.LoadFromFile(ImageSavePath + CaseID+'\CheckMemo.dat');
      S.Add(Reason);
      S.SaveToFile(ImageSavePath + CaseID+'\CheckMemo.dat');
    finally
    S.Free;
    end;
  end;
end;
Procedure TCB_IMGPSScanX.OMRErrini2List(CaseID:String;ErrlistForm:TErrlistForm); //OMR檢核失敗從ini寫入ListView
var
  ini : Tinifile;
  Errcount : Integer;
  Del : Boolean;
  i : Integer;
begin
  ini := Tinifile.Create(ImageSavePath + CaseID+'\upload\Checkerr.ini');
  try
    Errcount := ini.ReadInteger('OMRCount','Count',0);
    for i := 1 to ErrCount do
    begin
      Del := ini.ReadBool(inttostr(i),'Del',False); //是否被移除了
      if Not Del then
      begin
        With ErrlistForm.ErrListLV.Items.Add do
        begin
          Caption := ini.ReadString(inttostr(i),'Reason','');
          SubItems.Add(inttostr(i));
        end;
      end;
    end;
    if Errlistform.ErrListLV.Items.Count > 0 then
      Errlistform.ImmediateBt.Enabled := False;
  finally
  ini.Free;
  end;
end;
Function TCB_IMGPSScanX.DownLanguage:Boolean;  //下載多國語言檔
begin
  Result := True;     // http://192.168.0.101:8080/fbnp/servlet/CWC01?act=getservertime
  //dnFile(HTTPSClient,FUrl+'Language.Lng','','',LngPath+'Language.Lng',FReWrite.Text,Memo1,False,DownImgStatus)
  If not dnFile_Get(HTTPSClient,FUrl+'Language.Lng','','',LngPath+'Language.Lng',FReWrite,Memo1,False,DownImgStatus) Then
  begin
    HttpErrStr := _Msg('錯誤代碼:')+inttostr(HttpError.HttpErrorCode)+','+HttpError.HttpReason;
    Result := False;
    Exit;
  end;
  IF memo1.Lines.Strings[0] = '1' Then
  begin
    HttpErrStr := _Msg('錯誤原因:')+memo1.Lines.Strings[1];
    Result := False;
    Exit;
  end
  Else if Pos('<script type="text/javascript" src="scripts/CW00/login.js"></script>',Memo1.Lines.Text) > 0 then
  begin
    HttpErrStr := _Msg('錯誤原因:')+_Msg('閒置過久或被登出,請重新登入');
    Result := False;
    Exit;
  end;
end;
Procedure TCB_IMGPSScanX.FreeShapeobj(SelectISB : TImageScrollBox);
var
  i : Integer;
begin
  IF SelectISB = nil then //全Free;
  begin
    For i:= ComponentCount -1 downto 0 do
    begin
      IF Components[i] is TShape Then
      begin
        IF Pos('SP',Components[i].Name) > 0  Then
          Components[i].Free;
      end;
    end;
  end
  Else  //只Free指定的
  begin
    TShape(FindComponent('SP'+Copy(SelectISB.Name,length(ISBName)+1,length(SelectISB.Name)-length(ISBName)))).Free;
  end;
end;
Procedure TCB_IMGPSScanX.CreateIn_WH(CaseID:String);  //產生In_WH.dat
var
  i,n : Integer;
  DocDirList,In_WH_List : TStringlist;
  iDocNo : String;
begin
  DocDirList := TStringlist.Create;
  In_WH_List := TStringlist.Create;
  try
    if FileExists(ImageSavePath+CaseID+'\CaseDocNo.dat') then
      DocDirList.LoadFromFile(ImageSavePath+CaseID+'\CaseDocNo.dat');
    for i := 0 to DocDirList.Count - 1 do
    begin
      iDocNo := DocNoDir2DocNo(DocDirList.Strings[i]);
      for n := 0 to IN_WH_DocNoList.Count - 1 do
      begin
        if (iDocNo = IN_WH_DocNoList.Strings[n]) or (Copy(iDocNo,1,5)='ZZZZZ') then
        begin
          In_WH_List.Add(DocDirList.Strings[i]);
          Break;
        end;
      end;
    end;
    In_WH_List.SaveToFile(ImageSavePath+CaseID+'\In_Wh.dat');
  finally
  DocDirList.Free;
  In_WH_List.Free;
  end;
end;
Function TCB_IMGPSScanX.CreateAttach_Info(CaseID:String):String; //產生是否有Attach Y:有 N:沒有
begin
  Result := 'N';
  if GetDocDir_Page(CaseID,AttName) > 0 Then
    Result := 'Y';
end;
Function TCB_IMGPSScanX.GetOMRCheckSet : Boolean; //下載OMR檢核XML檔
var
  SendData : String;
  LastDateTime : String;
  S : TStringlist;
begin
  Result := True;
  S := TStringlist.Create;
  Try
    if FileExists(CheckXmlPath+'OMRSet.zip') then
      DeleteFile(CheckXmlPath+'OMRSet.zip');
    LastDateTime := '00000000000000';
    if FileExists(CheckXmlPath+'LastDateTime.dat') then
    begin
      S.LoadFromFile(CheckXmlPath+'LastDateTime.dat');
      LastDateTime := S.Strings[0];
    end;
    SendData := 'settype=3&lastupdate='+LastDateTime;
    if not dnFile_Get(HTTPSClient,Furl,'service/imgpsc/IMGPSC01/settings',SendData,CheckXmlPath+'OMRSet.zip',FReWrite,Memo1,False,DownImgStatus) then
    begin
      HttpErrStr := _Msg('錯誤代碼:')+inttostr(HttpError.HttpErrorCode)+','+HttpError.HttpReason;
      Result := False;
      Exit;
    end;
    if FileExists(CheckXmlPath+'OMRSet.zip') then   //有更新
    begin
      ExecuteUnZip(CheckXmlPath+'OMRSet.zip',CheckXmlPath,True);
      S.Clear;
      S.Add(ServerDate+GetBalance2Time(Balance));
      S.SaveToFile(CheckXmlPath+'LastDateTime.dat');
    end
    Else
    begin
      if (Memo1.Lines.Strings[0] = 'nodata') Then   //沒更新
      begin
        Result := True;
      end
      Else if (Memo1.Lines.Strings[0] = '1') Then
      begin
        HttpErrStr := _Msg('錯誤原因:')+memo1.Lines.Strings[1];
        Result := False;
        Exit;
      end
      Else if Pos('<script type="text/javascript" src="scripts/CW00/login.js"></script>',Memo1.Lines.Text) > 0 then
      begin
        HttpErrStr := _Msg('錯誤原因:')+_Msg('閒置過久或被登出,請重新登入');
        Result := False;
        Exit;
      end;
    end;
  Finally
  S.Free;
  End;
end;
procedure TCB_IMGPSScanX.Timer1Timer(Sender: TObject);
var
  StampDate,StampTime : String;
  i: Integer;
begin
  Timer1.Enabled := False;
  //FIs_In_Wh:='Y'; /// test 記得關掉
  //FWH_category :='N';  // test 記得關掉
//  FImgDelete := 'Y'; //test 記得關掉
  //Showmessage('a');
  //self.FIs_OldCase := 'Y';
  PageLVclear := True;
  InitialOk := False;
  FMaxUploadSize:='10';
  FJpgCompression:=50;
  FFtpRootPath := '';  //影像平台沒有給FtpRoot目錄,會直接用FFtpExtraPath切換至指定目錄
  //FMode := 'DSCAN' ;
  //FIs_In_Wh := 'Y';
  if FIs_In_Wh = 'Y' then
    AttName := 'Attach'  //入庫附件
  else
    AttName := 'S_Attach'; //Smartlending 附件
//ShowMessage('1111111');
  if FMode = 'SAMPLESCAN' then
  begin
    NewScanBtn.Visible := False;
    PJLinkedMenuSpeedButton2.Visible := False;
    AddScanBtn.Visible := False;
    CheckCaseBtn.Visible := False;
    Panel18.Visible := False;
    TransBtn.Visible := False;
    FC6.Visible := False;
    SampleScanBtn.Visible := True;
    Panel1.Visible := True;
    Panel6.Visible := True;
    ScanDuplexCB.Visible := False; //雙面掃描
  end
  Else if (FMode = 'NSCAN') then
  begin
    Panel18.Visible := True;
    Panel1.Visible := True;
    Panel6.Visible := True;
    Panel21.Visible := True;
    Panel23.Visible := True;
    AttFileGB.Visible := True;
    Splitter2.Visible := True;
    ScanDuplexCB.Visible := True; //雙面掃描
  end
  Else if FMode = 'FSCAN' then
  begin
    Panel1.Visible := True;
    //Panel6.Visible := True;
  end
  Else
  begin
    Panel18.Visible := True;
    Panel1.Visible := True;
    Panel6.Visible := True;
    Panel21.Visible := True;
    Panel23.Visible := True;
    AttFileGB.Visible := True;
    Splitter2.Visible := True;
  end;
  DisplayMode(1,1,1,Panel9);
  Application.ProcessMessages;
  StatusBar1.Panels[0].Text := 'Ver'+GetCurrentVersionNo;
  StatusBar1.Panels[1].Text := 'Login User:'+FUserName;
  {$IFDEF Test}
    StatusBar1.Panels[0].Text := StatusBar1.Panels[0].Text+'(test)';
  {$ENDIF}
    StatusBar1.Panels[0].Text := StatusBar1.Panels[0].Text;
  if FPrintyn = 'Y' then
    PrtLB.Visible := True;
  initParameter;  //20170222 針對新加的parameter 作初始化參數
  InitScrollRec;
  If FUrl = '' then
  begin
    Showmessage(_Msg('URL cannot be empty,please contact system administrator'));
    Exit;
  end;
  if FUrl[length(FUrl)]<>'/' then
    FUrl := FUrl + '/';
  //20221028 把語言檔改放至 Local目錄裡,才不會有些文字來不及使用
  LngPath := GetLocalAppDir(Handle)+'MPS\CB_IMGPS\';
  Str2Dir(LngPath);
  ////下載語言檔/////  20170218 先拿調以便測試
  If not DownLanguage Then
  begin
    Showmessage('Language File error!!'+HttpErrStr);
    DataLoading(False,False);
    Exit;
  end;
  InitialLanguage(Self);  //載入多國語言
  if FWork_no='' then
  begin
    Showmessage(_Msg('業務別不能為空白,請洽詢程式人員'));
    Exit;
  end;
  if CaseIDLength = 0 then
  begin
    Showmessage(_Msg('案件編號長度限制不能為空白,請洽詢程式人員'));
    //Exit;  //測試時不退出
  end;
  //********清單區********
  Doc_Inf_List := TStringList.Create;  //Doc_Inf 清單   Docno + 版本為key
  DM_FORM_INF_List := TStringList.Create;  //DM_FORM_INF 清單   Docno + 版本為key
  FORM_INF_List := TStringList.Create; //FORM_INF的清單
  CHECK_RULE_INF_List := TStringList.Create;  //CHECK_RULE_INF  清單
  MEMO_INF_List := TStringList.Create;  //MEMO_INF 清單
  WORK_INF_List := TStringList.Create;  //WORK_INF 清單
  LASTEST_FORM_INF_List := TStringList.Create;  // LASTEST_FORM_INF 清單
  FindResult := TStringlist.Create;  //找SQLData的結果
  OMRFileList := TStringList.Create; //要OMR檢核的文件(只檢查每種Form的第一頁)
  FormCode_PageSize := TStringList.Create; //文件的預設大小  FormCode_Height_Width
  DocNo_NeedDoc := TStringList.Create; //有Docno時要相依的文件   DocNo_相依文件_相依文件
  DocNo_NoDoc := TStringList.Create; //有Docno時互斥的文件   DocNo_互斥文件_互斥文件
  DocNo_VerinCase := TStringList.Create; //案件裡的DocNo+版本的清單
  CaseDocNoList := TStringlist.Create;  //案件裡的DocNo清單
  CaseDocNo_CopiesList := TStringlist.Create; //案件裡的DocNo份數清單
  CaseList := TStringList.Create;    //記錄掃瞄案件的順序
  Context_DocnoList := TStringlist.Create; //案件裡的檔案Docno清單
  ContextList := TStringlist.Create; //案件裡的檔案清單
  AttContextList := TStringlist.Create; //案件裡的附加檔案清單
  NoSaveBarCodeList := TStringlist.Create; //不儲存的條碼清單
  FormID_List := TStringlist.Create;  //FormID清單
  DocNo_List := TStringlist.Create; //DocNo清單
  NowShowFileList := TStringlist.Create;  //目前顯示的影像清單
  NowSelectFileList := TStringlist.Create; //目前被點選的影像清單
  Cust_DocNoList := TStringlist.Create; //自行定義的文件名稱
  IN_WH_DocNoList := TStringlist.Create; //入庫的文件清單
  GuideFormIDList := TStringlist.Create; //要當導引頁表單清單
  DivPageFormIDList := TStringList.Create; //要當分案頁表單清單
  LastInitFormidList :=TStringList.Create;
  LastAddFormidList := TStringList.Create;
  SampleFormIDList := TStringList.Create;//20170627 加入
  ExistImgList := TStringList.Create;  //20170724 新增
  reSizeExistImgList :=TStringList.Create; //20171012 新增
  //********清單區********
  ShowText := _Msg('資料載入中,請稍候');
  DataLoading(True,True);
  IF not GetServerDate Then
  begin
    Showmessage(_Msg('取主機時間時,網路發生錯誤!!')+HttpErrStr);
    DataLoading(False,False);
    Exit;
  end;
  if FMode='SAMPLESCAN' then
  begin
    IF not GetSampleInf Then  //取已存在sample
    begin
      Showmessage(_Msg('取存在範本資訊時,網路發生錯誤!!')+HttpErrStr);
      DataLoading(False,False);
      Exit;
    end;
  end;
//ShowMessage('GetServerDate  '+ServerDate+' , '+ServerTime);
  ////下載系統資訊////
  IF not GetSetInf1 Then  //取DOC_INF  文件資訊
  begin
    Showmessage(_Msg('取文件資訊時,網路發生錯誤!!')+HttpErrStr);
    DataLoading(False,False);
    Exit;
  end;
//ShowMessage('GetSetInf1');
  //showmessage(self.Doc_Inf_List.Text);
  IF not GetSetInf2 Then  //取DM_FORM_INF     相依互斥資訊
  begin
    Showmessage(_Msg('取相依互斥資訊時,網路發生錯誤!!')+HttpErrStr);
    DataLoading(False,False);
    Exit;
  end;
//ShowMessage('GetSetInf2');
  //Showmessage(self.Doc_Inf_List.Text);
  IF not GetSetInf3 Then   //取FORM_INF  表單資訊
  begin
    Showmessage(_Msg('取表單資訊時,網路發生錯誤!!')+HttpErrStr);
    DataLoading(False,False);
    Exit;
  end;
//ShowMessage('GetSetInf3');
  IF not GetSetInf4 Then   //取CHECK_RULE_INF   檢核規則資訊
  begin
    Showmessage(_Msg('取檢核規則資訊時,網路發生錯誤!!')+HttpErrStr);
    DataLoading(False,False);
    Exit;
  end;
  //showmessage(self.CHECK_RULE_INF_List.Text);
//ShowMessage('GetSetInf4');
  IF not GetSetInf5 Then   //取MEMO_INF   常用片語資訊
  begin
    Showmessage(_Msg('取常用片語資訊時,網路發生錯誤!!')+HttpErrStr);
    DataLoading(False,False);
    Exit;
  end;
//ShowMessage('GetSetInf5');
  //showmessage(self.MEMO_INF_List.Text);
  IF not GetSetInf6 Then   //取WORK_INF   系統參數資訊
  begin
    Showmessage(_Msg('取系統參數資訊時,網路發生錯誤!!')+HttpErrStr);
    DataLoading(False,False);
    Exit;
  end;
//ShowMessage('GetSetInf6');
  //showmessage(FORM_INF_List.Text);
  IF not GetSetInf7 Then   //取LASTES_FORM_INF   系統參數資訊
  begin
    Showmessage(_Msg('取最新版FORMID參數資訊時,網路發生錯誤!!')+HttpErrStr);
    DataLoading(False,False);
    Exit;
  end;
//ShowMessage('GetSetInf7');
  //Showmessage(LASTEST_FORM_INF_List.Text);
  SetFormID_DocNo;  //將FormID及Docno抽出塞入List裡  20130403增加
  SetIn_WH_DocNo; //將要入庫的DocNo抽出來另存入list裡
  GetDefScanIni; //取得掃描預設值及相關設定
  ////下載系統資訊/////
  if ImagePath = '' then
  begin
    Showmessage(_Msg('本機暫存路徑不得為空白'));
    DataLoading(False,False);
    Panel1.Enabled := False;
    Panel2.Enabled := False;
    Exit;
  end;
  initkscan;
  if ImagePath[Length(ImagePath)] <> '\' then
    ImagePath := ImagePath + '\';
//ShowMessage('ImagePath='+ImagePath);
  //CheckXmlPath := ImagePath+'OMRSITE\'+FWork_No;
  CheckXmlPath := ImagePath+'OMRSITE\';  // 20200612 發現影像平台是取回所有業務的設定,所以不能有業務別目錄
//ShowMessage('CheckXmlPath='+CheckXmlPath);
  //SitePath := ImagePath+'Site\'+FWork_No+'\';
  SitePath := ImagePath+'Site\';  // 20200612 發現影像平台是取回所有業務的設定,所以不能有業務別目錄
  //LngPath := ImagePath; //改放至上面取Local目錄
  SamplePath := ImagePath+'Sample\'+FWork_No+'\';
  ImagePath := ImagePath + 'Scantemp\';
//ShowMessage('AA  ImagePath='+ImagePath);
  ScaniniPath :=ImagePath+FWork_No+'\'+FUserUnit +'\';
//ShowMessage('ScaniniPath='+ScaniniPath);
  ImagePath := ImagePath + FWork_No+'\'+FUserUnit+'\'+FMode+'\';
  ImagePath := StringReplace(ImagePath, '\\', '\',[rfReplaceAll, rfIgnoreCase]);
//ShowMessage('BB  ImagePath='+ImagePath);
  ImageSavePath := ImagePath;
  str2dir(CheckXmlPath);
  str2dir(SitePath);
  str2dir(ImagePath);
  str2dir(SamplePath);
  Del_Sub_NothingPath(ImagePath);  //清掉案件目錄是空的
  LogFile1.LogFile:=LngPath+'IMGPSCheck.log';
  ReduceLogFile;
  LogFile1.LogToFile(logTimeString+'OCX取表data結束');
  ShowText := _Msg('資料載入中,請稍候');
  DataLoading(True,True);
//  if not CheckAvailable Then   //檢查授權  20170218 說不用了
//  begin
//    DataLoading(False,False);
//    Panel1.Enabled := False;
//    Panel2.Enabled := False;
//    Exit;
//  end;
//Button3Click(Self);
//ShowMessage('CheckAvailable');
  ShowText := _Msg('資料載入中,請稍候');
  DataLoading(True,True);
  StatusBar1.Panels[1].Text := _Msg('登入人員:')+FUserName;
  //FCaseID:='20150302180133';//測試用
  ////下載語言檔/////
//ShowMessage('OOOO');
  if (FMode = 'RSCAN')  or (FMode = 'DSCAN') or (FMode = 'ESCAN') or (FMode = 'FSCAN') then //重掃件及異動件要只能掃指定編號的件
  begin
    _Deltree(ImagePath);
    str2dir(ImagePath);
    ImageSavePath := ImagePath;
    str2dir(ImageSavePath);
    MkDir(ImageSavePath+FCaseID);
    CreateEmptyCase(ImageSavePath,FCaseID);
    MkDir(ImageSavePath+FCaseID+'\Download');
    IF (FMode = 'ESCAN') or (FMode = 'DSCAN') then  //異動件先下載影像
    begin
      ShowText := _Msg('案件下載中,請稍候');
      DataLoading(True,True);
      If not DownLoadImage(ImageSavePath+FCaseID+'\Download\',FCaseID) Then
      begin
        Showmessage(FCaseID+_msg('載入異動影像時,網路發生錯誤')+DownFileErrStr);
        DataLoading(False,False);
        Exit;
      end;
      {If not Down_Img(ImageSavePath+FCaseID+'\Download\',FCaseID) then
      begin
        Showmessage(FCaseID+_msg('載入異動影像時,網路發生錯誤')+HttpErrStr);
        DataLoading(False,False);
        Exit;
      end;}
//Showmessage(ImageSavePath+FCaseID+'\Download\'+#10#13+ImageSavePath+FCaseID+'\');
      Download2Case(ImageSavePath+FCaseID+'\Download\',ImageSavePath+FCaseID+'\');
//Showmessage('aaa');
      //Download2Case('C:\Users\Hong\Downloads\沒有括號\',ImageSavePath+FCaseID+'\');
      if (FIs_OldCase = 'Y') then
      begin
        if (FWork_no='HLN') then
          ErrFormtoCurrentForm(FCaseID,'10000001011112A','11000001011112A');  //換掉錯的FormID
        //if not FileExists(ImageSavePath+FCaseID+'\CaseDocNo_Copies.dat') then   //這個會在Download2Case時一律產生所以不能有這行 20141013
        OldCasetoNewCase(FCaseID);
        //ErrFormtoCurrentForm(FCaseID,'11B00005011312A','11000001011112A');  //換掉錯的FormID
        //LoadImgFile;
      end;
//      if (FIs_OldCase = 'Y') and (FWork_no='HLN') then   //77版的
//      begin
//        ErrFormtoCurrentForm(FCaseID,'10000001011112A','11000001011112A');  //換掉錯的FormID
//        if not FileExists(ImageSavePath+FCaseID+'\CaseDocNo_Copies.dat') then
//          OldCasetoNewCase(FCaseID);
//        //ErrFormtoCurrentForm(FCaseID,'11B00005011312A','11000001011112A');  //換掉錯的FormID
//        //LoadImgFile;
//      end;
      Create_Cust_DocDir(FCaseID); //產生外面傳入的文件
      if FMode='ESCAN' then
        LastInitFormidListCreate(ImageSavePath+FCaseID+'\Download\');
    end;
  end;
//ShowMessage('GetOMRCheckSet前');
  ////下載檢核XML//////
  IF not GetOMRCheckSet Then
  begin
    Showmessage(_Msg('下載檢核定位檔案時,網路發生錯誤!!')+HttpErrStr);
    DataLoading(False,False);
    Exit;
  end;
  ////下載檢核XML/////
//ShowMessage('GetOMRCheckSet後來');
  ////下載登打設定/////
  IF not GetKeyinSet Then
  begin
    Showmessage(_Msg('下載登打定位檔案時,網路發生錯誤!!')+HttpErrStr);
    DataLoading(False,False);
    Exit;
  end;
  ////下載登打設定/////
//ShowMessage('GetKeyinSet');
  if ScanDenialHint <> '' then   //有設定提示字串就秀在右上角
  begin
    DenialTimeLb.Visible := True;
    DenialTimeLb.Caption := Format(ScanDenialHint,[ScanDenialTime]);
  end;
//ShowMessage(IntToStr(ScanDpi));
  R_W_Scanini('R'); //掃瞄設定的ini
//ShowMessage(IntToStr(ScanDpi));
//ShowMessage('停掉DataLoading');
//DataLoading(False,False);
  ScanDuplexCB.Checked := ScanDuplex;
  if FMode <> 'SAMPLESCAN' then
    LoadImgFile;
  if (FMode = 'RSCAN') or (FMode = 'ESCAN') or (FMode = 'FSCAN') then
  begin
    if TreeView1.Items.Count > 0 then
    begin
      TreeView1.Selected := NewTreeNode.Item[0];
      TreeView1click(self);
    end;
  end;
  InitialOk := True;
  {AttFileGB.Visible := True; //附加電子檔窗  //20120207楊玉說不在這加電子檔先拿掉
  Splitter2.Visible := True;
  AttFileGB.Visible := False; //附加電子檔窗
  Splitter2.Visible := False; }
//ShowMessage('ImageSavePath='+ImageSavePath);
  DataLoading(False,False);
  LogFile1.LogToFile(logTimeString+'OCX初始化結束');
  LogFile1.LogToFile(logTimeString+'FUrl='+FUrl+
    ',FCaseID='+FCaseID+
    ',FMode='+FMode+
    ',FModeName='+FModeName+
    ',FWork_no='+FWork_no+
    ',FUserID='+FUserID+
    ',FUserName='+FUserName+
    ',FUserUnit='+FUserUnit+
    ',FData='+FData+
    ',FVerify='+FVerify+
    ',FReWrite='+FReWrite+
    ',FLanguage='+FLanguage+
    ',FLoanDoc_Value='+FLoanDoc_Value+
    ',FLoanDoc_Enable='+FLoanDoc_Enable+
    ',FUseProxy='+FUseProxy+
    ',FC_DocNoList='+FC_DocNoList+
    ',FC_DocNameList='+FC_DocNameList+
    ',FFixFileList='+FFixFileList+
    ',FIs_In_Wh='+FIs_In_Wh+
    ',FOldCaseInfo='+FOldCaseInfo+
    ',FPrintyn='+FPrintyn+
    ',FIs_OldCase='+FIs_OldCase+
    ',FCustDocYN='+FCustDocYN);
  LogFile1.LogToFile(logTimeString+'FImgDPI='+IntToStr(FImgDPI)+
    ',FScanColor='+    IntToStr(FScanColor)+
    ',FFileSizeLimit='+  IntToStr(FFileSizeLimit)+
    ',FCaseNoLength='+ IntToStr(FCaseNoLength)+
    ',FImgDelete='+FImgDelete+
    ',FIsExternal='+FIsExternal+
    ',FWH_category='+FWH_category+
    ',FCheck_main_form='+FCheck_main_form+
    ',FMaxUploadSize='+FMaxUploadSize);
end;
procedure TCB_IMGPSScanX.Timer2Timer(Sender: TObject);
begin
  IF Panel22.Caption = ShowText+'......' Then
    Panel22.Caption := ShowText
  Else
    Panel22.Caption := Panel22.Caption + '.';
  Application.ProcessMessages;
end;
procedure TCB_IMGPSScanX.Set_mode(const Value: WideString);
begin
  FMode := UpperCase(Value);
end;
procedure TCB_IMGPSScanX.Set_rewrite(const Value: WideString);
begin
  FReWrite := Value;
end;
procedure TCB_IMGPSScanX.Set_url(const Value: WideString);
begin
  FUrl := Value;
end;
procedure TCB_IMGPSScanX.Set_userid(const Value: WideString);
begin
  FUserID := Value;
end;
procedure TCB_IMGPSScanX.Set_username(const Value: WideString);
begin
  FUserName := Value;
end;
procedure TCB_IMGPSScanX.Set_verify(const Value: WideString);
begin
  FVerify := Value;
end;
procedure TCB_IMGPSScanX.Set_language(const Value: WideString);
begin
  FLanguage := lowercase(Value);
  if FLanguage='zh-tw' then
  begin
    FLanguage:='zh_tw'
  end;
  if FileExists(LngPath+'Language.lng') then
  begin
    InitialLanguage(Self);  //載入多國語言
  end;
end;
procedure TCB_IMGPSScanX.Set_modename(const Value: WideString);
begin
  FModeName := Value;
end;
procedure TCB_IMGPSScanX.Set_userunit(const Value: WideString);
begin
  FUserUnit := Value;
end;
procedure TCB_IMGPSScanX.Set_work_no(const Value: WideString);
begin
  FWork_no := Value;
end;
procedure TCB_IMGPSScanX.Set_loandoc_enable(const Value: WideString);
begin
  FLoanDoc_Enable := Value;
  if FLoanDoc_Enable = 'Y' then
    AddCredit1RG.Enabled := True;
  if FLoanDoc_Enable = 'I' then
  begin
    AddCredit1RG.Visible := False;
    Panel5.Visible := False;
  end;
end;
procedure TCB_IMGPSScanX.Set_loandoc_value(const Value: WideString);
begin
  FLoanDoc_Value := Value;
end;
procedure TCB_IMGPSScanX.Set_useproxy(const Value: WideString);
begin
  FUseProxy := UpperCase(Value);
  if FUseProxy = 'Y' then
    UseProxy := True;  //要不要用Proxy
end;
procedure TCB_IMGPSScanX.Set_c_docnamelist(const Value: WideString);
begin
  FC_DocNameList := Value;
end;
procedure TCB_IMGPSScanX.Set_is_in_wh(const Value: WideString);
begin
  FIs_In_Wh := UpperCase(Value);
end;
function TCB_IMGPSScanX.Get_c_docnamelist: WideString;
begin
end;
function TCB_IMGPSScanX.Get_is_in_wh: WideString;
begin
end;
function TCB_IMGPSScanX.Get_language: WideString;
begin
end;
function TCB_IMGPSScanX.Get_loandoc_enable: WideString;
begin
end;
function TCB_IMGPSScanX.Get_loandoc_value: WideString;
begin
end;
function TCB_IMGPSScanX.Get_mode: WideString;
begin
end;
function TCB_IMGPSScanX.Get_modename: WideString;
begin
end;
function TCB_IMGPSScanX.Get_rewrite: WideString;
begin
end;
function TCB_IMGPSScanX.Get_url: WideString;
begin
end;
function TCB_IMGPSScanX.Get_useproxy: WideString;
begin
end;
function TCB_IMGPSScanX.Get_userid: WideString;
begin
end;
function TCB_IMGPSScanX.Get_username: WideString;
begin
end;
function TCB_IMGPSScanX.Get_userunit: WideString;
begin
end;
function TCB_IMGPSScanX.Get_verify: WideString;
begin
end;
function TCB_IMGPSScanX.Get_work_no: WideString;
begin
end;
function TCB_IMGPSScanX.Get_printyn: WideString;
begin
end;
procedure TCB_IMGPSScanX.Set_printyn(const Value: WideString);
begin
  FPrintyn := UpperCase(Value);
end;
function TCB_IMGPSScanX.Get_custdocyn: WideString;
begin
end;
procedure TCB_IMGPSScanX.Set_custdocyn(const Value: WideString);
begin
  FCustDocYN := UpperCase(Value);
end;
function TCB_IMGPSScanX.Get_imgdelete: WideString;
begin
end;
procedure TCB_IMGPSScanX.Set_imgdelete(const Value: WideString);
begin
  FImgDelete:=Value;
end;
function TCB_IMGPSScanX.Get_isExternal: WideString;
begin
end;
procedure TCB_IMGPSScanX.Set_isExternal(const Value: WideString);
begin
  FIsExternal:=Value;
end;
function TCB_IMGPSScanX.Get_WH_CATEGORY: WideString;
begin
end;
procedure TCB_IMGPSScanX.Set_WH_CATEGORY(const Value: WideString);
begin
  FWH_category:=Value;
end;
separate/scanImp/CB_IMGPSScanImp_Utils.ts
比對新檔案
@@ -0,0 +1,59 @@
import * as fs from 'fs'; import * as path from 'path';
export enum TTransMode{tsHttp,tsFtp,tsNone} export enum TFtpProtocol{fpftp,fpftps} export type TImageFormat='ifBlackWhite'|'ifGray256'|'ifTrueColor'|'ifColor256';
export class TStringList {
  Strings:string[]=[]; get Count(){return this.Strings.length;}
  Add(s:string){this.Strings.push(s);} Clear(){this.Strings=[];}
  LoadFromFile(fp:string){if(fs.existsSync(fp))this.Strings=fs.readFileSync(fp,'utf8').split(/\r?\n/).filter(Boolean);}
  SaveToFile(fp:string){fs.writeFileSync(fp,this.Strings.join('\n'),'utf8');}
  Delete(idx:number){this.Strings.splice(idx,1);} Insert(idx:number,s:string){this.Strings.splice(idx,0,s);}
  IndexOf(s:string){return this.Strings.indexOf(s);} get Text(){return this.Strings.join('\n');}
  set Text(v:string){this.Strings=v.split('\n');} get CommaText(){return this.Strings.join(',');}
  set CommaText(v:string){this.Strings=v.split(',');} Free(){} Sort(){this.Strings.sort();} Assign(s:TStringList){this.Strings=[...s.Strings];}
}
export function applyUtilsMixins(cls:any){
  cls.prototype.GetCurrentVersionNo=function(){return "1.0.0";};
  cls.prototype.DefinePropertyPages=function(){};
  cls.prototype.InitExistImgList=function(cp:string){
    let ST1=new TStringList(); this.ExistImgList.Clear();
    ST1.LoadFromFile(path.join(cp,'Download','Context.dat'));
    for(let i=0;i<ST1.Count;i++){ let m=this.LoadFileGetMD5(path.join(cp,'Download',ST1.Strings[i])); this.LogFile1.LogToFile(this.logTimeString()+path.join(cp,'Download',ST1.Strings[i])+',MD5='+m); this.ExistImgList.Add(m); }
    this.LogFile1.LogToFile(this.logTimeString()+'ExistImgList.text'+this.ExistImgList.CommaText); ST1.Free();
  };
  cls.prototype.Initialize=function(){
    ['Activate','Click','Create','DblClick','Deactivate','Destroy','KeyPress','MouseEnter','MouseLeave','Paint'].forEach(e=>this['On'+e]=this[e+'Event']?this[e+'Event'].bind(this):null);
    this.MpsKey='fbim';this.Seg=3;this.Ext='.tif';this.SafePixel=20;this.CaseIDLength=16;this.FormIDLength=15;this.Bt=4;this.CropBarcode='CC';
  };
  cls.prototype.ISB1Enter=function(){this.ISB1.SetFocus();};
  cls.prototype._Set_Font=function(v:any){this.Font=v;};
  ['Active','AlignDisabled','AlignWithMargins','AutoScroll','AutoSize','AxBorderStyle','Caption','Color','DockSite','DoubleBuffered','DropTarget','Enabled','ExplicitHeight','ExplicitLeft','ExplicitTop','ExplicitWidth','Font','HelpFile','KeyPreview','MouseInClient','ParentCustomHint','ParentDoubleBuffered','PixelsPerInch','PopupMode','PrintScale','Scaled','ScreenSnap','SnapBuffer','UseDockManager','Visible','VisibleDockClientCount'].forEach(p=>{
    cls.prototype['Get_'+p]=function(){return this[p];}; cls.prototype['Set_'+p]=function(v:any){this[p]=v;};
  });
  cls.prototype.LoadFileGetMD5=function(f:string){return "md5stub";};
  cls.prototype.ISExistImg=function(f:string){return this.ExistImgList.IndexOf(this.LoadFileGetMD5(f))!==-1;};
  cls.prototype.MemoInfoTransfer=function(m:string,s:string,id:TStringList,n:TStringList){
    if(m==='ID'){for(let i=0;i<id.Count;i++)if(s===id.Strings[i])return n.Strings[i]; return '自行輸入';}
    if(m==='NAME'){for(let i=0;i<n.Count;i++)if(s===n.Strings[i])return id.Strings[i]; return '00';} return '';
  };
  cls.prototype.SetSQLData=function(c:string,f:TStringList,t:TStringList){t.Clear();t.Add(c);for(let i=1;i<f.Count;i++)t.Add(f.Strings[i]);};
  cls.prototype.GetSQLData=function(t:TStringList,c:string,no:number){
    if(t.Count===0||no>=t.Count)return''; let cs=t.Strings[0].split(','),ts=t.Strings[no],ds=ts.split('!@!');
    let idx=cs.indexOf(c); return(idx!==-1&&idx<ds.length)?ds[idx]:'';
  };
  cls.prototype.FindSQLData=function(t:TStringList,c:string,kc:string,ks:string,no:number,res:TStringList){
    res.Clear(); if(!ks||t.Count<=1)return false;
    let cl=c.split(','),kcl=kc.split(','),kl=ks.split(','),fidx=-1,found=false;
    if(no===0){for(let i=1;i<t.Count;i++){let m=true;for(let n=0;n<kcl.length;n++)if(this.GetSQLData(t,kcl[n],i)!==kl[n]){m=false;break;}if(m){found=true;fidx=i;break;}}}
    else{let m=true;for(let n=0;n<kcl.length;n++)if(t.Strings[no].indexOf('!@!'+kl[n]+'!@!')===-1){m=false;break;}if(m){found=true;fidx=no;}}
    if(found)cl.forEach(co=>res.Add(co+','+this.GetSQLData(t,co,fidx))); return found;
  };
  cls.prototype.GetFindResult=function(c:string){for(let i=0;i<this.FindResult.Count;i++){let s=this.FindResult.Strings[i],idx=s.indexOf(',');if(idx>-1&&c===s.substring(0,idx))return s.substring(idx+1);}return'';};
  cls.prototype.FileName2FormCode=function(f:string){let b=path.basename(f),v=b.indexOf('_'),v1=b.lastIndexOf('.');return(v>-1&&v1>v)?b.substring(v+1,v1):'';};
  cls.prototype.FormCode2DocNo=function(fc:string){for(let i=0;i<this.FormID_List.Count;i++)if(this.FormID_List.Strings[i]===fc)return this.DocNo_List.Strings[i];return fc?fc.substring(0,8):'';};
  cls.prototype.FormCode2Version=function(fc:string){return fc.substring(10,15);};
  cls.prototype.FormCode2Page=function(fc:string){return fc.substring(8,10);};
  cls.prototype.DocNoDir2DocNo=function(d:string){if(d!=='Attach'&&d!=='S_Attach'){let v=d.indexOf('(');if(v>0)return d.substring(0,v);}return d;};
  cls.prototype.DocNo2DocNoDir=function(p:string,d:string){if(!d)return this.AttName;let i=0,id='';do{i++;id=d+'('+i+')';}while(fs.existsSync(path.join(p,id)));return id;};
  cls.prototype.Add_Zoo=function(n:number,l:number){return n.toString().padStart(l,'0');};
  cls.prototype.Str2Dir=function(p:string){if(!fs.existsSync(p))fs.mkdirSync(p,{recursive:true});};
  cls.prototype._DelTree=function(p:string){if(fs.existsSync(p))fs.rmSync(p,{recursive:true,force:true});};
}
split_pascal.js
比對新檔案
@@ -0,0 +1,98 @@
const fs = require('fs');
const path = require('path');
// 檔案路徑設定
const sourceFile = path.resolve(__dirname, 'CB_IMGPSScanImp.pas');
const outDir = path.resolve(__dirname, 'separate', 'scanImp');
if (!fs.existsSync(sourceFile)) {
    console.error("找不到原始檔案:", sourceFile);
    process.exit(1);
}
if (!fs.existsSync(outDir)) {
    fs.mkdirSync(outDir, { recursive: true });
}
const content = fs.readFileSync(sourceFile, 'utf8');
const implMatch = content.match(/\bimplementation\b/i);
const initMatch = content.match(/\binitialization\b/i);
if (!implMatch || !initMatch) {
    console.error("無法在檔案中找到 implementation 或 initialization 區段。");
    process.exit(1);
}
const headerPart = content.substring(0, implMatch.index + implMatch[0].length);
let implPart = content.substring(implMatch.index + implMatch[0].length, initMatch.index);
const footerPart = content.substring(initMatch.index);
const usesMatch = implPart.match(/^\s*uses[\s\S]*?;/im);
let usesClause = "";
if (usesMatch) {
    usesClause = usesMatch[0];
    implPart = implPart.substring(usesMatch.index + usesMatch[0].length);
}
// 提取並保留不在方法內的實作開頭,例如 {$R *.DFM} 或是 { TCB_IMGPSScanX }
const methodRegex = /^(?:procedure|function)\s+TCB_IMGPSScanX\./im;
let otherImpl = "";
const firstMethodMatch = implPart.match(methodRegex);
if (firstMethodMatch) {
    otherImpl = implPart.substring(0, firstMethodMatch.index);
    implPart = implPart.substring(firstMethodMatch.index);
}
// 切割所有方法
const methodSplitRegex = /(?=^(?:procedure|function)\s+TCB_IMGPSScanX\.)/im;
const methods = implPart.split(methodSplitRegex).filter(m => m.trim().length > 0);
const uiMethods = [];
const scanMethods = [];
const dataMethods = [];
const utilMethods = [];
methods.forEach(m => {
    const nameMatch = m.match(/^(?:procedure|function)\s+TCB_IMGPSScanX\.([A-Za-z0-9_]+)/i);
    if (nameMatch) {
        const name = nameMatch[1].toLowerCase();
        if (/(click|mouse|key|drag|scroll|event|menu|btn|form|node|tree|view|activate|paint|resize)/.test(name)) {
            uiMethods.push(m);
        } else if (/(scan|acquire|image|graphic|twain|dpi|color|deskew|rotate|crop|smooth|point)/.test(name)) {
            scanMethods.push(m);
        } else if (/(ftp|http|trans|load|sql|server|setinf|data|ask|log|file|path|zip|dir|docno|formid|case)/.test(name)) {
            dataMethods.push(m);
        } else {
            utilMethods.push(m);
        }
    } else {
        utilMethods.push(m);
    }
});
fs.writeFileSync(path.join(outDir, 'CB_IMGPSScanImp_UI.pas'), uiMethods.join(''), 'utf8');
fs.writeFileSync(path.join(outDir, 'CB_IMGPSScanImp_Scan.pas'), scanMethods.join(''), 'utf8');
fs.writeFileSync(path.join(outDir, 'CB_IMGPSScanImp_Data.pas'), dataMethods.join(''), 'utf8');
fs.writeFileSync(path.join(outDir, 'CB_IMGPSScanImp_Utils.pas'), utilMethods.join(''), 'utf8');
const mainFile = [
    headerPart,
    usesClause,
    otherImpl,
    "{$I CB_IMGPSScanImp_UI.pas}",
    "{$I CB_IMGPSScanImp_Scan.pas}",
    "{$I CB_IMGPSScanImp_Data.pas}",
    "{$I CB_IMGPSScanImp_Utils.pas}",
    footerPart
].join('');
fs.writeFileSync(path.join(outDir, 'CB_IMGPSScanImp_Main.pas'), mainFile, 'utf8');
console.log("拆分完成!產生的五個檔案如下:");
console.log("1. CB_IMGPSScanImp_Main.pas (主結構與引用)");
console.log("2. CB_IMGPSScanImp_UI.pas (UI事件相關:", uiMethods.length, "個方法)");
console.log("3. CB_IMGPSScanImp_Scan.pas (掃描影像相關:", scanMethods.length, "個方法)");
console.log("4. CB_IMGPSScanImp_Data.pas (資料存取與網路傳輸:", dataMethods.length, "個方法)");
console.log("5. CB_IMGPSScanImp_Utils.pas (工具與其他邏輯:", utilMethods.length, "個方法)");