# # PDF 文档比较

从 8.5.0 版本开始，Foxit PDF SDK for Web 完整包 (full package) 提供了 API 来对比 PDF 文档，可以比较两个 PDF 文档中文本、注释、页面对象 (图片、路径) 和水印之间的差异。通常来说，使用比较接口来对比 PDF 文档，首先需要输入两个不同的文档，然后返回一个包含详细差异信息的结果文档，最后打开结果文档可以直观地看出两个文档的差异信息。接下来将详细介绍该 API 的用法。

## # 接口预览

https://developers.foxit.com/developer-hub/document/developer-guide-pdf-sdk-web
(opens new window)

## # 一个简单的示例

本节将提供一个简单的示例来展示如何使用文档比较接口来比较两个文档，以及控制结果文档中的内容显示。下面的示例将直接基于一个创建好的 `PDFViewer` 实例进行。

### # 加载文档

在开始之前，先使用 `PDFViewer.loadPDFDocByFile`
(opens new window) 或者 `PDFViewer.loadPDFDocByHttpRangeRequest`
(opens new window) 接口加载两个文档。这两个接口的作用都是加载 PDF 文档，然后返回 `PDFDoc` 对象，而不会在视图上渲染文档。

    const baseDoc = await pdfViewer.loadPDFDocByHttpRangeRequest({
        range: {
            url: '/assets/compare-base.pdf'
        }
    });
    const otherDoc = await pdfViewer.loadPDFDocByHttpRangeRequest({
        range: {
            url: '/assets/compare-other.pdf'
        }
    });

### # 开始对比文档

加载文档后，只需要通过 `PDFDoc.getId`
(opens new window) 接口获取文档的 id 值，然后就可以开始对比文档了。

    const baseDocId = baseDoc.getId();
    const otherDocId = otherDoc.getId();

    const comparedDoc = await pdfViewer.compareDocuments(
        baseDocId,
        otherDocId,
        {
            // The file name of baseDoc, which will be displayed in the result document.
            baseFileName: 'baseFile.pdf',
            // The file name of otherDoc, which will be displayed in the result document.
            otherFileName: 'otherFile.pdf',
            // The file name of the result document.
            resultFileName: pdfViewer.i18n.t('comparison:resultFileName') || 'The result of comparison.pdf'
        }
    );

效果预览：

    <script>
        const libPath = window.top.location.origin + '/lib';
        const pdfui = new UIExtension.PDFUI({
            viewerOptions: {
                libPath: libPath,
                jr: {
                    fontPath: 'http://webpdf.foxitsoftware.com/webfonts',
                    licenseSN: licenseSN,
                    licenseKey: licenseKey
                }
            },
            renderTo: document.body,
            appearance: UIExtension.appearances.adaptive,
            addons: libPath + '/uix-addons/allInOne.js'
        });
        (async function() {
            const pdfViewer = await pdfui.getPDFViewer();
            const baseDoc = await pdfViewer.loadPDFDocByHttpRangeRequest({
                range: {
                    url: '/assets/compare-base.pdf'
                }
            });
            const otherDoc = await pdfViewer.loadPDFDocByHttpRangeRequest({
                range: {
                    url: '/assets/compare-other.pdf'
                }
            });
            const baseDocId = baseDoc.getId();
            const otherDocId = otherDoc.getId();

            const comparedDoc = await pdfViewer.compareDocuments(
                baseDocId,
                otherDocId,
                {
                    baseFileName: 'baseFile.pdf',
                    otherFileName: 'otherFile.pdf',
                    resultFileName: pdfViewer.i18n.t('comparison:resultFileName') || 'The result of comparison.pdf'
                }
            );
            const comparedDocFile = await comparedDoc.getFile();
            pdfui.openPDFByFile(comparedDocFile);
        })()
    </script>

    {
        "iframeOptions": {
            "style": "height: 600px"
        }
    }

## # compareDocuments 接口参数

### # 页面范围

通过以下两个参数，可以指定待比较的两个文档的页面范围：

    pdfViewer.compareDocuments(
        baseDocId,
        otherDocId,
        {
            // ...
            basePageRange: {
                from: 0,
                end: 2
            },
            otherPageRange: {
                from: 1,
                end: 3
            },
            options: {
                // ...
            }
        }
    )

`basePageRange` 和 `otherPageRange` 对象都必须包括 `from` 和 `end` 属性，这两个属性分别代表待比较页面的起始页和结束页索引。在上述示例代码中，最终待比较的页面是： baseDoc: `[0, 1, 2]`，otherDoc: `[1, 2, 3]`。需要注意的是，`basePageRange` 和 `otherPageRange` 指定的页数必须相同。

### # options 参数

options 用于指定待比较的对象和比较方法，其中各个参数解释如下：

    // Whether to compare tables, default is false, which means not to compare.
    compareTable: false,
    // Whether to detect page deletions and insertions, default is false, which means not to detect.
    detectPage: false,
    // The width of the outline border of the marker, and the unit is point.
    lineThickness: {
        delete: 2,
        insert: 2,
        replace: 2
    },
    // This parameter is used to set the marker color for different types of differences. The format of marker color is 0xRRGGBB without transparent channel, and the transparency needs to be specified in opacity.
    markingColor: {
        delete: 0xfa0505,
        insert: 0x149bff,
        replace: 0xffcc00
    },
    // This parameter is used to set the transparency of different types of differences.
    opacity: {
        delete: 100,
        insert: 100,
        replace: 100
    },
    // Whether only to compare text differences. If the value is true, only compare the text differences, otherwise compare the annotations, page oobjects (include text), and so on.
    textOnly: false

以下是使用上述参数的示例：

    <script>
        const libPath = window.top.location.origin + '/lib';
        const pdfui = new UIExtension.PDFUI({
            viewerOptions: {
                libPath: libPath,
                jr: {
                    fontPath: 'http://webpdf.foxitsoftware.com/webfonts',
                    licenseSN: licenseSN,
                    licenseKey: licenseKey
                }
            },
            renderTo: document.body,
            appearance: UIExtension.appearances.adaptive,
            addons: libPath + '/uix-addons/allInOne.js'
        });
        class ComparisonOptionsLayerComponent extends UIExtension.SeniorComponentFactory.createSuperClass({
            template: `
                <layer class="center fv__ui-comparison-options-dialog" @var.dialog="$component" style="width: 680px" append-to="body">
                    <layer-view class="fv__ui-comparison-dialog-body">
                        <div class="fv__ui-layout-row">
                            <div class="fv__ui-layout-col-1">
                                <fieldset class="fv__ui-comparison-fieldset">
                                    <legend>comparison:options-dialog.include</legend>
                                    <div class="fv__ui-comparison-fieldset-content">
                                        <form-group label="comparison:options-dialog.compareTextOnly" direction="rtl">
                                            <checkbox  @model="dialog.currentOptions.textOnly" name="compareTextOnly"></checkbox>
                                        </form-group>
                                        <form-group label="comparison:options-dialog.compareTable" direction="rtl">
                                            <checkbox  @model="dialog.currentOptions.compareTable" name="compareTable"></checkbox>
                                        </form-group>
                                        <form-group label="comparison:options-dialog.detectPage" direction="rtl">
                                            <checkbox  @model="dialog.currentOptions.detectPage" name="detectPageDeletionsOrInserts"></checkbox>
                                        </form-group>
                                    </div>
                                </fieldset>
                            </div>
                            <div class="fv__ui-layout-col-1">
                                <fieldset class="fv__ui-comparison-fieldset">
                                    <legend>comparison:options-dialog.markingColor</legend>
                                    <div class="fv__ui-comparison-fieldset-content">
                                        <form-group label="comparison:options-dialog.replaceObjects">
                                            <inline-color-picker name="fv--comparison-options-replace-marking-color" @model="dialog.currentOptions.markingColor.replace|comparison:color"></inline-color-picker>
                                        </form-group>
                                        <form-group label="comparison:options-dialog.insertObjects">
                                            <inline-color-picker name="fv--comparison-options-insert-marking-color" @model="dialog.currentOptions.markingColor.insert|comparison:color"></inline-color-picker>
                                        </form-group>
                                        <form-group label="comparison:options-dialog.deleteObjects">
                                            <inline-color-picker name="fv--comparison-options-delete-marking-color" @model="dialog.currentOptions.markingColor.delete|comparison:color"></inline-color-picker>
                                        </form-group>
                                    </div>
                                </fieldset>
                            </div>
                        </div>
                        <div class="fv__ui-layout-row">
                            <div class="fv__ui-layout-col-1">
                                <fieldset class="fv__ui-comparison-fieldset">
                                    <legend>comparison:options-dialog.opacity</legend>
                                    <div class="fv__ui-comparison-fieldset-content fv__ui-comparison-options-checkbox-list">
                                        <form-group label="comparison:options-dialog.replaceObjects">
                                            <number type="number" min="0" max="100" step="1" suffix="%" @model="dialog.currentOptions.opacity.replace" name="fv--comparison-options-replace-opacity-replace"></number>
                                        </form-group>
                                        <form-group label="comparison:options-dialog.insertObjects">
                                            <number type="number" min="0" max="100" step="1" suffix="%" @model="dialog.currentOptions.opacity.insert" name="fv--comparison-options-replace-opacity-insert"></number>
                                        </form-group>
                                        <form-group label="comparison:options-dialog.deleteObjects">
                                            <number type="number" min="0" max="100" step="1" suffix="%" @model="dialog.currentOptions.opacity.delete" name="fv--comparison-options-replace-opacity-delete"></number>
                                        </form-group>
                                    </div>
                                </fieldset>
                            </div>
                            <div class="fv__ui-layout-col-1">
                                <fieldset class="fv__ui-comparison-fieldset">
                                    <legend>comparison:options-dialog.lineThickness</legend>
                                    <div class="fv__ui-comparison-fieldset-content fv__ui-comparison-options-checkbox-list">
                                        <form-group label="comparison:options-dialog.replaceObjects">
                                            <number type="number" min="1" max="12" step="1" @model="dialog.currentOptions.lineThickness.replace"  name="fv--comparison-options-replace-lineThickness-replace"></number>
                                        </form-group>
                                        <form-group label="comparison:options-dialog.insertObjects">
                                            <number type="number" min="1" max="12" step="1" @model="dialog.currentOptions.lineThickness.insert"  name="fv--comparison-options-replace-lineThickness-insert"></number>
                                        </form-group>
                                        <form-group label="comparison:options-dialog.deleteObjects">
                                            <number type="number" min="1" max="12" step="1" @model="dialog.currentOptions.lineThickness.delete"  name="fv--comparison-options-replace-lineThickness-delete"></number>
                                        </form-group>
                                    </div>
                                </fieldset>
                            </div>
                        </div>
                    </layer-view>
                    <div class="fv__ui-comparison-dialog-footer fv__ui-layout-row">
                        <div class="fv__ui-layout-col-1 fv__ui-comparison-footer-buttons">
                            <xbutton text="dialog.ok" class="fv__ui-dialog-button fv__ui-dialog-ok-button" name="fv__ui-dialog-ok-button" @on.click="dialog.ok()"></xbutton>
                            <xbutton text="dialog.cancel" class="fv__ui-dialog-button fv__ui-dialog-cancel-button" name="fv__ui-dialog-cancel-button" @on.click="dialog.cancel()" @cannotBeDisabled></xbutton>
                        </div>
                    </div>
                </layer>
            `
        }) {
            static getName() {
                return 'comparison-options-layer'
            }
            currentOptions = this.getDefaultOptions();

            getDefaultOptions() {
                return {
                    compareTable: false,
                    detectPage: false,
                    lineThickness: {
                        delete: 2,
                        insert: 2,
                        replace: 2
                    },
                    markingColor: {
                        delete: 0xfa0505,
                        insert: 0x149bff,
                        replace: 0xffcc00
                    },
                    opacity: {
                        delete: 100,
                        insert: 100,
                        replace: 100
                    },
                    textOnly: false
                };
            }
            onOk(callback) {
                this.onOkCallback = callback;
            }
            ok() {
                if(typeof this.onOkCallback == 'function') {
                    this.onOkCallback(this.currentOptions);
                }
                this.hide();
                this.currentOptions = this.getDefaultOptions();
            }
            cancel() {
                this.hide();
                this.currentOptions = this.getDefaultOptions();
            }
        }

        UIExtension.modular.root().registerComponent(ComparisonOptionsLayerComponent);

        class LegendLayerComponent extends UIExtension.SeniorComponentFactory.createSuperClass({
            template: `
            <layer class="fv__ui-comparison-legend-layer">
            <header class="fv__ui-layout-row fv__ui-comparison-legend-header" name="fv__ui-comparison-legend-header">
                <h2 class="fv__ui-layer-col-1 fv__ui-comparison-legend-title">comparison:toolbar.legend</h2>
                <button class="fv__ui-layer-col-1 fv__ui-comparison-legend-close-btn" @on.native.click="hide()"></button>
            </header>
            test
            <dl class="fv__ui-comparison-legend-item fv__ui-comparison-legend-replace-item" name="fv__ui-comparison-legend-replace-item">
                <dt >ABC</dt>
                <dd>comparison:toolbar.replace</dd>
            </dl>
            <dl class="fv__ui-comparison-legend-item fv__ui-comparison-legend-insert-item" name="fv__ui-comparison-legend-insert-item">
                <dt>ABC<span class="fv__ui-comparison-legend-item-insert-mark"></span></dt>
                <dd>comparison:toolbar.insert</dd>
            </dl>
            <dl class="fv__ui-comparison-legend-item fv__ui-comparison-legend-delete-item" name="fv__ui-comparison-legend-delete-item">
                <dt>ABC<span class="fv__ui-comparison-legend-item-delete-mark"></span></dt>
                <dd>comparison:toolbar.delete</dd>
            </dl>
        </layer>`
        }) {
            static getName() {
                return 'legend-layer'
            }
            constructor(...args) {
                super(...args);
            }
            show(options){
                super.show();
                const colorConvertor = UIExtension.PDFViewCtrl.shared.colorConvertor;
                this.element.querySelector(".fv__ui-comparison-legend-replace-item dt").style.textDecorationColor = colorConvertor(options.markingColor.replace,"#");
                this.element.querySelector(".fv__ui-comparison-legend-insert-item dt").style.textDecorationColor = colorConvertor(options.markingColor.insert,"#");
                this.element.querySelector(".fv__ui-comparison-legend-insert-item .fv__ui-comparison-legend-item-insert-mark").style.color = colorConvertor(options.markingColor.insert,"#");
                this.element.querySelector(".fv__ui-comparison-legend-delete-item dt").style.textDecorationColor = colorConvertor(options.markingColor.delete,"#");
                this.element.querySelector(".fv__ui-comparison-legend-delete-item .fv__ui-comparison-legend-item-delete-mark").style.color = colorConvertor(options.markingColor.delete,"#");
            }
        }

        UIExtension.modular.root().registerComponent(LegendLayerComponent);

        (async function() {
            const root = await pdfui.getRootComponent();
            root.append('<comparison-options-layer visible>');
            root.append('<legend-layer>');

            const pdfViewer = await pdfui.getPDFViewer();
            const baseDoc = await pdfViewer.loadPDFDocByHttpRangeRequest({
                range: {
                    url: '/assets/compare-base.pdf'
                }
            });
            const otherDoc = await pdfViewer.loadPDFDocByHttpRangeRequest({
                range: {
                    url: '/assets/compare-other.pdf'
                }
            });
            const baseDocId = baseDoc.getId();
            const otherDocId = otherDoc.getId();

            const optionsLayer = root.querySelector('@comparison-options-layer');
            optionsLayer.onOk(async options => {
                const comparedDoc = await pdfViewer.compareDocuments(
                    baseDocId,
                    otherDocId,
                    {
                        baseFileName: 'baseFile.pdf',
                        otherFileName: 'otherFile.pdf',
                        resultFileName: pdfViewer.i18n.t('comparison:resultFileName') || 'The result of comparison.pdf',
                        options: options
                    }
                );
                console.log(options);
                const comparedDocFile = await comparedDoc.getFile();
                pdfui.openPDFByFile(comparedDocFile).then(_=>{
                    const legendLayer = root.querySelector('@comparison:legend-layer')
                    legendLayer.show(options);
                });
            });
        })()
    </script>

    {
        "iframeOptions": {
            "style": "height: 600px"
        }
    }

### # 比较文档的进度

当待比较的文档比较大时，生成结果文档需要花费较多的时间。此时，`compareDocuments` 方法的第四个参数可以接受一个回调函数，用于获取处理进度信息：

    pdfViewer.compareDocuments(
        baseDocId,
        otherDocId,
        {
            ...
        },
        (currentRate) => {
            console.log(currentRate);
        }
    )

`currentRate` 的取值范围为 0~100。您可以使用这个值在 UI 界面上更新进度条。

带有进度条的示例:

    <script>
        const libPath = window.top.location.origin + '/lib';
        const pdfui = new UIExtension.PDFUI({
            viewerOptions: {
                libPath: libPath,
                jr: {
                    fontPath: 'http://webpdf.foxitsoftware.com/webfonts',
                    licenseSN: licenseSN,
                    licenseKey: licenseKey
                }
            },
            renderTo: document.body,
            appearance: UIExtension.appearances.adaptive,
            addons: libPath + '/uix-addons/allInOne.js'
        });
        class ProgressBarComponent extends UIExtension.SeniorComponentFactory.createSuperClass({
            template: `<layer class="center" visible @var.self="$component">
                @{self.currentRate + '%'}
            </layer>`
        }) {
            static getName() {
                return 'progress-bar-layer'
            }
            currentRate = 0;
            setCurrentRate(rate) {
                this.currentRate = rate;
                this.digest();
                if(rate >= 100) {
                    setTimeout(() => {
                        this.hide();
                    }, 500);
                }
            }
        }

        UIExtension.modular.root().registerComponent(ProgressBarComponent);

        (async function() {
            const pdfViewer = await pdfui.getPDFViewer();
            const baseDoc = await pdfViewer.loadPDFDocByHttpRangeRequest({
                range: {
                    url: '/assets/compare-base.pdf'
                }
            });
            const otherDoc = await pdfViewer.loadPDFDocByHttpRangeRequest({
                range: {
                    url: '/assets/compare-other.pdf'
                }
            });
            const baseDocId = baseDoc.getId();
            const otherDocId = otherDoc.getId();

            const rootComponent = await pdfui.getRootComponent();
            rootComponent.append('<progress-bar-layer>');

            const comparedDoc = await pdfViewer.compareDocuments(
                baseDocId,
                otherDocId,
                {
                    baseFileName: 'baseFile.pdf',
                    otherFileName: 'otherFile.pdf',
                    resultFileName: pdfViewer.i18n.t('comparison:resultFileName') || 'The result of comparison.pdf'
                },
                currentRate => {
                    rootComponent.querySelector('@progress-bar-layer').setCurrentRate(currentRate);
                }
            );
            const comparedDocFile = await comparedDoc.getFile();
            pdfui.openPDFByFile(comparedDocFile);
        })()
    </script>

    {
        "iframeOptions": {
            "style": "height: 600px"
        }
    }

## # 判断文档是否为比较结果文档

The dictionary information of the PDF file generated by the `PDFViewer.compareDocuments` interface can be used to determine whether a document is a comparison result document. In the SDK, it uses `PDFDoc.isCompareDoc()` method.

`PDFViewer.compareDocuments` 接口生成的 PDF 文件的字典信息可以用于判定文档是否为比较结果文档。在 SDK 中，则通过 `PDFDoc.isCompareDoc()` 接口来判定。

    pdfViewer.eventEmitter.on(PDFViewCtrl.ViewerEvent.openFileSuccess, doc => {
        doc.isCompareDoc();
    })

下面是一个典型的比较结果文档的字典信息，对象 (1 0) 末尾处的 /PieceInfo 指向了 (244 0) 对象，也就是指向了 /ComparePDF 字典项， 我们可以据此判定一个文档是否为比较结果文档。

    1 0 obj
    <</AcroForm 110 0 R/Pages 2 0 R/ViewerPreferences <<>>/OCProperties <</OCGs [62 0 R 63 0 R 64 0 R 65 0 R 66 0 R 67 0 R 68 0 R]/D <</Order [62 0 R 63 0 R 64 0 R 65 0 R 66 0 R 67 0 R 68 0 R]/ON [62 0 R 63 0 R 64 0 R]/OFF [65 0 R 66 0 R 67 0 R 68 0 R]>>>>/Names 367 0 R/PageLayout(TwoColumnLeft)/Type/Catalog/PieceInfo 244 0 R>>
    endobj
    ...

    244 0 obj
    <</ComparePDF 235 0 R>>
    endobj
    ...
    235 0 obj
    <</Private 236 0 R>>
    endobj
    236 0 obj
    <</Differences 237 0 R>>
    endobj
    237 0 obj
    <</Nums [1 238 0 R 2 239 0 R 3 240 0 R 4 241 0 R 5 242 0 R 6 243 0 R]>>
    endobj
    ...