1. 自定义组件
  2. PDF 组件示例

案例研究:一个用于显示PDF的组件

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

demo

步骤0:先决条件

请确保您已安装gradio 5.0或更高版本以及node 20+。 截至发布时,最新版本为4.1.1。 此外,在开始之前,请阅读自定义组件的五分钟导览关键概念指南。

第一步:创建自定义组件

导航到您选择的目录并运行以下命令:

gradio cc create PDF

提示:您应该更改组件的名称。 一些截图假设组件名为PDF,但概念是相同的!

这将在您当前的工作目录中创建一个名为 pdf 的子目录。 pdf 中有三个主要的子目录:frontendbackenddemo。 如果您在代码编辑器中打开 pdf,它将如下所示:

directory structure

提示: 对于这个演示,我们没有基于当前的gradio组件进行模板化。但你可以使用`gradio cc show`查看可用的模板列表,然后将模板名称传递给`--template`选项,例如`gradio cc create --template `

步骤2:前端 - 修改javascript依赖项

我们将使用pdfjs JavaScript库在前端显示PDF。让我们首先将其添加到我们前端项目的依赖项中,同时添加一些我们需要的其他项目。

frontend目录中,运行npm install @gradio/client @gradio/upload @gradio/icons @gradio/buttonnpm 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@` 安装特定版本。您可以在此处找到所有 gradio javascript 包的文档 [here](https://www.gradio.app/main/docs/js)。建议您使用与我相同的版本,因为 API 可能会发生变化。

导航到 Index.svelte 并删除对 JSONView 的提及

import { JsonView } from "@zerodevx/svelte-json-view";
<JsonView json={value} />

步骤3:前端 - 启动开发服务器

运行dev命令以启动开发服务器。 这将在环境中打开demo/app.py中的演示,在该环境中,对frontendbackend目录的更改将立即反映在启动的应用程序中。

启动开发服务器后,你应该会在控制台上看到一个链接,上面写着 Frontend Server (Go here): ...

你应该看到以下内容:

虽然现在还不令人印象深刻,但我们准备好开始编码了!

步骤4:前端 - 基本框架

我们将首先编写前端的骨架,然后添加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包中的UploadModifyUpload组件来实现这一点。 在标签下方,删除所有当前代码并添加以下内容:

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

在保存当前更改后导航到您的应用程序时,您应该看到以下内容:

步骤5:前端 - 更美观的上传文本

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>

保存代码后,前端现在应该看起来像这样:

步骤6: PDF渲染逻辑

这是最先进的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>

步骤7:处理文件上传和清除

现在是乐趣部分 - 实际上传文件时渲染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("")` 调用。这些被称为 `gradio` 事件,它们可以从整个 Gradio 应用程序中监听。你可以使用 `dispatch` 函数分发一个内置的 `svelte` 事件。这些事件只能从组件的直接父级监听。从[官方文档](https://learn.svelte.dev/tutorial/component-events)中了解更多关于 svelte 事件的信息。

现在,我们将在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上传器!

upload-gif

步骤8:添加按钮以导航页面

如果用户上传了一个多页的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);
    }

恭喜!前端几乎完成了 🎉

multipage-pdf-gif

步骤 8.5: 示例视图

我们希望组件的用户能够在gr.Interfacegr.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` 之间的代码重复 😊

在我们对后端代码进行下一步更改之前,您将无法渲染示例!

步骤9:后端

后端所需的更改较小。 我们几乎完成了!

我们将要做的是:

  • 向我们的组件添加 changeupload 事件。
  • 添加一个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"

步骤10:添加演示并发布!

为了测试我们的后端代码,让我们添加一个更复杂的演示,使用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命令发布它! 这将引导您完成将组件上传到PyPiHuggingFace 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社区。