如何在Azure上设置IronPDF for Java

This article was translated from English: Does it need improvement?
Translated
View the article in English

本指南涵盖了在Azure Functions容器内部署IronPDF for Java所需的所有内容,并从无服务器HTTP端点按需生成PDF。 由于IronPDF带有本地Chromium渲染引擎,因此必须将其打包为Docker镜像——标准的Azure Functions Zip部署无法在运行时执行IronPDF依赖的二进制文件。按照本指南,可以使一个工作中的Azure Function接受URL作为查询参数并返回一个完全渲染的PDF作为可下载的文件。

这种方法使用Microsoft推荐的Linux Azure Functions自定义容器工作流。 Maven项目提供函数代码和依赖管理。 Docker构建容器镜像,该镜像被推到注册表并由Azure Function App引用。部署后,冷启动时间是主要的性能考虑因素——随后的调用速度快且一致。

在开始之前,请确保Azure CLI、Docker Desktop、Maven 3.8+和JDK 11或JDK 17已在本地安装。 还需要一个活跃的Azure订阅,并拥有创建Function App和存储账户的权限。

快速启动:在Azure Functions上部署IronPDF for Java

下面的代码展示了完整的RenderPdf Azure Function。 它接受一个url查询参数并返回一个PDF字节流。 在完成Maven依赖设置后,将其添加到Function.java中。

//:path=/static-assets/pdf/content-code-examples/tutorials/azure/RenderPdf.java
import com.microsoft.azure.functions.*;
import com.ironsoftware.ironpdf.PdfDocument;
import java.util.Optional;

public class Function {

    /**
     * HTTP-triggered Azure Function: accepts a URL, renders it as a PDF,
     * and returns the PDF bytes as a downloadable attachment.
     */
    @FunctionName("RenderPdf")
    public HttpResponseMessage renderPdf(
            @HttpTrigger(
                    name = "req",
                    methods = {HttpMethod.GET, HttpMethod.POST},
                    authLevel = AuthorizationLevel.ANONYMOUS)
            HttpRequestMessage<Optional<String>> request,
            final ExecutionContext context) {

        context.getLogger().info("RenderPdf function triggered.");

        // Read the target URL from the query string
        final String url = request.getQueryParameters().get("url");

        if (url == null) {
            return request.createResponseBuilder(HttpStatus.BAD_REQUEST)
                    .body("Provide a 'url' query parameter.")
                    .build();
        }

        try {
            context.getLogger().info("Rendering URL as PDF: " + url);

            // IronPDF renders the full page including JavaScript
            PdfDocument pdf = PdfDocument.renderUrlAsPdf(url);
            byte[] pdfBytes = pdf.getBinaryData();

            return request.createResponseBuilder(HttpStatus.OK)
                    .body(pdfBytes)
                    .header("Content-Disposition", "attachment; filename=output.pdf")
                    .header("Content-Type", "application/pdf")
                    .build();

        } catch (Exception ex) {
            context.getLogger().severe("PDF rendering failed: " + ex.getMessage());
            return request.createResponseBuilder(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("PDF rendering failed. Check function logs for details.")
                    .build();
        }
    }
}
//:path=/static-assets/pdf/content-code-examples/tutorials/azure/RenderPdf.java
import com.microsoft.azure.functions.*;
import com.ironsoftware.ironpdf.PdfDocument;
import java.util.Optional;

public class Function {

    /**
     * HTTP-triggered Azure Function: accepts a URL, renders it as a PDF,
     * and returns the PDF bytes as a downloadable attachment.
     */
    @FunctionName("RenderPdf")
    public HttpResponseMessage renderPdf(
            @HttpTrigger(
                    name = "req",
                    methods = {HttpMethod.GET, HttpMethod.POST},
                    authLevel = AuthorizationLevel.ANONYMOUS)
            HttpRequestMessage<Optional<String>> request,
            final ExecutionContext context) {

        context.getLogger().info("RenderPdf function triggered.");

        // Read the target URL from the query string
        final String url = request.getQueryParameters().get("url");

        if (url == null) {
            return request.createResponseBuilder(HttpStatus.BAD_REQUEST)
                    .body("Provide a 'url' query parameter.")
                    .build();
        }

        try {
            context.getLogger().info("Rendering URL as PDF: " + url);

            // IronPDF renders the full page including JavaScript
            PdfDocument pdf = PdfDocument.renderUrlAsPdf(url);
            byte[] pdfBytes = pdf.getBinaryData();

            return request.createResponseBuilder(HttpStatus.OK)
                    .body(pdfBytes)
                    .header("Content-Disposition", "attachment; filename=output.pdf")
                    .header("Content-Type", "application/pdf")
                    .build();

        } catch (Exception ex) {
            context.getLogger().severe("PDF rendering failed: " + ex.getMessage());
            return request.createResponseBuilder(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("PDF rendering failed. Check function logs for details.")
                    .build();
        }
    }
}
JAVA

今天在您的项目中使用 IronPDF,免费试用。

第一步:
green arrow pointer

目录

需求条件是什么?

开始前,请确认所有必需的工具已安装,并且一个Azure订阅是活跃的。跳过这些检查常常会导致部署过程中的构建失败。

所需的本地工具

所需的Azure资源

  • 一个活跃的Azure订阅
  • 创建资源组、存储账户和Function App计划的权限
  • 一个Docker Hub账户(或者Azure容器注册表)用来托管构建的镜像

重要IronPDF for Java在任何Docker容器内部运行时需要ironpdf-engine-linux-x64工件。 在Azure Functions上标准的Zip部署无法执行IronPDF的本地二进制文件——Docker是唯一支持的部署方法。

在继续到下一部分之前运行az login以对Azure CLI进行身份验证。

如何设置Azure Function项目?

Microsoft指南使用自定义镜像在Linux上创建一个函数包含了完整的脚手架过程。 按照这些步骤进行,其中一个重要选择:在询问编程语言时选择Java

完成教程直到脚手架项目构建完毕,并使用Azure Functions Core Tools本地运行占位符函数。 通过以下方法进行验证:

//:path=/static-assets/pdf/content-code-examples/tutorials/azure/local-run.sh
mvn clean package
func start
//:path=/static-assets/pdf/content-code-examples/tutorials/azure/local-run.sh
mvn clean package
func start
SHELL

一旦占位符响应本地HTTP请求,项目结构即正确且准备好进行IronPDF集成。 关键文件是Dockerfile(容器定义)。

请注意Azure Functions Maven原型生成一个pom.xml一起。 local.settings.json文件存储用于本地开发的环境变量——它默认情况下从源代码控制中排除并且绝不应该提交。

如何将IronPDF依赖项添加到Maven项目中?

IronPDF for Java通过Maven Central分发。 需要两个工件:提供Java API的核心ironpdf库和捆绑了为Linux x86-64编译的Chromium引擎的ironpdf-engine-linux-x64。引擎工件是使得Docker部署成为必需的原因——它携带二进制文件,必须在运行时执行。

打开<dependencies>块内添加以下内容。 将LATEST_VERSION替换为Maven Central上可用的当前版本:

//:path=/static-assets/pdf/content-code-examples/tutorials/azure/pom.xml
<dependencies>

    <dependency>
        <groupId>com.ironsoftware</groupId>
        <artifactId>ironpdf</artifactId>
        <version>LATEST_VERSION</version>
    </dependency>

    <dependency>
        <groupId>com.ironsoftware</groupId>
        <artifactId>ironpdf-engine-linux-x64</artifactId>
        <version>LATEST_VERSION</version>
    </dependency>
</dependencies>
//:path=/static-assets/pdf/content-code-examples/tutorials/azure/pom.xml
<dependencies>

    <dependency>
        <groupId>com.ironsoftware</groupId>
        <artifactId>ironpdf</artifactId>
        <version>LATEST_VERSION</version>
    </dependency>

    <dependency>
        <groupId>com.ironsoftware</groupId>
        <artifactId>ironpdf-engine-linux-x64</artifactId>
        <version>LATEST_VERSION</version>
    </dependency>
</dependencies>
XML

两个工件必须使用同一个版本号。 ironpdf-engine-linux-x64之间不匹配的版本会在函数首次尝试渲染PDF时导致运行时异常。

更新mvn dependency:resolve以验证Maven能从Central下载这两个工件,然后再投入时间构建Docker镜像。

提示检查IronPDF for Java发行说明以获取最新的稳定版本。 使用最新的发行版可确保与最新的Chromium渲染引擎兼容,且避免已知的错误。

如何编写RenderPdf函数?

RenderPdf函数是一个HTTP触发的Azure Function,接受一个Content-Disposition: attachment头二进制响应返回结果PDF。 此头告知浏览器(或HTTP客户端)下载PDF而不是内联显示。

完整的函数代码在上面的快速启动中展示。 把它放在src/main/java/com/example/Function.java中,替换或扩展Maven原型生成的占位符。

//:path=/static-assets/pdf/content-code-examples/tutorials/azure/RenderPdf-annotated.java
import com.microsoft.azure.functions.*;
import com.ironsoftware.ironpdf.PdfDocument;
import java.util.Optional;

public class Function {

    @FunctionName("RenderPdf")
    public HttpResponseMessage renderPdf(
            @HttpTrigger(
                    name = "req",
                    methods = {HttpMethod.GET, HttpMethod.POST},
                    authLevel = AuthorizationLevel.ANONYMOUS)
            HttpRequestMessage<Optional<String>> request,
            final ExecutionContext context) {

        // Log each invocation for Azure Monitor / Application Insights
        context.getLogger().info("RenderPdf triggered.");

        final String url = request.getQueryParameters().get("url");

        // Return 400 if no URL was supplied
        if (url == null) {
            return request.createResponseBuilder(HttpStatus.BAD_REQUEST)
                    .body("Provide a 'url' query parameter.")
                    .build();
        }

        try {
            // renderUrlAsPdf launches Chromium, loads the page, and captures it as PDF
            PdfDocument pdf = PdfDocument.renderUrlAsPdf(url);

            // getBinaryData returns the raw PDF bytes ready for transmission
            byte[] pdfBytes = pdf.getBinaryData();

            return request.createResponseBuilder(HttpStatus.OK)
                    .body(pdfBytes)
                    .header("Content-Disposition", "attachment; filename=output.pdf")
                    .header("Content-Type", "application/pdf")
                    .build();

        } catch (Exception ex) {
            context.getLogger().severe("Rendering error: " + ex.getMessage());
            return request.createResponseBuilder(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("PDF rendering failed.")
                    .build();
        }
    }
}
//:path=/static-assets/pdf/content-code-examples/tutorials/azure/RenderPdf-annotated.java
import com.microsoft.azure.functions.*;
import com.ironsoftware.ironpdf.PdfDocument;
import java.util.Optional;

public class Function {

    @FunctionName("RenderPdf")
    public HttpResponseMessage renderPdf(
            @HttpTrigger(
                    name = "req",
                    methods = {HttpMethod.GET, HttpMethod.POST},
                    authLevel = AuthorizationLevel.ANONYMOUS)
            HttpRequestMessage<Optional<String>> request,
            final ExecutionContext context) {

        // Log each invocation for Azure Monitor / Application Insights
        context.getLogger().info("RenderPdf triggered.");

        final String url = request.getQueryParameters().get("url");

        // Return 400 if no URL was supplied
        if (url == null) {
            return request.createResponseBuilder(HttpStatus.BAD_REQUEST)
                    .body("Provide a 'url' query parameter.")
                    .build();
        }

        try {
            // renderUrlAsPdf launches Chromium, loads the page, and captures it as PDF
            PdfDocument pdf = PdfDocument.renderUrlAsPdf(url);

            // getBinaryData returns the raw PDF bytes ready for transmission
            byte[] pdfBytes = pdf.getBinaryData();

            return request.createResponseBuilder(HttpStatus.OK)
                    .body(pdfBytes)
                    .header("Content-Disposition", "attachment; filename=output.pdf")
                    .header("Content-Type", "application/pdf")
                    .build();

        } catch (Exception ex) {
            context.getLogger().severe("Rendering error: " + ex.getMessage());
            return request.createResponseBuilder(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("PDF rendering failed.")
                    .build();
        }
    }
}
JAVA

PdfDocument.renderUrlAsPdf(url)在容器内启动一个无头Chromium实例,完全加载目标URL(包括JavaScript),并将渲染输出捕获为PDF。 这会生成与用户在浏览器中看到的视觉效果相同的输出,适合捕捉现代网络应用程序、仪表板和报告页面。

重要函数触发中的authLevel = AuthorizationLevel.ANONYMOUS设置使得该端点可以公开访问。 对于生产部署,将其更改为ADMIN并在请求头中传递函数密钥。

如何为IronPDF配置Dockerfile?

IronPDF的Chromium引擎依赖于一组基础Azure Functions镜像中不包含的共享Linux库。 基础镜像mcr.microsoft.com/azure-functions/java:4-java17-build构建在Debian 11之上,因此必须使用apt安装软件包。

以下RUN命令必须添加到由Azure Functions Maven原型生成的Dockerfile中。 将它们放置在COPY步骤添加应用程序JAR之前:

//:path=/static-assets/pdf/content-code-examples/tutorials/azure/Dockerfile
FROM mcr.microsoft.com/azure-functions/java:4-java17-build AS installer-env

# Install system dependencies required by IronPDF's Chromium renderer
RUN apt-get update && apt-get install -y \
    libgdiplus \
    libxkbcommon-x11-0 \
    libc6 \
    libc6-dev \
    libgtk2.0-0 \
    libnss3 \
    libatk-bridge2.0-0 \
    libx11-xcb1 \
    libxcb-dri3-0 \
    libdrm-common \
    libgbm1 \
    libasound2 \
    libxrender1 \
    libfontconfig1 \
    libxshmfence1 \
    && apt-get install -y xvfb libva-dev libgdiplus \
    && rm -rf /var/lib/apt/lists/*

# Copy the built function JAR
COPY --from=installer-env /home/site/wwwroot /home/site/wwwroot

ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true

libgdiplus软件包提供了用于图形渲染的GDI+兼容性。 libatk-bridge2.0-0是Chromium沙盒和可访问性层所必需的。 xvfb提供虚拟帧缓冲,即使在一些Debian配置中的无头模式下Chromium也需要它。 RUN块的末尾移除包管理器缓存,保持最终镜像大小尽可能小。

请注意如果Azure Functions基础镜像版本更改或使用不同的Linux发行版作为基础,则所需的包可能有所不同。 请查阅IronPDF Linux安装指南以获取Debian、Ubuntu、CentOS和Alpine的完整依赖矩阵。

如何构建和推送Docker镜像?

在Maven项目构建完毕并更新Dockerfile后,可以组装容器镜像并将其上传到Docker注册表。 创建或更新Function App时,Azure Functions会拉取此镜像。

步骤1—构建并打包Maven项目

//:path=/static-assets/pdf/content-code-examples/tutorials/azure/build.sh
# Compile the Java code and package it as a JAR
mvn clean package
//:path=/static-assets/pdf/content-code-examples/tutorials/azure/build.sh
# Compile the Java code and package it as a JAR
mvn clean package
SHELL

Maven编译函数代码,解决所有依赖关系(包括两个IronPDF工件),并在target/目录中生成可部署的JAR。 在继续之前修复所有编译错误。

步骤2—构建Docker镜像

//:path=/static-assets/pdf/content-code-examples/tutorials/azure/docker-build.sh
# Replace <DOCKER_ID> with your Docker Hub username or ACR login server
docker build --tag <DOCKER_ID>/ironpdf-azure-functions:v1.0.0 .
//:path=/static-assets/pdf/content-code-examples/tutorials/azure/docker-build.sh
# Replace <DOCKER_ID> with your Docker Hub username or ACR login server
docker build --tag <DOCKER_ID>/ironpdf-azure-functions:v1.0.0 .
SHELL

构建安装Dockerfile中列出的Linux包,复制JAR,并将所有东西分层到最终镜像中。 首次构建可能需要几分钟,因为包下载和层缓存正在建立。 使用相同基础镜像的后续构建快得多。

步骤3—推送镜像到Docker Hub

//:path=/static-assets/pdf/content-code-examples/tutorials/azure/docker-push.sh
# Authenticate if not already logged in
docker login

# Push the image to the registry
docker push <DOCKER_ID>/ironpdf-azure-functions:v1.0.0
//:path=/static-assets/pdf/content-code-examples/tutorials/azure/docker-push.sh
# Authenticate if not already logged in
docker login

# Push the image to the registry
docker push <DOCKER_ID>/ironpdf-azure-functions:v1.0.0
SHELL

提示Azure容器注册表(ACR)是Docker Hub的私有替代方案。 ACR直接与Azure Active Directory集成,是希望镜像隐私的生产工作负荷的推荐选择。

如何将功能部署到Azure?

在注册表中有了镜像后,Azure Function App可以被创建(或更新)以参考它。 az functionapp create命令配备Function App,将其链接到存储帐户,并在一个步骤中设置容器图像。

步骤1—创建或更新Function App

//:path=/static-assets/pdf/content-code-examples/tutorials/azure/az-deploy.sh
az functionapp create \
  --name <APP_NAME> \
  --storage-account <STORAGE_NAME> \
  --resource-group AzureFunctionsContainers-rg \
  --plan myPremiumPlan \
  --deployment-container-image-name <DOCKER_ID>/ironpdf-azure-functions:v1.0.0
//:path=/static-assets/pdf/content-code-examples/tutorials/azure/az-deploy.sh
az functionapp create \
  --name <APP_NAME> \
  --storage-account <STORAGE_NAME> \
  --resource-group AzureFunctionsContainers-rg \
  --plan myPremiumPlan \
  --deployment-container-image-name <DOCKER_ID>/ironpdf-azure-functions:v1.0.0
SHELL

用Function App的全局唯一名称替换<APP_NAME>,用现有的Azure Storage帐户名替换<STORAGE_NAME>,并用在上一步中使用的Docker Hub用户名或ACR登录服务器替换<DOCKER_ID>

--plan myPremiumPlan标志选择一个Premium托管计划。 IronPDF的Chromium引擎在渲染过程中消耗大量内存; 消耗计划的1.5 GB内存上限通常是不够的。 高级计划提供至少3.5 GB并支持预热实例,从而消除冷启动延迟。

步骤2 — 验证部署

//:path=/static-assets/pdf/content-code-examples/tutorials/azure/az-verify.sh
# Check that the function app is running and the container has been pulled
az functionapp show \
  --name <APP_NAME> \
  --resource-group AzureFunctionsContainers-rg \
  --query "state"
//:path=/static-assets/pdf/content-code-examples/tutorials/azure/az-verify.sh
# Check that the function app is running and the container has been pulled
az functionapp show \
  --name <APP_NAME> \
  --resource-group AzureFunctionsContainers-rg \
  --query "state"
SHELL

当容器成功启动时,命令返回"Running"。 如果返回"Starting"或出现错误,请检查Azure门户中Function App下的Log Stream以了解容器拉取或启动错误。

警告不建议在Azure Functions上使用IronPDF的消耗(无服务器)计划。 使用Chromium进行PDF渲染所需的内存超过了消耗计划分配的内存。 使用高级或专用(App Service)计划以避免内存不足错误。

如何触发和测试功能?

一旦Function App报告RenderPdf端点就可以接受请求了。 端点URL根据Function App名称和在@FunctionName注释中定义的函数名称形成一种可预测的模式。

使用浏览器或curl进行测试:

//:path=/static-assets/pdf/content-code-examples/tutorials/azure/test-request.sh
# Replace <APP_NAME> with the Function App name
curl -o output.pdf \
  "https://<APP_NAME>.azurewebsites.net/api/RenderPdf?url=https://www.example.com"
//:path=/static-assets/pdf/content-code-examples/tutorials/azure/test-request.sh
# Replace <APP_NAME> with the Function App name
curl -o output.pdf \
  "https://<APP_NAME>.azurewebsites.net/api/RenderPdf?url=https://www.example.com"
SHELL

成功响应会在当前目录中保存一个名为output.pdf的PDF文件。 curl中的-o标志将二进制响应主体写入文件而不是打印到终端。

在浏览器中测试时,请导航到:

https://<APP_NAME>.azurewebsites.net/api/RenderPdf?url=https://www.example.com

浏览器会提示下载一个PDF。 打开它以验证页面是否正确渲染。

重要冷启动后的第一个请求可能需要20-60秒,因为Azure要拉取容器镜像及IronPDF初始化Chromium。 在同一容器生命周期内的后续请求会快得多。 高级计划的预热实例功能通过保持至少一个实例持续运行来消除冷启动。

检查错误日志:导航到Azure门户,打开功能应用,在监控下选择日志流。 来自context.getLogger()调用的日志条目几乎实时出现在这里,使渲染故障的诊断简单明了。

下一步是什么?

本指南演示了如何在Azure Functions Docker容器内部署IronPDF for Java,编写一个HTTP触发的功能将URL渲染为PDF,配置Dockerfile以满足Linux依赖项,并测试活动端点。 相同的模式仅需少量更改即可扩展到更高级的使用案例。

扩展功能:

  • 直接使用PdfDocument.renderHtmlAsPdf(htmlString)渲染HTML字符串而不是URL
  • 使用IronPDF的完整Java PDF API应用水印、合并多个PDF或添加数字签名
  • 读取请求头或POST主体以传递自定义HTML内容或渲染选项

改进生产准备度:

探索更多IronPDF for Java指南:

开始免费IronPDF试用,在评估期间无需水印即可访问所有渲染和操作功能。 准备好部署到生产环境时,查看IronPDF许可选项以找到适合项目规模的计划。

常见问题解答

为什么Azure Functions需要Docker部署IronPDF?

IronPDF附带一个本地的Chromium渲染引擎,它必须在运行时执行二进制文件。Azure Functions的Zip部署不能运行本地二进制文件,因此Docker容器镜像是唯一支持的部署路径。

运行IronPDF在Docker容器中需要哪些Maven工件?

pom.xml中需要两个工件:com.ironsoftware:ironpdf用于Java API,com.ironsoftware:ironpdf-engine-linux-x64用于本地Chromium引擎。两者必须共享相同的版本号。

Dockerfile需要为IronPDF安装哪些Linux包?

Dockerfile必须安装libgdipluslibxkbcommon-x11-0libc6libc6-devlibgtk2.0-0libnss3libatk-bridge2.0-0libx11-xcb1libxcb-dri3-0libdrm-commonlibgbm1libasound2libxrender1libfontconfig1libxshmfence1xvfblibva-dev

RenderPdf函数的作用是什么?

RenderPdf函数是一个HTTP触发的Azure函数,它读取url查询参数,将其传递给PdfDocument.renderUrlAsPdf,并使用Content-Disposition: attachment头返回生成的PDF字节,使调用者收到一个可下载的PDF文件。

使用IronPDF的Azure Functions托管计划应该选择哪个?

推荐选择Premium计划。IronPDF的Chromium引擎需要大量内存——通常超过消费计划的1.5 GB上限。Premium计划至少提供3.5 GB的内存,并支持预热实例以消除冷启动延迟。

为什么对新部署函数的首次请求很慢?

因为需要拉取容器镜像并初始化IronPDF的Chromium引擎,冷启动后的首次请求可能需要20-60秒。相同容器生命周期内的后续请求响应速度快得多。Premium计划的预热实例功能可以消除这种延迟。

如何更新现有的Azure Function App以使用新的Docker镜像?

重新构建并推送标记更新后的新镜像,然后再次运行az functionapp create,并使用新的--deployment-container-image-name值,或在Azure门户中的Deployment Center下更新容器设置。

IronPDF能否在Azure Function中渲染HTML字符串,而不仅仅是URL?

可以。替换PdfDocument.renderUrlAsPdf(url)PdfDocument.renderHtmlAsPdf(htmlString)以直接渲染HTML字符串。函数结构和响应处理保持不变。

如果请求中缺少url查询参数,会发生什么?

函数会检查url参数是否为null,并在尝试任何PDF渲染之前,返回带有描述性消息的HTTP 400错误请求响应。

Curtis Chau
技术作家

Curtis Chau 拥有卡尔顿大学的计算机科学学士学位,专注于前端开发,精通 Node.js、TypeScript、JavaScript 和 React。他热衷于打造直观且美观的用户界面,喜欢使用现代框架并创建结构良好、视觉吸引力强的手册。

除了开发之外,Curtis 对物联网 (IoT) 有浓厚的兴趣,探索将硬件和软件集成的新方法。在空闲时间,他喜欢玩游戏和构建 Discord 机器人,将他对技术的热爱与创造力相结合。

准备开始了吗?
版本: 2026.6 刚刚发布
Still Scrolling Icon

还在滚动吗?

想快速获得证据?
运行示例看着你的HTML代码变成PDF文件。