0

0

将 Snappy PDF 生成的字符串转换为服务器上的加密 PDF

花韻仙語

花韻仙語

发布时间:2025-12-04 13:34:20

|

789人浏览过

|

来源于php中文网

原创

将 snappy pdf 生成的字符串转换为服务器上的加密 pdf

本文详细介绍了如何在 Symfony 3.4 应用程序中,将 Snappy PDF 生成的原始 PDF 字符串保存为服务器上的文件,并利用 `qpdf` 工具通过 Symfony 的 `Process` 组件对其进行密码保护。教程涵盖了文件写入、外部命令执行、错误处理以及临时文件清理等关键步骤,旨在提供一个将无保护 PDF 转换为安全加密 PDF 的完整解决方案。

背景与挑战

在使用 Snappy PDF(基于 wkhtmltopdf)生成 PDF 文档时,其通常以字符串形式返回生成的 PDF 内容。然而,wkhtmltopdf 本身不直接提供密码保护功能。为了满足对 PDF 文档进行加密的需求,我们需要将这个 PDF 字符串写入服务器上的临时文件,然后借助外部工具(如 qpdf)对其进行加密处理。在 Symfony 应用程序中,执行此类系统级命令的最佳实践是使用其内置的 Process 组件。

解决方案概述

本教程将分步指导您完成以下操作:

  1. 将 Snappy PDF 返回的 PDF 字符串保存到服务器上的一个临时文件。
  2. 使用 Symfony 的 Process 组件执行 qpdf 命令,对临时 PDF 文件进行密码保护。
  3. 读取加密后的 PDF 文件内容,并清理所有临时文件。

步骤一:保存 PDF 字符串到临时文件

首先,您需要将 Snappy PDF 生成的 PDF 内容(一个二进制字符串)写入服务器上的一个临时文件。这可以通过 PHP 的 file_put_contents 函数轻松实现。为了确保文件名的唯一性,可以使用 tempnam 函数生成一个唯一的临时文件名。

<?php

use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;

// 假设 $pdfString 是 Snappy PDF 返回的原始 PDF 内容
$pdfString = '%PDF-1.4
%âã
1 0 obj
<<
/Title ()
/Creator (??wkhtmltopdf 0.12.5)
/Producer (??Qt 5.11.3)
/CreationDate (D:20211221165455Z)
>>
endobj
2 0 obj
<<
/Type /Catalog
/Pages 3 0 R
...
'; // 实际的 PDF 字符串会更长

// 生成一个唯一的临时文件路径
// sys_get_temp_dir() 获取系统临时目录
$tempDir = sys_get_temp_dir();
$unprotectedPdfPath = tempnam($tempDir, 'unprotected_pdf_');
if ($unprotectedPdfPath === false) {
    throw new \RuntimeException('无法创建临时文件用于未保护的 PDF。');
}

// 将 PDF 字符串写入临时文件
$bytesWritten = file_put_contents($unprotectedPdfPath, $pdfString);
if ($bytesWritten === false) {
    // 尝试清理可能已创建但写入失败的文件
    if (file_exists($unprotectedPdfPath)) {
        unlink($unprotectedPdfPath);
    }
    throw new \RuntimeException('无法将 PDF 字符串写入文件: ' . $unprotectedPdfPath);
}

// 确保文件权限正确,以便 qpdf 可以读取
chmod($unprotectedPdfPath, 0644);

echo "未保护的 PDF 已保存到: " . $unprotectedPdfPath . "\n";

?>

注意事项:

  • tempnam() 函数会创建一个空文件,并返回其路径。
  • sys_get_temp_dir() 确保临时文件存储在系统指定的临时目录中。
  • 务必检查 file_put_contents 的返回值,以确保写入成功。
  • 设置正确的文件权限 (chmod) 对于外部工具访问文件至关重要。

步骤二:使用 qpdf 进行密码保护

接下来,我们将使用 qpdf 工具对刚刚创建的临时 PDF 文件进行密码保护。qpdf 是一个强大的命令行工具,用于转换和检查 PDF 文件。您需要确保 qpdf 已安装在您的服务器上。

Tago AI
Tago AI

AI生成带货视频,专为电商卖货而生

下载

我们将使用 Symfony 的 Process 组件来执行 qpdf 命令。

<?php
// ... (接上面的代码)

// 定义加密后的 PDF 文件的临时路径
$protectedPdfPath = tempnam($tempDir, 'protected_pdf_');
if ($protectedPdfPath === false) {
    // 清理未保护的 PDF
    if (file_exists($unprotectedPdfPath)) {
        unlink($unprotectedPdfPath);
    }
    throw new \RuntimeException('无法创建临时文件用于受保护的 PDF。');
}

$ownerPassword = 'owner_password_here'; // 所有者密码
$userPassword = 'user_password_here';   // 用户密码
$keyLength = 256; // 密钥长度,可选 40, 128, 256

// 构建 qpdf 命令
// --encrypt [用户密码] [所有者密码] [密钥长度] -- [权限] [输入文件] [输出文件]
// 权限可以根据需要调整,例如:
// --print=full --copy=n --modify=n --annotate=n --extract=n --assemble=n --fill-form=n --access-support=y
// 这里我们使用一个简单的权限设置,允许打印和修改
$command = [
    'qpdf',
    '--encrypt',
    $userPassword,
    $ownerPassword,
    (string)$keyLength, // 密钥长度必须是字符串
    '--print=full',     // 允许打印
    '--modify=full',    // 允许修改
    '--',               // 权限选项和文件路径之间的分隔符
    $unprotectedPdfPath, // 输入文件
    $protectedPdfPath    // 输出文件
];

// 创建 Process 实例
$process = new Process($command);
$process->setTimeout(60); // 设置超时时间,根据 PDF 大小调整

try {
    $process->run();

    // 检查命令是否成功执行
    if (!$process->isSuccessful()) {
        throw new ProcessFailedException($process);
    }

    echo "PDF 已成功加密并保存到: " . $protectedPdfPath . "\n";

    // 确保加密后的文件权限正确
    chmod($protectedPdfPath, 0644);

} catch (ProcessFailedException $exception) {
    // 处理命令执行失败的情况
    echo "加密 PDF 失败。\n";
    echo "错误输出: " . $exception->getMessage() . "\n";
    echo "标准输出: " . $process->getOutput() . "\n";
    echo "错误输出: " . $process->getErrorOutput() . "\n";

    // 清理可能已创建的文件
    if (file_exists($unprotectedPdfPath)) {
        unlink($unprotectedPdfPath);
    }
    if (file_exists($protectedPdfPath)) {
        unlink($protectedPdfPath);
    }
    throw $exception; // 重新抛出异常
} finally {
    // 无论成功与否,都尝试删除未保护的临时文件
    if (file_exists($unprotectedPdfPath)) {
        unlink($unprotectedPdfPath);
        echo "已清理未保护的临时文件: " . $unprotectedPdfPath . "\n";
    }
}

?>

注意事项:

  • qpdf 安装:确保您的服务器上已安装 qpdf。在 Debian/Ubuntu 系统上,可以使用 sudo apt-get install qpdf 安装。
  • 命令参数:qpdf --encrypt 命令的参数顺序和含义非常重要。
    • --encrypt zuojiankuohaophpcnuser-password> <owner-password> <key-length>:设置用户密码、所有者密码和加密密钥长度。
    • 权限选项(如 --print=full)控制用户可以对文档执行的操作。
    • -- 是一个重要的分隔符,用于区分 qpdf 的选项和文件路径参数。
  • 错误处理:ProcessFailedException 是处理命令执行失败的关键。通过 getOutput() 和 getErrorOutput() 可以获取命令的标准输出和错误输出,这对于调试非常有帮助。
  • 超时设置:setTimeout() 方法可以防止长时间运行的命令导致脚本挂起。

步骤三:读取加密后的 PDF 并清理

加密完成后,您可以读取加密后的 PDF 文件内容,以便进行后续处理(例如将其作为 HTTP 响应返回给用户,或上传到云存储)。最后,务必清理所有临时文件,以避免服务器磁盘空间被占用。

<?php
// ... (接上面的代码)

$protectedPdfContent = null;
if (file_exists($protectedPdfPath)) {
    $protectedPdfContent = file_get_contents($protectedPdfPath);
    if ($protectedPdfContent === false) {
        echo "无法读取加密后的 PDF 文件内容。\n";
        // 尝试清理文件
        unlink($protectedPdfPath);
        throw new \RuntimeException('无法读取加密后的 PDF 文件: ' . $protectedPdfPath);
    }
    echo "已成功读取加密后的 PDF 内容。\n";
}

// 在完成所有操作后,清理加密后的临时文件
if (file_exists($protectedPdfPath)) {
    unlink($protectedPdfPath);
    echo "已清理加密后的临时文件: " . $protectedPdfPath . "\n";
}

// 现在 $protectedPdfContent 包含了密码保护的 PDF 文件的二进制内容
// 您可以将其返回给浏览器,或者进行其他处理
// 例如:
// return new Response($protectedPdfContent, 200, [
//     'Content-Type' => 'application/pdf',
//     'Content-Disposition' => 'attachment; filename="protected_document.pdf"',
// ]);

?>

综合示例代码

将上述所有步骤整合到一个完整的 PHP 脚本中,您可以在 Symfony 控制器或服务中调用此逻辑。

<?php

namespace AppBundle\Service; // 假设在一个 Symfony 服务中

use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\HttpFoundation\Response; // 如果要直接返回给浏览器

class PdfProtectorService
{
    public function protectPdfString(string $pdfString, string $userPassword, string $ownerPassword): ?string
    {
        $tempDir = sys_get_temp_dir();
        $unprotectedPdfPath = false;
        $protectedPdfPath = false;

        try {
            // 1. 保存 PDF 字符串到临时文件
            $unprotectedPdfPath = tempnam($tempDir, 'unprotected_pdf_');
            if ($unprotectedPdfPath === false) {
                throw new \RuntimeException('无法创建临时文件用于未保护的 PDF。');
            }
            if (file_put_contents($unprotectedPdfPath, $pdfString) === false) {
                throw new \RuntimeException('无法将 PDF 字符串写入文件: ' . $unprotectedPdfPath);
            }
            chmod($unprotectedPdfPath, 0644);

            // 2. 使用 qpdf 进行密码保护
            $protectedPdfPath = tempnam($tempDir, 'protected_pdf_');
            if ($protectedPdfPath === false) {
                throw new \RuntimeException('无法创建临时文件用于受保护的 PDF。');
            }

            $keyLength = 256;
            $command = [
                'qpdf',
                '--encrypt',
                $userPassword,
                $ownerPassword,
                (string)$keyLength,
                '--print=full',
                '--modify=full',
                '--',
                $unprotectedPdfPath,
                $protectedPdfPath
            ];

            $process = new Process($command);
            $process->setTimeout(120); // 增加超时时间以适应大型 PDF

            $process->run();

            if (!$process->isSuccessful()) {
                throw new ProcessFailedException($process);
            }
            chmod($protectedPdfPath, 0644);

            // 3. 读取加密后的 PDF 内容
            $protectedPdfContent = file_get_contents($protectedPdfPath);
            if ($protectedPdfContent === false) {
                throw new \RuntimeException('无法读取加密后的 PDF 文件: ' . $protectedPdfPath);
            }

            return $protectedPdfContent;

        } catch (\Exception $e) {
            // 记录错误或进行其他错误处理
            error_log("PDF 加密失败: " . $e->getMessage());
            if ($e instanceof ProcessFailedException) {
                error_log("QPDF 错误输出: " . $e->getProcess()->getErrorOutput());
                error_log("QPDF 标准输出: " . $e->getProcess()->getOutput());
            }
            return null; // 或者抛出自定义异常
        } finally {
            // 清理所有临时文件
            if ($unprotectedPdfPath && file_exists($unprotectedPdfPath)) {
                unlink($unprotectedPdfPath);
            }
            if ($protectedPdfPath && file_exists($protectedPdfPath)) {
                unlink($protectedPdfPath);
            }
        }
    }
}

// 如何在控制器中使用:
// class MyController extends Controller
// {
//     public function generateProtectedPdfAction()
//     {
//         // 假设您已经从 Snappy PDF 获取了 $originalPdfString
//         $originalPdfString = $this->get('snappy.pdf')->getOutputFromHtml('<h1>Hello World</h1>');
//
//         $pdfProtectorService = $this->get('app.pdf_protector_service'); // 假设服务已注册
//         $protectedPdfContent = $pdfProtectorService->protectPdfString(
//             $originalPdfString,
//             'my_user_password',
//             'my_owner_password'
//         );
//
//         if ($protectedPdfContent) {
//             return new Response($protectedPdfContent, 200, [
//                 'Content-Type' => 'application/pdf',
//                 'Content-Disposition' => 'attachment; filename="protected_document.pdf"',
//             ]);
//         } else {
//             // 处理加密失败的情况
//             return new Response('PDF 加密失败。', 500);
//         }
//     }
// }

?>

总结

通过利用 Symfony 的 Process 组件,我们能够有效地在 PHP 应用程序中执行外部命令行工具(如 qpdf),从而实现了将 Snappy PDF 生成的原始 PDF 字符串转换为密码保护的 PDF 文件。这个过程涉及临时文件管理、外部命令的构建与执行、以及健壮的错误处理。遵循这些步骤和最佳实践,可以确保您的应用程序能够安全、可靠地处理 PDF 加密需求。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

WorkBuddy
WorkBuddy

腾讯云推出的AI原生桌面智能体工作台

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
PHP Symfony框架
PHP Symfony框架

本专题专注于PHP主流框架Symfony的学习与应用,系统讲解路由与控制器、依赖注入、ORM数据操作、模板引擎、表单与验证、安全认证及API开发等核心内容。通过企业管理系统、内容管理平台与电商后台等实战案例,帮助学员全面掌握Symfony在企业级应用开发中的实践技能。

87

2025.09.11

python中print函数的用法
python中print函数的用法

python中print函数的语法是“print(value1, value2, ..., sep=' ', end=' ', file=sys.stdout, flush=False)”。本专题为大家提供print相关的文章、下载、课程内容,供大家免费下载体验。

192

2023.09.27

python print用法与作用
python print用法与作用

本专题整合了python print的用法、作用、函数功能相关内容,阅读专题下面的文章了解更多详细教程。

19

2026.02.03

js 字符串转数组
js 字符串转数组

js字符串转数组的方法:1、使用“split()”方法;2、使用“Array.from()”方法;3、使用for循环遍历;4、使用“Array.split()”方法。本专题为大家提供js字符串转数组的相关的文章、下载、课程内容,供大家免费下载体验。

760

2023.08.03

js截取字符串的方法
js截取字符串的方法

js截取字符串的方法有substring()方法、substr()方法、slice()方法、split()方法和slice()方法。本专题为大家提供字符串相关的文章、下载、课程内容,供大家免费下载体验。

221

2023.09.04

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1567

2023.10.24

字符串介绍
字符串介绍

字符串是一种数据类型,它可以是任何文本,包括字母、数字、符号等。字符串可以由不同的字符组成,例如空格、标点符号、数字等。在编程中,字符串通常用引号括起来,如单引号、双引号或反引号。想了解更多字符串的相关内容,可以阅读本专题下面的文章。

649

2023.11.24

java读取文件转成字符串的方法
java读取文件转成字符串的方法

Java8引入了新的文件I/O API,使用java.nio.file.Files类读取文件内容更加方便。对于较旧版本的Java,可以使用java.io.FileReader和java.io.BufferedReader来读取文件。在这些方法中,你需要将文件路径替换为你的实际文件路径,并且可能需要处理可能的IOException异常。想了解更多java的相关内容,可以阅读本专题下面的文章。

1228

2024.03.22

C# ASP.NET Core微服务架构与API网关实践
C# ASP.NET Core微服务架构与API网关实践

本专题围绕 C# 在现代后端架构中的微服务实践展开,系统讲解基于 ASP.NET Core 构建可扩展服务体系的核心方法。内容涵盖服务拆分策略、RESTful API 设计、服务间通信、API 网关统一入口管理以及服务治理机制。通过真实项目案例,帮助开发者掌握构建高可用微服务系统的关键技术,提高系统的可扩展性与维护效率。

76

2026.03.11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PHP课程
PHP课程

共137课时 | 13.4万人学习

JavaScript ES5基础线上课程教学
JavaScript ES5基础线上课程教学

共6课时 | 11.3万人学习

PHP新手语法线上课程教学
PHP新手语法线上课程教学

共13课时 | 1.0万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号