Skip to content

PDF 文档比较

从 8.5.0 版本开始,Foxit PDF SDK for Web 完整包 (full package) 提供了 API 来对比 PDF 文档,可以比较两个 PDF 文档中的:

  • 文本
  • 注释
  • 页面对象(图片、路径)
  • 水印

TIP

使用比较接口时,需要:

  1. 输入两个不同的文档
  2. 获取包含详细差异信息的结果文档
  3. 打开结果文档查看差异信息

接口预览

详细 API 说明请参考:开发者指南

一个简单的示例

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

加载文档

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

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

开始对比文档

加载文档后,只需要通过 PDFDoc.getId 接口获取文档的 id 值,然后就可以开始对比文档了。

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

const comparedDoc = await pdfViewer.compareDocuments(
    baseDocId,
    otherDocId,
    {
        // baseDoc 的文件名,将显示在结果文档中
        baseFileName: 'baseFile.pdf',
        // otherDoc 的文件名,将显示在结果文档中
        otherFileName: 'otherFile.pdf',
        // 结果文档的文件名
        resultFileName: pdfViewer.i18n.t('comparison:resultFileName') || 'The result of comparison.pdf'
    }
);

效果预览

javascript
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);
})()
json
{
  "iframeOptions": {
    "style": "height: 600px"
  }
}

compareDocuments 接口参数

页面范围

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

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

页面范围说明

  • fromend 属性分别代表起始页和结束页索引
  • 示例中待比较的页面为:
    • baseDoc: [0, 1, 2]
    • otherDoc: [1, 2, 3]
  • basePageRangeotherPageRange 指定的页数必须相同

options 参数

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

javascript
{
    // 是否比较表格,默认为 false
    compareTable: false,
        // 是否检测页面删除和插入,默认为 false
        detectPage
:
    false,
        // 标记线条粗细(单位:点)
        lineThickness
:
    {
        delete
    :
        2,
            insert
    :
        2,
            replace
    :
        2
    }
,
    // 标记颜色(格式:0xRRGGBB,不含透明通道)
    markingColor: {
        delete
    :
        0xfa0505,
            insert
    :
        0x149bff,
            replace
    :
        0xffcc00
    }
,
    // 标记透明度
    opacity: {
        delete
    :
        100,
            insert
    :
        100,
            replace
    :
        100
    }
,
    // 是否仅比较文本差异,true 则只比较文本,false 则比较注释、页面对象等
    textOnly: false
}

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

html

<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, "#");
        }
    }

    const module = UIExtension.modular.module("comparison");
    const registry = module.getRegistry();
    registry.registerComponent(LegendLayerComponent, true);

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

        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>
json
{
  "iframeOptions": {
    "style": "height: 600px"
  }
}

比较文档的进度

TIP

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

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

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

带有进度条的示例:

html

<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>
json
{
  "iframeOptions": {
    "style": "height: 600px"
  }
}

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

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

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

TIP

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

text
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
...