php使用Imagick处理pdf

  • 安装

FROM registry.cn-hangzhou.aliyuncs.com/jcleng/library-php:8.1-cli
ADD https://github.com/mlocati/docker-php-extension-installer/releases/download/2.7.5/install-php-extensions /usr/local/bin/install-php-extensions
RUN chmod +x /usr/local/bin/install-php-extensions && \
    ls
RUN install-php-extensions imagick
  • 配置 ImageMagick 环境设置

apt install ghostscript
vim /etc/ImageMagick-6/policy.xml
# 主要是增加:
<policy domain="coder" rights="read | write" pattern="PDF" />

# 全文
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policymap [
  <!ELEMENT policymap (policy)*>
  <!ATTLIST policymap xmlns CDATA #FIXED ''>
  <!ELEMENT policy EMPTY>
  <!ATTLIST policy xmlns CDATA #FIXED '' domain NMTOKEN #REQUIRED
    name NMTOKEN #IMPLIED pattern CDATA #IMPLIED rights NMTOKEN #IMPLIED
    stealth NMTOKEN #IMPLIED value CDATA #IMPLIED>
]>
<!--
  Configure ImageMagick policies.

  Domains include system, delegate, coder, filter, path, or resource.

  Rights include none, read, write, execute and all.  Use | to combine them,
  for example: "read | write" to permit read from, or write to, a path.

  Use a glob expression as a pattern.

  Suppose we do not want users to process MPEG video images:

    <policy domain="delegate" rights="none" pattern="mpeg:decode" />

  Here we do not want users reading images from HTTP:

    <policy domain="coder" rights="none" pattern="HTTP" />

  The /repository file system is restricted to read only.  We use a glob
  expression to match all paths that start with /repository:

    <policy domain="path" rights="read" pattern="/repository/*" />

  Lets prevent users from executing any image filters:

    <policy domain="filter" rights="none" pattern="*" />

  Any large image is cached to disk rather than memory:

    <policy domain="resource" name="area" value="1GP"/>

  Use the default system font unless overwridden by the application:

    <policy domain="system" name="font" value="/usr/share/fonts/favorite.ttf"/>

  Define arguments for the memory, map, area, width, height and disk resources
  with SI prefixes (.e.g 100MB).  In addition, resource policies are maximums
  for each instance of ImageMagick (e.g. policy memory limit 1GB, -limit 2GB
  exceeds policy maximum so memory limit is 1GB).

  Rules are processed in order.  Here we want to restrict ImageMagick to only
  read or write a small subset of proven web-safe image types:

    <policy domain="delegate" rights="none" pattern="*" />
    <policy domain="filter" rights="none" pattern="*" />
    <policy domain="coder" rights="none" pattern="*" />
    <policy domain="coder" rights="read|write" pattern="{GIF,JPEG,PNG,WEBP}" />
-->
<policymap>
  <policy domain="coder" rights="read | write" pattern="PDF" />
  <!-- <policy domain="resource" name="temporary-path" value="/tmp"/> -->
  <policy domain="resource" name="memory" value="256MiB"/>
  <policy domain="resource" name="map" value="512MiB"/>
  <policy domain="resource" name="width" value="16KP"/>
  <policy domain="resource" name="height" value="16KP"/>
  <!-- <policy domain="resource" name="list-length" value="128"/> -->
  <policy domain="resource" name="area" value="128MP"/>
  <policy domain="resource" name="disk" value="1GiB"/>
</policymap>
  • pdf 转图片

/**
 * 使用Imagick把pdf转为图片
 * @param string $url_or_abpath url会在
 * @param string $get_base64 是否获取base64或者二进制文件流
 * @return string|source base64的图片|二进制数据
 * @author lxx
 */
public function pdf2toBase64ImgString($url_or_abpath, $get_base64 = true)
{
    if (!extension_loaded('imagick')) {
        throw new MineException('imagick扩展未安装!');
    }
    $backgroundColor = new \ImagickPixel('white');
    $image = new \Imagick();
    // 设置清晰度
    $image->setResolution(300, 300);
    $image->setCompressionQuality(100);
    // 读取文件
    $new_file_path = BASE_PATH . '/runtime/' . date('Ymd_His') . rand(10, 20) . '.pdf';
    file_put_contents($new_file_path, file_get_contents($url_or_abpath));
    if (!file_exists($new_file_path)) {
        throw new MineException('文件保存失败!');
    }
    $image->readImage($new_file_path);
    $image->setImageBackgroundColor($backgroundColor);
    $image = $image->mergeImageLayers(\Imagick::LAYERMETHOD_FLATTEN);
    // 设置新的宽度和高度
    $newWidth = (76 - 6) * 8;  // 设定需要的宽度
    $newHeight = (130 - 0) * 8; // 设定需要的高度
    // 调整图片大小
    $image->resizeImage($newWidth, $newHeight, \Imagick::FILTER_LANCZOS, 1);
    $image->setImageFormat('png');

    // // 新增顶部距离
    // $topMargin = 16; // 顶部边距
    // $new_image = new \Imagick();
    // $new_image->newImage($newWidth, $1newHeight + $topMargin, $backgroundColor);

    // // 将当前页放置在新画布上,调整位置以应用顶部边距
    // $new_image->compositeImage($image, \Imagick::COMPOSITE_OVER, 0, $topMargin);
    // $new_image->setImageFormat('png');
    // $image = $new_image;
    if ($get_base64) {
        return 'data:image/png;base64,' .  base64_encode($image->getimageblob());
    } else {
        return $image->getimageblob();
    }
}
  • 图片转 pdf

public function base64_to_pdf($base64_string, $output_file)
{
    // 去掉 Base64 字符串中的头部信息
    $base64_string = preg_replace('/^data:image\/\w+;base64,/', '', $base64_string);
    $base64_string = str_replace(' ', '+', $base64_string);

    // 解码 Base64 字符串
    $image_data = base64_decode($base64_string);

    if ($image_data === false) {
        throw new \Exception("Base64 解码失败");
    }

    // 创建 Imagick 对象并读取图像
    $imagick = new \Imagick();
    $imagick->readImageBlob($image_data);
    // $imagick->readImage("file_img.png");

    // 将图像格式转换为 PDF
    $imagick->setImageFormat('pdf');

    // 写入 PDF 文件
    // $imagick->writeImage($output_file);

    // * 直接输出文件
    $pdfBinary = $imagick->getimageblob();
    $pdfFilePath = 'file.pdf';
    header("Access-Control-Allow-Origin: *");
    header("Access-Control-Allow-Methods: GET, POST, OPTIONS, PUT, DELETE");
    header("Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With");
    //
    header('Content-Type: application/pdf');
    header('Content-Disposition: inline; filename="' . basename($pdfFilePath) . '"');
    header('Content-Length: ' . strlen($pdfBinary));
    // 清除 Imagick 资源
    $imagick->clear();
    $imagick->destroy();
    echo $pdfBinary;
    // return $output_file;

    // ! 不用echo或者使用php://output写
    $pdfFilePath = 'file.pdf';
    $Binary = file_get_contents("./output.pdf");
    // 把内容写到php://output
    $output = fopen('php://output', 'wb');

    header('Content-Type: application/pdf');
    header('Content-Disposition: inline; filename="' . basename($pdfFilePath) . '"');
    header('Content-Length: ' . strlen($Binary));
    // 写数据
    fwrite($output, $Binary);
    fclose($output);
    // end

    // ! 如果是hyperf
    $pdfFilePath = 'file.pdf';
    $Binary = file_get_contents(BASE_PATH . "/.vscode/output.pdf");

    $filename = basename($pdfFilePath);
    return container()->get(MineResponse::class)->getResponse()
        ->withHeader('Server', 'MineAdmin')
        ->withHeader('access-control-expose-headers', 'content-disposition')
        ->withHeader('content-description', 'File Transfer')
        ->withHeader('content-type', 'application/pdf')
        ->withHeader('content-disposition', "attachment; filename={$filename}; filename*=UTF-8''" . rawurlencode($filename))
        ->withHeader('content-transfer-encoding', 'binary')
        ->withHeader('pragma', 'public')
        ->withBody(new SwooleStream($Binary));

}

/**
 * 文件转base64
 *
 * @param string $url_or_abpath 文件/url地址的文件
 * @param bool $is_cotent 是否是内容
 * @return string
 * @author lxx
 */
public function toBase64($url_or_abpath, $is_cotent = false)
{
    if ($is_cotent == true) {
        return base64_encode($url_or_abpath);
    } else {
        $fileContent = file_get_contents($url_or_abpath);
        if ($fileContent === false) {
            throw new MineException('转换Base64失败, 文件获取失败');
        }
        return base64_encode($fileContent);
    }
}
  • 配合(printjs)[https://printjs.crabbly.com/]进行页面打印pdf文件或者base64图片

# 使用 print-js-updated 版本, 原版在firefox上显示空白
# https://www.npmjs.com/package/print-js-updated?activeTab=versions
# https://github.com/crabbly/Print.js/issues/665
npm i print-js-updated

import printJS from "print-js-updated";
<script>
    function printPdf(pdfurl) {
        printJS({
            printable: pdfurl,
            onPrintDialogClose: function (res) {
                // 弹窗询问?
            },
        });
    }
    function printBaseImg(base64Data) {
        var ext_config = {
            type: "image",
            base64: true,
            style: "@media print { @page {size: auto; margin: 0; } body{margin:0 5px}}",
            imageStyle: "width: 100%; margin-bottom: 0px;",
        };
        printJS({
            printable: base64Data,
            ...ext_config,
            onPrintDialogClose: function (res) {
                // 弹窗询问?
            },
        });
    }
</script>