让我们通过一个例子来构建一个自定义的gradio组件,用于显示PDF文件。 这个组件将非常有用,用于展示文档问答模型,这些模型通常处理PDF输入。 这是我们完成组件后的预览:

请确保您已安装gradio 5.0或更高版本以及node 20+。 截至发布时,最新版本为4.1.1。 此外,在开始之前,请阅读自定义组件的五分钟导览和关键概念指南。
导航到您选择的目录并运行以下命令:
gradio cc create PDF提示:您应该更改组件的名称。
一些截图假设组件名为PDF,但概念是相同的!
这将在您当前的工作目录中创建一个名为 pdf 的子目录。
pdf 中有三个主要的子目录:frontend、backend 和 demo。
如果您在代码编辑器中打开 pdf,它将如下所示:

提示:
对于这个演示,我们没有基于当前的gradio组件进行模板化。但你可以使用`gradio cc show`查看可用的模板列表,然后将模板名称传递给`--template`选项,例如`gradio cc create
我们将使用pdfjs JavaScript库在前端显示PDF。让我们首先将其添加到我们前端项目的依赖项中,同时添加一些我们需要的其他项目。
在frontend目录中,运行npm install @gradio/client @gradio/upload @gradio/icons @gradio/button和npm install --save-dev pdfjs-dist@3.11.174。
此外,让我们通过运行npm uninstall @zerodevx/svelte-json-view来卸载@zerodevx/svelte-json-view依赖项。
完整的 package.json 应该如下所示:
{
"name": "gradio_pdf",
"version": "0.2.0",
"description": "Gradio component for displaying PDFs",
"type": "module",
"author": "",
"license": "ISC",
"private": false,
"main_changeset": true,
"exports": {
".": "./Index.svelte",
"./example": "./Example.svelte",
"./package.json": "./package.json"
},
"devDependencies": {
"pdfjs-dist": "3.11.174"
},
"dependencies": {
"@gradio/atoms": "0.2.0",
"@gradio/statustracker": "0.3.0",
"@gradio/utils": "0.2.0",
"@gradio/client": "0.7.1",
"@gradio/upload": "0.3.2",
"@gradio/icons": "0.2.0",
"@gradio/button": "0.2.3",
"pdfjs-dist": "3.11.174"
}
}
提示:
运行 `npm install` 将安装可用的最新版本的包。您可以使用 `npm install package@
导航到 Index.svelte 并删除对 JSONView 的提及
import { JsonView } from "@zerodevx/svelte-json-view";<JsonView json={value} />运行dev命令以启动开发服务器。
这将在环境中打开demo/app.py中的演示,在该环境中,对frontend和backend目录的更改将立即反映在启动的应用程序中。
启动开发服务器后,你应该会在控制台上看到一个链接,上面写着 Frontend Server (Go here): ... 。

你应该看到以下内容:

虽然现在还不令人印象深刻,但我们准备好开始编码了!
我们将首先编写前端的骨架,然后添加pdf渲染逻辑。
在
import { tick } from "svelte";
import type { Gradio } from "@gradio/utils";
import { Block, BlockLabel } from "@gradio/atoms";
import { File } from "@gradio/icons";
import { StatusTracker } from "@gradio/statustracker";
import type { LoadingStatus } from "@gradio/statustracker";
import type { FileData } from "@gradio/client";
import { Upload, ModifyUpload } from "@gradio/upload";
export let elem_id = "";
export let elem_classes: string[] = [];
export let visible = true;
export let value: FileData | null = null;
export let container = true;
export let scale: number | null = null;
export let root: string;
export let height: number | null = 500;
export let label: string;
export let proxy_url: string;
export let min_width: number | undefined = undefined;
export let loading_status: LoadingStatus;
export let gradio: Gradio<{
change: never;
upload: never;
}>;
let _value = value;
let old_value = _value;提示: 这里传入的`gradio`对象包含一些关于应用程序的元数据以及一些实用方法。其中一个实用方法是调度方法。我们希望在PDF更改或更新时调度更改和上传事件。这一行提供了类型提示,表明这些是我们将调度的唯一事件。
我们希望我们的前端组件能让用户上传一个PDF文档,如果还没有加载的话。
如果已经加载了,我们希望在“清除”按钮下方显示它,让用户可以上传一个新文档。
我们将使用@gradio/upload包中的Upload和ModifyUpload组件来实现这一点。
在标签下方,删除所有当前代码并添加以下内容:
<Block {visible} {elem_id} {elem_classes} {container} {scale} {min_width}>
{#if loading_status}
<StatusTracker
autoscroll={gradio.autoscroll}
i18n={gradio.i18n}
{...loading_status}
/>
{/if}
<BlockLabel
show_label={label !== null}
Icon={File}
float={value === null}
label={label || "File"}
/>
{#if _value}
<ModifyUpload i18n={gradio.i18n} absolute />
{:else}
<Upload
filetype={"application/pdf"}
file_count="single"
{root}
>
Upload your PDF
</Upload>
{/if}
</Block>在保存当前更改后导航到您的应用程序时,您应该看到以下内容:

Upload your PDF 文本看起来有点小且简陋。
让我们来定制它!
创建一个名为PdfUploadText.svelte的新文件,并复制以下代码。
它正在创建一个新的div来显示我们的“上传文本”,并带有一些自定义样式。
提示: 注意,我们在这里利用了Gradio核心的现有CSS变量:`var(--size-60)`和`var(--body-text-color-subdued)`。这使得我们的组件在亮模式和暗模式下都能很好地工作,同时也能与Gradio内置的主题兼容。
<script lang="ts">
import { Upload as UploadIcon } from "@gradio/icons";
export let hovered = false;
</script>
<div class="wrap">
<span class="icon-wrap" class:hovered><UploadIcon /> </span>
Drop PDF
<span class="or">- or -</span>
Click to Upload
</div>
<style>
.wrap {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: var(--size-60);
color: var(--block-label-text-color);
line-height: var(--line-md);
height: 100%;
padding-top: var(--size-3);
}
.or {
color: var(--body-text-color-subdued);
display: flex;
}
.icon-wrap {
width: 30px;
margin-bottom: var(--spacing-lg);
}
@media (--screen-md) {
.wrap {
font-size: var(--text-lg);
}
}
.hovered {
color: var(--color-accent);
}
</style>现在在你的
import PdfUploadText from "./PdfUploadText.svelte";
...
<Upload
filetype={"application/pdf"}
file_count="single"
{root}
>
<PdfUploadText />
</Upload>保存代码后,前端现在应该看起来像这样:

这是最先进的javascript部分。 我花了一些时间才弄明白! 如果你遇到困难,不要担心,重要的是不要气馁 💪 如果需要帮助,请在gradio的discord中寻求帮助。
既然已经解决了这个问题,让我们首先导入pdfjs并从mozilla cdn加载pdf worker的代码。
import pdfjsLib from "pdfjs-dist";
...
pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdn.bootcss.com/pdf.js/3.11.174/pdf.worker.js";同时创建以下变量:
let pdfDoc;
let numPages = 1;
let currentPage = 1;
let canvasRef;现在,我们将使用pdfjs将PDF的给定页面渲染到html文档上。
将以下代码添加到Index.svelte中:
async function get_doc(value: FileData) {
const loadingTask = pdfjsLib.getDocument(value.url);
pdfDoc = await loadingTask.promise;
numPages = pdfDoc.numPages;
render_page();
}
function render_page() {
// Render a specific page of the PDF onto the canvas
pdfDoc.getPage(currentPage).then(page => {
const ctx = canvasRef.getContext('2d')
ctx.clearRect(0, 0, canvasRef.width, canvasRef.height);
let viewport = page.getViewport({ scale: 1 });
let scale = height / viewport.height;
viewport = page.getViewport({ scale: scale });
const renderContext = {
canvasContext: ctx,
viewport,
};
canvasRef.width = viewport.width;
canvasRef.height = viewport.height;
page.render(renderContext);
});
}
// If the value changes, render the PDF of the currentPage
$: if(JSON.stringify(old_value) != JSON.stringify(_value)) {
if (_value){
get_doc(_value);
}
old_value = _value;
gradio.dispatch("change");
}提示: Svelte中的`$:`语法用于声明响应式语句。每当语句的任何输入发生变化时,Svelte会自动重新运行该语句。
现在将canvas放置在ModifyUpload组件下方:
<div class="pdf-canvas" style="height: {height}px">
<canvas bind:this={canvasRef}></canvas>
</div>并将以下样式添加到
<style>
.pdf-canvas {
display: flex;
justify-content: center;
align-items: center;
}
</style>现在是乐趣部分 - 实际上传文件时渲染PDF!
将以下函数添加到
async function handle_clear() {
_value = null;
await tick();
gradio.dispatch("change");
}
async function handle_upload({detail}: CustomEvent<FileData>): Promise<void> {
value = detail;
await tick();
gradio.dispatch("change");
gradio.dispatch("upload");
}
提示:
`gradio.dispatch` 方法实际上是触发后端 `change` 或 `upload` 事件的原因。对于组件后端中定义的每个事件,我们将在第9步中解释如何做到这一点,必须至少有一个 `gradio.dispatch("
现在,我们将在Upload组件上传文件时以及ModifyUpload组件清除当前文件时运行这些函数。组件会分发一个load事件,其有效载荷类型为FileData,对应于上传的文件。on:load语法告诉Svelte在响应此事件时自动运行此函数。
<ModifyUpload i18n={gradio.i18n} on:clear={handle_clear} absolute />
...
<Upload
on:load={handle_upload}
filetype={"application/pdf"}
file_count="single"
{root}
>
<PdfUploadText/>
</Upload>恭喜!您已经拥有一个可用的PDF上传器!

如果用户上传了一个多页的PDF文档,他们只能看到第一页。
让我们添加一些按钮来帮助他们浏览页面。
我们将使用@gradio/button中的BaseButton,使它们看起来像常规的Gradio按钮。
导入 BaseButton 并添加以下函数,这些函数将渲染 PDF 的下一页和上一页。
import { BaseButton } from "@gradio/button";
...
function next_page() {
if (currentPage >= numPages) {
return;
}
currentPage++;
render_page();
}
function prev_page() {
if (currentPage == 1) {
return;
}
currentPage--;
render_page();
}现在我们将在画布下方的一个单独的中添加它们
...
<ModifyUpload i18n={gradio.i18n} on:clear={handle_clear} absolute />
<div class="pdf-canvas" style="height: {height}px">
<canvas bind:this={canvasRef}></canvas>
</div>
<div class="button-row">
<BaseButton on:click={prev_page}>
⬅️
</BaseButton>
<span class="page-count"> {currentPage} / {numPages} </span>
<BaseButton on:click={next_page}>
➡️
</BaseButton>
</div>
...
<style>
.button-row {
display: flex;
flex-direction: row;
width: 100%;
justify-content: center;
align-items: center;
}
.page-count {
margin: 0 10px;
font-family: var(--font-mono);
}恭喜!前端几乎完成了 🎉

我们希望组件的用户能够在gr.Interface或gr.Examples中使用example时预览PDF。
为此,我们将在Index.svelte中添加一些pdf渲染逻辑到Example.svelte中。
<script lang="ts">
export let value: string;
export let type: "gallery" | "table";
export let selected = false;
import pdfjsLib from "pdfjs-dist";
pdfjsLib.GlobalWorkerOptions.workerSrc = "https://cdn.bootcss.com/pdf.js/3.11.174/pdf.worker.js";
let pdfDoc;
let canvasRef;
async function get_doc(url: string) {
const loadingTask = pdfjsLib.getDocument(url);
pdfDoc = await loadingTask.promise;
renderPage();
}
function renderPage() {
// Render a specific page of the PDF onto the canvas
pdfDoc.getPage(1).then(page => {
const ctx = canvasRef.getContext('2d')
ctx.clearRect(0, 0, canvasRef.width, canvasRef.height);
const viewport = page.getViewport({ scale: 0.2 });
const renderContext = {
canvasContext: ctx,
viewport
};
canvasRef.width = viewport.width;
canvasRef.height = viewport.height;
page.render(renderContext);
});
}
$: get_doc(value);
</script>
<div
class:table={type === "table"}
class:gallery={type === "gallery"}
class:selected
style="justify-content: center; align-items: center; display: flex; flex-direction: column;"
>
<canvas bind:this={canvasRef}></canvas>
</div>
<style>
.gallery {
padding: var(--size-1) var(--size-2);
}
</style>提示: 读者练习 - 减少 `Index.svelte` 和 `Example.svelte` 之间的代码重复 😊
在我们对后端代码进行下一步更改之前,您将无法渲染示例!
后端所需的更改较小。 我们几乎完成了!
我们将要做的是:
change 和 upload 事件。height属性,让用户可以控制PDF的高度。data_model设置为FileData。这是为了让Gradio能够自动缓存并安全地提供由我们组件处理的任何文件。preprocess方法以返回一个字符串,该字符串对应于我们上传的PDF的路径。postprocess以将事件处理程序中创建的PDF路径转换为FileData。当一切都说完了,你的组件的后端代码应该看起来像这样:
from __future__ import annotations
from typing import Any, Callable, TYPE_CHECKING
from gradio.components.base import Component
from gradio.data_classes import FileData
from gradio import processing_utils
if TYPE_CHECKING:
from gradio.components import Timer
class PDF(Component):
EVENTS = ["change", "upload"]
data_model = FileData
def __init__(self, value: Any = None, *,
height: int | None = None,
label: str | None = None, info: str | None = None,
show_label: bool | None = None,
container: bool = True,
scale: int | None = None,
min_width: int | None = None,
interactive: bool | None = None,
visible: bool = True,
elem_id: str | None = None,
elem_classes: list[str] | str | None = None,
render: bool = True,
load_fn: Callable[..., Any] | None = None,
every: Timer | float | None = None):
super().__init__(value, label=label, info=info,
show_label=show_label, container=container,
scale=scale, min_width=min_width,
interactive=interactive, visible=visible,
elem_id=elem_id, elem_classes=elem_classes,
render=render, load_fn=load_fn, every=every)
self.height = height
def preprocess(self, payload: FileData) -> str:
return payload.path
def postprocess(self, value: str | None) -> FileData:
if not value:
return None
return FileData(path=value)
def example_payload(self):
return "https://gradio-builds.s3.amazonaws.com/assets/pdf-guide/fw9.pdf"
def example_value(self):
return "https://gradio-builds.s3.amazonaws.com/assets/pdf-guide/fw9.pdf"为了测试我们的后端代码,让我们添加一个更复杂的演示,使用huggingface transformers执行文档问答。
在我们的demo目录中,创建一个包含以下包的requirements.txt文件
torch
transformers
pdf2image
pytesseract提示: 记得自己安装这些并重启开发服务器!你可能需要为`pdf2image`安装额外的非Python依赖。参见[这里](https://pypi.org/project/pdf2image/)。如果你遇到问题,可以自由编写你自己的演示。
import gradio as gr
from gradio_pdf import PDF
from pdf2image import convert_from_path
from transformers import pipeline
from pathlib import Path
dir_ = Path(__file__).parent
p = pipeline(
"document-question-answering",
model="impira/layoutlm-document-qa",
)
def qa(question: str, doc: str) -> str:
img = convert_from_path(doc)[0]
output = p(img, question)
return sorted(output, key=lambda x: x["score"], reverse=True)[0]['answer']
demo = gr.Interface(
qa,
[gr.Textbox(label="Question"), PDF(label="Document")],
gr.Textbox(),
)
demo.launch()请查看我们下面的实际演示!
最后,让我们使用gradio cc build构建我们的组件,并使用gradio cc publish命令发布它!
这将引导您完成将组件上传到PyPi和HuggingFace Spaces的过程。
提示: 您可能需要在 HuggingFace Space 的 `Dockerfile` 中添加以下行。
RUN mkdir -p /tmp/cache/
RUN chmod a+rwx -R /tmp/cache/
RUN apt-get update && apt-get install -y poppler-utils tesseract-ocr
ENV TRANSFORMERS_CACHE=/tmp/cache/为了在任何gradio 4.0应用中使用我们的新组件,只需使用pip安装它,例如pip install gradio-pdf。然后你可以像使用内置的gr.File()组件一样使用它(除了它只接受和显示PDF文件)。
这是一个使用Blocks API的简单演示:
import gradio as gr
from gradio_pdf import PDF
with gr.Blocks() as demo:
pdf = PDF(label="Upload a PDF", interactive=True)
name = gr.Textbox()
pdf.upload(lambda f: f, pdf, name)
demo.launch()我希望你喜欢这个教程! 我们组件的完整源代码在这里。 如果你遇到困难,请不要犹豫,在HuggingFace Discord上联系gradio社区。