本文由Vildan Softic进行同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

在Web应用程序中处理PDF文件一直很麻烦。 如果幸运的话,您的用户只需要下载文件。 但是,有时您的用户需要更多。 过去,我很幸运,但是这次,我们的用户需要我们的应用程序显示PDF文档,以便他们可以保存与每个页面相关的元数据。 以前,可能是通过运行在浏览器中的昂贵的PDF插件(例如Adobe Reader)来实现的。 但是,经过一段时间和试验,我发现了将PDF查看器集成到Web应用程序中的更好方法。 今天,我们将看一下如何使用Aurelia和PDF.js简化PDF处理。

概述:目标

今天,我们的目标是在Aurelia中构建PDF查看器组件,以允许查看器和我们的应用程序之间双向数据流。 我们有三个主要要求。

  1. 我们希望用户能够以不错的性能加载文档,滚动和放大和缩小。
  2. 我们希望能够将查看器属性(例如当前页面和当前缩放级别)双向绑定到我们应用程序中的属性。
  3. 我们希望该查看器成为可重用的组件; 我们希望能够将多个查看器同时拖放到我们的应用程序中,而不会发生冲突且花费很少的精力。

您可以在我们的GitHub存储库中找到本教程的代码,并在此处演示完成的代码 。

引入PDF.js

PDF.js是由Mozilla Foundation编写的JavaScript库。 它加载PDF文档,解析文件和关联的元数据,并将页面输出呈现到DOM节点(通常是<canvas>元素)。 该项目随附的默认查看器为Chrome和Firefox中的嵌入式PDF查看器提供了强大的动力,并且可以用作独立页面或资源(嵌入在iframe中)。

诚然,这很酷。 这里的问题是,默认查看器虽然具有很多功能,但被设计为可作为独立网页使用。 这意味着尽管可以将其集成到Web应用程序中,但实际上必须在iframe沙箱中运行。 默认查看器旨在通过其查询字符串获取配置输入,但是在初始加载后我们无法轻松更改配置,也无法轻松地从查看器获取信息和事件。 为了将其与Aurelia Web应用程序集成(完成事件处理和双向绑定),我们需要创建Aurelia自定义组件。

注意:如果需要有关PDF.js的复习,请查看我们的教程: 使用Mozilla的PDF.js在JavaScript中进行自定义PDF渲染

实施

为了实现我们的目标,我们将创建一个Aurelia自定义元素 。 但是,我们不会将默认查看器放入组件中。 相反,我们将创建连接到PDF.js核心和查看器库的自己的查看器,以便可以最大程度地控制可绑定属性和渲染。 对于我们的初始概念验证,我们将从框架Aurelia应用程序开始 。

样板

如您所见,如果您单击上面的链接,则骨架应用程序中包含许多文件,我们将不需要其中的许多文件。 为了简化生活,我们准备了骨骼的精简版,并在其中添加了以下内容:

  • Gulp任务,将我们的PDF文件复制到dist文件夹(Aurelia用于捆绑)。
  • PDF.js依赖项已添加到package.json
  • 在应用程序的根目录中, index.htmlindex.css已收到一些初始样式。
  • 我们将要处理的文件的空副本已添加。
  • 文件src/resources/elements/pdf-document.css包含一些自定义元素的CSS样式。

因此,让我们启动并运行该应用程序。

首先,确保在全球范围内安装了gulp和jspm:

npm install -g gulp jspm

然后克隆骨骼并将其放入cd

git clone git@github.com:sitepoint-editors/aurelia-pdfjs.git -b skeleton
cd aurelia-pdfjs

然后安装必要的依赖项:

npm install
jspm install -y

最后运行gulp watch并导航到http:// localhost:9000 。 如果一切按计划进行,您应该会看到一条欢迎消息。

一些更多的设置

接下来要做的是查找几个PDF并将它们放在src/documents 。 将它们命名为one.pdftwo.pdf 。 为了最大程度地测试我们的自定义组件,如果其中一个PDF确实很长(例如可以在Gutenberg Project上找到的War and Peace),则最好。

放置好PDF后,打开src/app.htmlsrc/app.js (按照惯例, App组件是根目录或Aurelia应用程序),然后用以下两个文件的内容替换其中的代码: src / app.html和src / app.js。 在本教程中,我们不会涉及这些文件,但是对代码进行了很好的注释。

Gulp会自动检测到这些更改,您应该会看到我们应用渲染的用户界面。 设置就是这样。 现在节目开始了……

创建一个Aurelia自定义元素

我们想要创建一个可在任何Aurelia视图中使用的嵌入式组件。 由于Aurelia视图只是包装在HTML5 模板标签内的HTML片段,因此示例如下所示:

<template><require from="resources/elements/pdf-document"></require><pdf-document url.bind="document.url"page.bind="document.pageNumber"lastpage.bind="document.lastpage"scale.bind="document.scale"></pdf-document>
</template>

<pdf-document>标记是自定义元素的示例。 它及其属性(例如scalepage )不是HTML固有的,但是我们可以使用Aurelia自定义元素来创建它。 使用Aurelia的基本构建块,可以轻松创建自定义元素:Views和ViewModels。 这样,我们将首先搭建名为pdf-document.js ViewModel,如下所示:

// src/resources/elements/pdf-document.jsimport {customElement, bindable, bindingMode} from 'aurelia-framework';@customElement('pdf-document')@bindable({ name: 'url' })
@bindable({ name: 'page', defaultValue: 1, defaultBindingMode: bindingMode.twoWay })
@bindable({ name: 'scale', defaultValue: 1, defaultBindingMode: bindingMode.twoWay })
@bindable({ name: 'lastpage', defaultValue: 1, defaultBindingMode: bindingMode.twoWay })export class PdfDocument {constructor () {// Instantiate our custom element.}detached () {// Aurelia lifecycle method. Clean up when element is removed from the DOM.}urlChanged () {// React to changes to the URL attribute value.}pageChanged () {// React to changes to the page attribute value.}scaleChanged () {// React to changes to the scale attribute value.}pageHandler () {// Change the current page number as we scroll}renderHandler () {// Batch changes to the DOM and keep track of rendered pages}
}

这里要注意的主要是@bindable装饰器; 通过使用配置defaultBindingMode: bindingMode.twoWay创建可绑定属性,并在我们的ViewModel中创建处理程序方法( urlChangedpageChanged等),我们可以监视对放置在自定义元素上的相关属性的更改并对它们做出反应。 这将使我们能够简单地通过更改元素的属性来控制PDF查看器。

然后,我们将创建初始视图以与ViewModel配对。

// src/resources/elements/pdf-document.html<template><require from="./pdf-document.css"></require><div ref="container" class="pdf-container">My awesome PDF viewer.</div>
</template>

整合PDF.js

PDF.js分为三个部分。 有一个核心库,用于处理PDF文档的解析和解释。 显示库,它在核心层的顶部构建了可用的API; 最后是Web查看器插件,它是我们之前提到的预构建网页。 为了我们的目的,我们将通过显示API使用核心库; 我们将建立自己的查看器。

显示API导出一个名为PDFJS的库对象,该对象允许我们设置一些配置变量并使用PDFJS.getDocument(url)加载文档。 该API是完全异步的-它发送和接收来自Web Worker的消息,因此它很大程度上基于JavaScript承诺。 我们主要将处理从PDFJS.getDocument()方法异步返回的PDFDocumentProxy对象,以及从PDFDocumentProxy.getPage()异步返回的PDFPageProxy对象。

尽管文档有点稀疏,但是PDF.js 在此处 ( demo )和here ( demo )都有一些用于创建基本查看器的示例 。 我们将在这些示例的基础上构建自定义组件。

网络工作者集成

PDF.js使用Web Worker卸载其渲染任务。 由于网络工作者在浏览器环境中运行的方式(它们已被有效地沙盒化),我们被迫使用JavaScript文件的直接文件路径而不是通常的模块加载器来加载网络工作者。 幸运的是,Aurelia提供了一个加载器抽象,因此我们不必引用静态文件路径(当捆绑应用程序时,它可能会更改)。

如果您正在遵循我们的repo版本,那么您将已经安装了pdfjs-dist软件包,否则,您现在需要这样做(例如,使用jspm jspm install npm:pdfjs-dist@^1.5.391 )。 然后,我们将使用Aurelia的依赖项注入模块注入Aurelia的加载器抽象,并使用加载器将Web Worker文件加载到构造函数中,如下所示:

// src/resources/elements/pdf-document.jsimport {customElement, bindable, bindingMode, inject, Loader} from 'aurelia-framework';
import {PDFJS} from 'pdfjs-dist';@customElement('pdf-document')... // all of our @bindables@inject(Loader)
export class PdfDocument {constructor (loader) {// Let Aurelia handle resolving the filepath to the worker.PDFJS.workerSrc = loader.normalizeSync('pdfjs-dist/build/pdf.worker.js');// Create a worker instance for each custom element instance.this.worker = new PDFJS.PDFWorker();}detached () {// Release and destroy our worker instance when the the PDF element is removed from the DOM.this.worker.destroy();}...
}

加载我们的页面

PDF.js库处理加载,解析和显示PDF文档。 它具有对部分下载和身份验证的内置支持。 我们要做的就是提供所讨论文档的URI,PDF.js将返回一个Promise对象,该对象解析为表示PDF文档及其元数据的JavaScript对象。

加载和显示PDF将由我们的可绑定属性驱动; 在这种情况下,它将是url属性。 实质上,当URL更改时,自定义元素应要求PDF.js对该文件进行请求。 我们将在urlChanged处理程序中执行此操作,并对构造函数进行一些更改以初始化一些属性,并对detached方法进行一些更改以进行清理。

对于文档的每一页,我们将在DOM中创建一个<canvas>元素,该元素位于具有固定高度的可滚动容器内。 为了实现这一点,我们将使用Repeater来使用Aurelia的基本模板功能。 由于每个PDF页面可以具有自己的大小和方向,因此我们将基于PDF页面视口设置每个canvas元素的宽度和高度。

这是我们的观点:

// src/resources/elements/pdf-document.html<template><require from="./pdf-document.css"></require><div ref="container" id.bind="fingerprint" class="pdf-container"><div repeat.for="page of lastpage" class="text-center"><canvas id="${fingerprint}-page${(page + 1)}"></canvas></div></div>
</template>

加载PDF文档后,我们需要获取PDF中每一页的大小,以便我们可以将每个canvas大小与其页面大小进行匹配。 (此时,我们可以设置查看器进行滚动;如果现在不这样做,则每个页面的高度都不正确。)因此,在加载每个页面之后,我们将一个任务排队使用Aurelia的TaskQueue抽象来调整canvas元素的大小。 (这是出于DOM性能的原因。您可以在此处阅读有关微任务的更多信息 )。

这是我们的ViewModel:

// src/resources/elements/pdf-document.jsimport {customElement, bindable, bindingMode, inject, Loader} from 'aurelia-framework';
import {TaskQueue} from 'aurelia-task-queue';
import {PDFJS} from 'pdfjs-dist';@customElement('pdf-document')... // all of our @bindables@inject(Loader, TaskQueue)
export class PdfDocument {constructor (loader, taskQueue) {PDFJS.workerSrc = loader.normalizeSync('pdfjs-dist/build/pdf.worker.js');this.worker = new PDFJS.PDFWorker();// Hold a reference to the task queue for later use.this.taskQueue = taskQueue;// Add a promise property.this.resolveDocumentPending;// Add a fingerprint property to uniquely identify our DOM nodes.// This allows us to create multiple viewers without issues.this.fingerprint = generateUniqueDomId();this.pages = [];this.currentPage = null;}urlChanged (newValue, oldValue) {if (newValue === oldValue) return;// Load our document and store a reference to PDF.js' loading promise.var promise = this.documentPending || Promise.resolve();this.documentPending = new Promise((resolve, reject) => {this.resolveDocumentPending = resolve.bind(this);});return promise.then((pdf) => {if (pdf) {pdf.destroy();}return PDFJS.getDocument({ url: newValue, worker: this.worker });}).then((pdf) => {this.lastpage = pdf.numPages;pdf.cleanupAfterRender = true;// Queue loading of all of our PDF pages so that we can scroll through them later.for (var i = 0; i < pdf.numPages; i++) {this.pages[i] = pdf.getPage(Number(i + 1)).then((page) => {var viewport = page.getViewport(this.scale);var element = document.getElementById(`${this.fingerprint}-page${page.pageNumber}`);// Update page canvas elements to match viewport dimensions. // Use Aurelia's TaskQueue to batch the DOM changes.this.taskQueue.queueMicroTask(() => {element.height = viewport.height;element.width = viewport.width;});return {element: element,page: page,rendered: false,clean: false};});}// For the initial render, check to see which pages are currently visible, and render them./* Not implemented yet. */this.resolveDocumentPending(pdf);});}detached () {// Destroy our PDF worker asynchronously to avoid any race conditions.return this.documentPending.then((pdf) => {if (pdf) {pdf.destroy();}this.worker.destroy();}).catch(() => {this.worker.destroy();});}
}// Generate unique ID values to avoid any DOM conflicts and allow multiple PDF element instances.
var generateUniqueDomId = function () {var S4 = function() {return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);};return `_${S4()}${S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`;
}

保存您的工作,Gulp应该重新渲染页面。 您会注意到该容器显示了相应PDF的正确页数。 唯一的问题是它们为空。 让我们解决这个问题!

渲染页面

现在,我们已经加载了页面,我们需要能够将它们呈现为DOM元素。 为此,我们将依靠PDF.js的呈现功能。 PDF.js查看器库具有专用于呈现页面的异步API。 他们网站上有一个很棒的示例,展示了如何创建renderContext对象并将其传递给PDF.js render方法。 我们将从示例中删除此代码,并将其包装在render函数中:

src / resources / elements / pdf-document.js

...
export class PdfDocument { ... }var generateUniqueDomId = function () { ... }var render = function (renderPromise, scale) {return Promise.resolve(renderPromise).then((renderObject) => {if (renderObject.rendered) return Promise.resolve(renderObject);renderObject.rendered = true;var viewport = renderObject.page.getViewport(scale);var context = renderObject.element.getContext('2d');return renderObject.page.render({canvasContext: context,viewport: viewport}).promise.then(() => {return renderObject;});});
};

PDF.JS中的渲染有些昂贵。 因此,我们要限制负载。 我们只想渲染当前可见的内容,因此我们将渲染限制在可见边界内的页面上,而不是一次渲染所有内容。 我们将做一些简单的数学运算来检查视口中的内容:

// src/resources/elements/pdf-document.jsexport class PdfDocument { ... }var generateUniqueDomId = function () { ... }var render = function (...) { ... }var checkIfElementVisible = function (container, element) {var containerBounds = {top: container.scrollTop,bottom: container.scrollTop + container.clientHeight};var elementBounds = {top: element.offsetTop,bottom: element.offsetTop + element.clientHeight};return (!((elementBounds.bottom < containerBounds.top && elementBounds.top < containerBounds.top)|| (elementBounds.top > containerBounds.bottom && elementBounds.bottom > containerBounds.bottom)));
}

首次加载文档时以及滚动时,我们将运行这些视口检查。 现在,在加载时,我们将像这样简单地渲染可见的内容。

// src/resources/elements/pdf-document.jsexport class PdfDocument {
...urlChanged (newValue, oldValue) {...// For the initial render, check to see which pages are currently visible, and render them.this.pages.forEach((page) => {page.then((renderObject) => {if (checkIfElementVisible(this.container, renderObject.element)){if (renderObject.rendered) return;render(page, this.scale);}});});this.resolveDocumentPending(pdf);});}

重新加载应用程序,您将看到每个PDF的第一页都呈现出来。

实现滚动

为了提供熟悉和无缝的体验,我们的组件应将页面显示为完全可滚动文档的各个部分。 我们可以通过CSS使容器具有固定的高度(具有滚动溢出功能)来实现此目的。

为了最大程度地处理较大的文档,我们将做一些事情。 首先,我们将利用Aurelia的TaskQueue批量更改DOM。 其次,我们将跟踪PDF.js已经呈现的页面,因此它不必重做已经完成的工作。 最后,只有在滚动停止后,才使用Aurelia的debounce 绑定行为呈现可见的页面。 滚动时将运行以下方法:

// src/resources/elements/pdf-document.jsexport class PdfDocument {
...renderHandler () {Promise.all(this.pages).then((values) => {values.forEach((renderObject) => {if (!renderObject) return;if (!checkIfElementVisible(this.container, renderObject.element)){if (renderObject.rendered && renderObject.clean) {renderObject.page.cleanup();renderObject.clean = true;}return;}this.taskQueue.queueMicroTask(() => {if (renderObject.rendered) return;render(renderObject, this.scale);});});});}
...
}

这是我们的观点; 我们使用定义的方法在scroll.trigger利用Aurelia的事件绑定,以及去抖动绑定行为。

// src/resources/elements/pdf-document.html<template><require from="./pdf-document.css"></require><div ref="container" id.bind="fingerprint" class="pdf-container" scroll.trigger="pageHandler()" scroll.trigger2="renderHandler() & debounce:100"><div repeat.for="page of lastpage" class="text-center"><canvas id="${fingerprint}-page${(page + 1)}"></canvas></div></div>
</template>

我们正在将page属性绑定到查看器中。 当它更改时,我们要更新滚动位置以显示当前页面。 我们也希望这能以其他方式起作用; 当我们滚动浏览文档时,我们希望当前页码更新为当前正在查看的页面。 因此,我们将以下两个方法添加到我们的ViewModel中:

export class PdfDocument {
...// If the page changes, scroll to the associated element.pageChanged (newValue, oldValue) {if (newValue === oldValue || isNaN(Number(newValue)) || Number(newValue) > this.lastpage || Number(newValue) < 0) {this.page = oldValue;return;}// Prevent scroll update collisions with the pageHandler method.if (Math.abs(newValue - oldValue) <= 1) return;this.pages[newValue - 1].then((renderObject) => {this.container.scrollTop = renderObject.element.offsetTop;render(this.pages[newValue - 1], this.scale);});}...// Change the current page number as we scroll.pageHandler () {this.pages.forEach((page) => {page.then((renderObject) => {if ((this.container.scrollTop + this.container.clientHeight) >= renderObject.element.offsetTop&& (this.container.scrollTop <= renderObject.element.offsetTop)){this.page = renderObject.page.pageNumber;}});});}
...
}

我们将在容器中的scroll.trigger事件中调用pageHandler方法。

注意:由于Aurelia模板的当前限制,因此无法在事件处理程序中使用单独的绑定行为声明多个方法。 我们通过将这些行添加到ViewModel的顶部来解决此问题。

import {SyntaxInterpreter} from 'aurelia-templating-binding';
SyntaxInterpreter.prototype.trigger2 = SyntaxInterpreter.prototype.trigger;

…并将新方法放在scroll.trigger2事件上。

Gulp应该重新加载应用程序,并且您会看到PDF的新页面在滚动到视图时将呈现。 好极了!

实施缩放

缩放时,我们想更新当前的缩放级别。 我们在scaleChanged属性处理程序中执行此操作。 本质上,我们调整所有画布元素的大小以反映具有给定比例的每个页面的新视口大小。 然后,我们重新渲染当前视口中的内容,重新开始循环。

// src/resources/elements/pdf-document.jsexport class PdfDocument {
...scaleChanged (newValue, oldValue) {if (newValue === oldValue || isNaN(Number(newValue))) return;Promise.all(this.pages).then((values) => {values.forEach((renderObject) => {if (!renderObject) return;var viewport = renderObject.page.getViewport(newValue);renderObject.rendered = false;this.taskQueue.queueMicroTask(() => {renderObject.element.height = viewport.height;renderObject.element.width = viewport.width;if (renderObject.page.pageNumber === this.page) {this.container.scrollTop = renderObject.element.offsetTop;}});});return values;}).then((values) => {this.pages.forEach((page) => {page.then((renderObject) => {this.taskQueue.queueMicroTask(() => {if (checkIfElementVisible(this.container, renderObject.element)) {render(page, this.scale);}});});});});}
...
}

最终结果

让我们回顾一下我们的目标:

  1. 我们希望用户能够以不错的性能加载文档,滚动和放大和缩小。
  2. 我们希望能够将查看器属性(例如当前页面和当前缩放级别)双向绑定到我们应用程序中的属性。
  3. 我们希望该查看器成为可重用的组件; 我们希望能够将多个查看器同时拖放到我们的应用程序中,而不会发生冲突且花费很少的精力。

最终代码可在我们的GitHub存储库中找到,以及此处的完成代码演示 。 尽管还有改进的余地,但我们已经达到了目标!!

项目后分析和改进

总是有改进的余地,进行项目后分析并确定在将来的迭代中要解决的领域始终是一个好习惯。 根据PDF查看器的实现,以下是我要升级的一些内容:

各个页面组件

当前,此概念验证仅允许滚动视口。 理想情况下,即使在查看器外部,我们也可以在任何地方渲染任何页面-例如,将PDF缩略图生成为单独的元素。 创建<pdf-page>自定义元素或类似的东西可以提供此功能,而查看者可以通过合成简单地使用这些元素。

API优化

PDF.js具有广泛的API。 尽管有使用PDF.js的良好示例,但其显示API可能会使用更多文档。 使用查看器API可能会有更清洁,更优化的方法来实现我们的目标。

虚拟滚动和性能优化

当前,文档查看器内部的canvas元素数量等于文档中的页面数量。 所有画布都存在于DOM中,这对于大型文档而言可能非常昂贵。

存在一个Aurelia插件– ui虚拟化插件 ( demo )–通过动态添加和删除DOM中的元素以与活动视口相对应,极大地提高了超大型数据集的性能。 理想情况下,PDF查看器可以将其合并以提高性能(避免在DOM中包含成千上万的画布,这实际上会损害性能)。 这种优化与单独的页面组件结合在一起,对于大型文档确实可以产生巨大的变化。

创建一个插件

Aurelia提供了一个插件系统。 将此概念验证转换为Aurelia插件将使其成为任何Aurelia应用程序的直接资源。 Aurelia Github存储库提供了一个插件框架项目 ,这将是开始开发的一个好地方。 这样,其他人就可以使用此功能而不必重建它!

向前走

在Web应用程序中处理PDF文件一直很麻烦。 但是利用当今可用的资源,通过组合库及其功能,我们可以做的比以往更多。 今天,我们已经看到了一个基本的PDF查看器示例-可以通过自定义功能进行扩展,因为我们可以完全控制它。 可能性是无止境! 您准备好要建造东西了吗? 在下面的评论中让我知道。

From: https://www.sitepoint.com/aurelia-custom-pdf-viewer-component/

Aurelia历险记:创建自定义PDF查看器相关推荐

  1. 在线PDF查看器和PDF编辑器:GrapeCity Documents PDF (GcPdf)

    跨平台 JavaScript PDF 查看器 使用我们的 JavaScript PDF 查看器在网络上阅读和编辑 PDF.跨浏览器和框架打开和打印.GrapeCity Documents PDF (G ...

  2. HTML PDF 查看器--RAD PDF 3.33 FOR ASP.NET

    RAD PDF 的主要特点 基于 HTML 的 PDF 阅读器 客户端 PDF 编辑器 功能丰富的 PDF 表单填写器 交互式 PDF 表单设计器 保护 PDF 内容 签署和认证 PDF 文件 广泛的 ...

  3. WordPress的最佳PDF查看器比较

    PDF文件是共享和显示文档的有效且经过时间考验的方式,但是当您的网站没有PDF查看器时,存在一些限制. 首先,您可能会失去访问者的风险:浏览器可以加载PDF文档时,文件会加载到新的标签页或窗口中,这意 ...

  4. vb怎么添加html帮助文件,创建自定义 HTML 帮助器 (VB) | Microsoft Docs

    创建自定义 HTML 帮助程序 (VB) 10/07/2008 本文内容 本教程的目标是演示如何创建自定义 HTML 帮助器,您可以在 MVC 视图中使用. 通过使用 HTML 帮助器,可以减少创建标 ...

  5. 文献翻译利器——自定义PDF阅读器+Saladict+Quicker

    文献翻译利器--自定义PDF阅读器+Saladict+Quicker 0 前言 1 翻译方式的对比 1.1 在线翻译 1.2 金山词霸 1.3 知云文献翻译 1.4 Chrome+Saladict 1 ...

  6. PDF控件PDFToolkit VCL V5.0.0.612发布 | 修复了PDF查看器和打印机

    2019独角兽企业重金招聘Python工程师标准>>> PDFtoolkit VCL 5.0.0.612 更新 修复以下问题: PDF查看器 查看器冻结加载特定的PDF文件时查看器冻 ...

  7. Texmaker中PDF查看器的设置经验

    这个问题很简单,不过有时候记不清,所以特意总结一下. Texmaker是一个不错的LaTeX编辑器,在我的推荐下现在实验室的小伙伴们都在用.但是我注意到很多人用的时候有个问题,Texmaker的PDF ...

  8. texstudio调用外部pdf查看器的配置方法

    选项-设置-命令-外部PDF查看器-打印符号-找到wps或者其他pdf查看软件的安装位置(可以在桌面的快捷方式右边查看),然后导入即可

  9. 如何使用Foxit Mobile SDK 6.0 快速创建一个PDF阅读器-Android篇

    Foxit MobilePDF SDK是一款用于移动平台的快速开发包,专注于帮助开发人员将强大的Foxit PDF技术轻松地集成到他们的应用程序中.不知不觉,产品的版本已经到了6.0,近期刚刚发布,6 ...

最新文章

  1. dataframe多列合并成一列
  2. Manage Jenkins管理界面提示“依赖错误: 部分插件由于缺少依赖无法加载...“问题解决办法
  3. JS(JavaScript)的初了解6(更新中···)
  4. javascript 函数和对象 再顺一顺
  5. 告诉服务器端当前请求的意图
  6. 服务器排障 之 nginx 499 错误的解决
  7. Arduino_esp32_WiFi代码
  8. 2020牛客国庆集训派对day2 MATRIX MULTIPLICATION CALCULATOR
  9. vue : 无法加载文件 XXXXXXX\vue.ps1,因为在此系统上禁止运行脚本。
  10. 算法设计和数据结构学习_2(常见排序算法思想)
  11. vue-resource 和 axios的区别
  12. VMware vSphere 6 序列号大全
  13. 千兆以太网(二)——MDIO接口协议
  14. 台式计算机把硬盘换了怎么进系统,联想台式机怎么进bios设置硬盘启动
  15. EF MYSQL批量更新_Entity Framework Core 5中实现批量更新、删除
  16. FSCE: Few-Shot Object Detection via Contrastive Proposal Encoding个人理解
  17. 在浏览器输入URL,按下回车之后的流程
  18. UNREAL 多人在綫更换pawn(possess pawn)
  19. [51单片机]按键部分(软件消抖)
  20. 计算机主板上的ide,计算机主板上的IDE接口通常是连接什么设备的数据接口?

热门文章

  1. 现代教育技术没有计算机基础知识教程,浅析现代教育技术在专科计算机基础课教学中的应用...
  2. OpenFOAM常用类
  3. 如何清理和优化你的Mac:14个小技巧推荐给你!
  4. Android 画圆
  5. linux服务器怎么查看cpu配置信息,linux服务器cpu信息查看详解
  6. 数据结构之线性表----一文看懂顺序表、单链表、双链表、循环链表
  7. 使用bash解析xml
  8. TPS,MIS,DSS,ESS,临时表
  9. 互联网晚报 | 5月13日 星期五 | 罗永浩回应被叫行业冥灯;新一轮汽车下乡政策最快将于本月出台;字节跳动鲸鲮操作系统获批...
  10. 调用链监控 - Tracing - APM