【使用 php 为 excel 添加水印 php给excel添加背景图片水印】


Github源代码
Laravel项目下调用

使用 php 为 excel 添加水印 php给excel添加背景图片水印

<?php

namespace Services;

use ZipArchive;
use Exception;

/**
 * Class Watermark
 * 用于在基于OpenXML格式的Excel文件(xlsx)中添加水印
 *
 * @version 1.0
 *
 * @example
 *          $watermark = new \Services\Watermark('D:/a.xlsx');
 *          $imageNumber = $watermark->addImage('D:/images/b.png');
 *          $watermark->getSheet(1)->setBgImg($imageNumber);
 *          $watermark->close();
 *
 * @package Services
 */
class Watermark {

    /**
     * @var ZipArchive 用于处理zip文件的ZipArchive对象
     */
    private $zip;

    /**
     * @var int 图像序号,用于给每个添加的图片分配唯一编号
     */
    private $num = 1;

    /**
     * @var int sheet序号,用于指定当前操作的工作表
     */
    private $sheet = 1;

    /**
     * @var array 存储图像后缀名的数组
     */
    private $suffixArr = [];

    /**
     * @var array 存储图像唯一名称的数组
     */
    private $nameArr = [];

    /**
     * @var string 关系文件路径的格式字符串
     */
    private const RELS_PATH = 'xl/worksheets/_rels/sheet%s.xml.rels';

    /**
     * @var string 工作表文件路径的格式字符串
     */
    private const SHEET_PATH = 'xl/worksheets/sheet%s.xml';

    /**
     * @var string 媒体文件路径的格式字符串
     */
    private const MEDIA_PATH = 'xl/media/bgimage%s.%s';

    /**
     * 初始化
     *
     * @param string|null $file 压缩包文件名
     *
     * @throws Exception 如果文件无法打开,抛出异常
     */
    public function __construct(?string $file = null) {
        if (!empty($file)) {
            $this->openFile($file); // 如果提供了文件名,尝试打开文件
        }
    }

    /**
     * 打开zip文件
     *
     * @param string $file 压缩包文件名
     * @throws Exception 如果文件无法打开,抛出异常
     */
    private function openFile(string $file): void {
        $this->zip = new ZipArchive(); // 创建一个新的ZipArchive对象
        if ($this->zip->open($file) !== true) {
            throw new Exception("Unable to open the file: $file"); // 如果打开文件失败,抛出异常
        }
    }

    /**
     * 设置zip文件
     *
     * @param string $file 压缩包文件名
     * @return bool 成功打开文件返回true
     *
     * @throws Exception 如果文件无法打开,抛出异常
     */
    public function setFile(string $file): bool {
        $this->openFile($file); // 调用openFile方法打开文件
        return true; // 返回true表示成功
    }

    /**
     * 添加图片到zip文件中
     *
     * @param string $file 图片文件名
     * @return int 添加进来的图像的编号
     *
     * @throws Exception 如果无法添加图片,抛出异常
     */
    public function addImage(string $file): int {
        $suffix = pathinfo($file, PATHINFO_EXTENSION); // 获取图片文件的后缀名
        $name = uniqid(); // 生成一个唯一的图片名称
        if (!$this->zip->addFile($file, sprintf(self::MEDIA_PATH, $name, $suffix))) {
            throw new Exception("Unable to add image: $file"); // 如果无法添加图片,抛出异常
        }
        $num = $this->num; // 获取当前图片编号
        $this->suffixArr[$num] = $suffix; // 存储图片的后缀名
        $this->nameArr[$num] = $name; // 存储图片的唯一名称
        $this->num++; // 增加图片编号
        return $num; // 返回图片编号
    }

    /**
     * 获取指定的工作表
     *
     * @param int $num sheet编号
     * @return $this 返回当前对象实例
     */
    public function getSheet(int $num = 1): self {
        $this->sheet = $num; // 设置当前操作的工作表编号
        return $this; // 返回当前对象实例,支持方法链调用
    }

    /**
     * 设置背景图
     *
     * @param int $num 图像编号
     *
     * @throws Exception 如果图像编号无效,抛出异常
     */
    public function setBgImg(int $num): void {
        if (!isset($this->suffixArr[$num]) || !isset($this->nameArr[$num])) {
            throw new Exception("Invalid image number: $num"); // 如果图像编号无效,抛出异常
        }

        $nowSuffix = strtolower($this->suffixArr[$num]); // 获取图像后缀名并转为小写
        $relContent = $this->generateRelContent($num, $nowSuffix); // 生成关系文件的内容
        $this->addToZip(sprintf(self::RELS_PATH, $this->sheet), $relContent); // 将关系文件内容添加到zip文件中

        $sheetContent = $this->getUpdatedSheetContent(); // 获取更新后的工作表内容
        $this->addToZip(sprintf(self::SHEET_PATH, $this->sheet), $sheetContent); // 将更新后的工作表内容添加到zip文件中

        $contentTypes = $this->updateContentTypes($nowSuffix); // 更新内容类型文件
        $this->addToZip("[Content_Types].xml", $contentTypes); // 将更新后的内容类型文件添加到zip文件中
    }

    /**
     * 关闭zip文件
     */
    public function close(): void {
        if ($this->zip) {
            $this->zip->close(); // 关闭zip文件
        }
    }

    /**
     * 生成关系文件的内容
     *
     * @param int $num 图像编号
     * @param string $suffix 图像后缀名
     * @return string 生成的关系文件内容
     */
    private function generateRelContent(int $num, string $suffix): string {
        return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/bgimage' . $this->nameArr[$num] . '.' . $suffix . '"/>
</Relationships>'; // 返回生成的关系文件内容
    }

    /**
     * 获取更新后的工作表内容
     *
     * @return string 更新后的工作表内容
     *
     * @throws Exception 如果无法获取工作表内容,抛出异常
     */
    private function getUpdatedSheetContent(): string {
        $sheetContent = $this->zip->getFromName(sprintf(self::SHEET_PATH, $this->sheet)); // 获取当前工作表的内容
        if ($sheetContent === false) {
            throw new Exception("Unable to get sheet content"); // 如果无法获取工作表内容,抛出异常
        }
        return str_replace('</worksheet>', '<picture r:id="rId1"/></worksheet>', $sheetContent); // 在工作表内容中添加图片标记并返回更新后的内容
    }

    /**
     * 更新内容类型文件
     *
     * @param string $suffix 图像后缀名
     * @return string 更新后的内容类型文件内容
     *
     * @throws Exception 如果无法获取内容类型文件,抛出异常
     */
    private function updateContentTypes(string $suffix): string {
        $contentTypes = $this->zip->getFromName('[Content_Types].xml'); // 获取内容类型文件的内容
        if ($contentTypes === false) {
            throw new Exception("Unable to get content types"); // 如果无法获取内容类型文件,抛出异常
        }
        if (!strpos($contentTypes, 'Extension="' . $suffix . '"') || !strpos($contentTypes, 'ContentType="image/' . $suffix . '"')) {
            $contentTypes = str_replace('</Types>', '<Default ContentType="image/' . $suffix . '" Extension="' . $suffix . '"/></Types>', $contentTypes); // 如果内容类型文件中不包含当前图像的后缀名,添加相应的内容类型
        }
        return $contentTypes; // 返回更新后的内容类型文件内容
    }

    /**
     * 添加内容到zip文件
     *
     * @param string $path 文件路径
     * @param string $content 文件内容
     *
     * @throws Exception 如果无法添加内容到zip文件,抛出异常
     */
    private function addToZip(string $path, string $content): void {
        if (!$this->zip->addFromString($path, $content)) {
            throw new Exception("Unable to add content to zip: $path"); // 如果无法添加内容到zip文件,抛出异常
        }
    }
}

调用示例

// 创建Watermark实例并打开Excel文件
$watermark = new \Services\Watermark('D:/a.xlsx');

// 添加图片到Excel文件中,并获取图像编号
$imageNumber = $watermark->addImage('D:/images/b.png');

// 获取指定的工作表并设置背景图
$watermark->getSheet(1)->setBgImg($imageNumber);

// 关闭Excel文件
$watermark->close();
<?php
namespace Modules\Admin\Services\excel;

use Modules\Admin\Services\BaseApiService;
use Services\Watermark;
use Services\WatermarkImg;
use App\Jobs\GenerateExcelJob;
use Illuminate\Http\Request;

class ExportService extends BaseApiService
{
    /**
     * 导出Excel
     * @return void
     * @throws \PHPExcel_Exception
     * @throws \PHPExcel_Reader_Exception
     * @throws \PHPExcel_Writer_Exception
     */
    public function export(){
        date_default_timezone_set("Asia/Shanghai");

        $objPhpExcel = new \PHPExcel();

        // 设置 Excel 文件属性
        $objPhpExcel->getProperties()->setCreator("Me")
            ->setLastModifiedBy("Me")
            ->setTitle("Office 2007 XLSX Test Document")
            ->setSubject("Office 2007 XLSX Test Document")
            ->setDescription("Test document for Office 2007 XLSX, generated using PHP classes.")
            ->setKeywords("office 2007 openxml php")
            ->setCategory("Test result file");

        // 设置活动工作表
        $objPhpExcel->setActiveSheetIndex(0);
        $sheet = $objPhpExcel->getActiveSheet();

        // 填充示例数据
        $sheet->setCellValue('A1', 'Locked Cell');
        $sheet->setCellValue('B1', 'Unlocked Cell');
        $sheet->setCellValue('C1', 'Locked Cell');
        $sheet->setCellValue('D1', 'Locked Cell');
        $sheet->setCellValue('E1', 'Locked Cell');
        $sheet->setCellValue('F1', 'Locked Cell');
        $sheet->setCellValue('G1', 'Locked Cell');
        $sheet->setCellValue('H1', 'Locked Cell');
        for ($i = 2; $i <= 100; $i++) {
            $sheet->setCellValue('A' . $i, 'Locked ' . $i);
            $sheet->setCellValue('B' . $i, 'Unlocked ' . $i);
            $sheet->setCellValue('C' . $i, 'Locked ' . $i);
            $sheet->setCellValue('D' . $i, 'Locked ' . $i);
            $sheet->setCellValue('E' . $i, 'Locked ' . $i);
            $sheet->setCellValue('F' . $i, 'Locked ' . $i);
            $sheet->setCellValue('G' . $i, 'Locked ' . $i);
            $sheet->setCellValue('H' . $i, 'Locked ' . $i);
        }

        // 保护整个工作表
        $sheet->getProtection()->setSheet(true);
        $sheet->getProtection()->setPassword('123456');

        // 解锁特定列(例如,列 B)
        foreach (range(1, 7) as $row) {
            $sheet->getStyle('B' . $row)->getProtection()->setLocked(\PHPExcel_Style_Protection::PROTECTION_UNPROTECTED);
        }

        $filePath = public_path() . "/uploads/" . "protected_excel_test.xlsx";
        $objWriter = \PHPExcel_IOFactory::createWriter($objPhpExcel, 'Excel2007');
        $objWriter->save($filePath);

        $water = new Watermark($filePath);
        //生成水印图片
        [$status,$msg,$imgPath] = WatermarkImg::create(["某某系统", date("Y年m月d日 H时i分s秒")],'#FED8D8',public_path() . '/font.ttf',public_path() . "/uploads/");
        if(!$status){
            die($msg);
        }
        $num = $water->addImage($imgPath);
        $water->getSheet(1)->setBgImg($num);
        $water->close();

        header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
        header('Content-Disposition: attachment;filename="protected_excel_test.xlsx"');
        header('Cache-Control: max-age=0');

        readfile($filePath);

        unlink($filePath);
        unlink($imgPath);
        exit;
    }

根据文本生成水印图片

<?php

namespace Services;

/**
 * 生成水印图片
 *
 * @example 使用示例:
 *          WatermarkImg::create(['Sample Watermark Line 1', 'Sample Watermark Line 2'],'#FF6666','/path/to/font.ttf','/path/to/save/folder',3500,2400,20,30,1,130);
 */
class WatermarkImg
{

    /**
     * 创建水印图片
     *
     * @param array $lines 需要显示的水印文字行数组
     * @param string $fontColorHex 水印文字的颜色(十六进制表示)
     * @param string $fontPath 字体文件的路径
     * @param string $saveFolderPath 保存水印图片的文件夹路径
     * @param int $width 生成水印图片的宽度
     * @param int $height 生成水印图片的高度
     * @param int $fontSize 水印文字的初始字体大小
     * @param int $angle 水印文字的旋转角度
     * @param int $spacingDiagonal 倾斜方向上的间距
     * @param int $spacingVertical 垂直方向上的间距
     *
     * @return array 返回包含生成结果的数组:成功与否、错误信息、保存路径
     *
     * @example
     * create(
     *     ['Sample Watermark Line 1', 'Sample Watermark Line 2'],
     *     '#FF6666',
     *     '/path/to/font.ttf',
     *     '/path/to/save/folder',
     *     3500,
     *     2400,
     *     20,
     *     30,
     *     1,
     *     130
     * );
     */
    public static function create(
        $lines = [],
        $fontColorHex = '#FF6666',
        $fontPath = '',
        $saveFolderPath = '',
        $width = 3500,
        $height = 2400,
        $fontSize = 20,
        $angle = 30,
        $spacingDiagonal = 1, // 倾斜方向上的间距
        $spacingVertical = 130 // 垂直方向上的间距
    ) {
        // 确保提供的 lines 数组不为空
        if (empty($lines)) {
            return [false, "水印文本参数不能为空", null];
        }

        // 创建一个透明图像
        $image = imagecreatetruecolor($width, $height);

        // 设置背景为透明并开启 alpha 通道
        imagesavealpha($image, true);
        $transparent = imagecolorallocatealpha($image, 255, 255, 255, 127);
        imagefill($image, 0, 0, $transparent);

        // 设置文字颜色
        $fontColor = sscanf($fontColorHex, "#%02x%02x%02x");
        $fontColor = imagecolorallocate($image, ...$fontColor);

        // 检查字体文件是否存在
        if (!file_exists($fontPath)) {
            return [false, "生成水印图片所需字体文件不存在", $fontPath];
        }

        // 动态调整字体大小,确保两行文字都能适应图片
        do {
            $fontSize--;
            $textBox = [];
            $textWidth = 0;
            $textHeight = 0;
            foreach ($lines as $line) {
                // 计算每行文字的边界框,并更新文本宽度和高度
                $textBox[] = imagettfbbox($fontSize, $angle, $fontPath, $line);
                $textWidth = max($textWidth, $textBox[count($textBox) - 1][2] - $textBox[count($textBox) - 1][0]);
                $textHeight += abs($textBox[count($textBox) - 1][7] - $textBox[count($textBox) - 1][1]);
            }
        } while ($textWidth > $width || $textHeight * count($lines) > $height);

        // 计算每个水印块的宽高
        $blockWidth = $textWidth + $spacingDiagonal;
        $blockHeight = $textHeight * count($lines) + $spacingDiagonal;

        // 计算水印块在画布上均匀分布的行列数
        $rows = ceil(($height + $blockHeight) / ($blockHeight + $spacingVertical));
        $cols = ceil(($width + $blockWidth) / ($blockWidth));

        // 确保每个水印块都在画布内
        for ($row = 0; $row < $rows; $row++) {
            for ($col = 0; $col < $cols; $col++) {
                // 计算当前水印块的左上角位置,使其按照倾斜角度排列
                $x = $col * $blockWidth - ($row * $blockHeight * tan(deg2rad($angle)));
                $y = $row * ($blockHeight + $spacingVertical);

                // 计算文字四个角的坐标,确保文字不超出画布边界
                $valid = true;
                foreach ($textBox as $box) {
                    $xPoints = array($box[0] + $x, $box[2] + $x, $box[4] + $x, $box[6] + $x);
                    $yPoints = array($box[1] + $y, $box[3] + $y, $box[5] + $y, $box[7] + $y);
                    foreach ($xPoints as $xp) {
                        if ($xp < 0 || $xp > $width) {
                            $valid = false;
                            break;
                        }
                    }
                    foreach ($yPoints as $yp) {
                        if ($yp < 0 || $yp > $height) {
                            $valid = false;
                            break;
                        }
                    }
                    if (!$valid) break;
                    $y += $textHeight; // 下一行文字的 y 坐标
                }

                // 如果所有点都在画布内,则绘制文字
                if ($valid) {
                    $y = $row * ($blockHeight + $spacingVertical); // 重置 y 坐标
                    foreach ($lines as $line) {
                        imagettftext($image, $fontSize, $angle, $x, $y, $fontColor, $fontPath, $line);
                        $y += $textHeight;
                    }
                }
            }
        }

        // 设置默认保存路径
        if (!$saveFolderPath) {
            $saveFolderPath = public_path() . "/uploads/";
        }

        // 确保保存目录存在
        if (!file_exists($saveFolderPath) && !mkdir($saveFolderPath, 0777, true)) {
            return [false, "水印图片临时保存目录不存在且无法创建", $saveFolderPath];
        }

        // 指定保存图片的路径
        $timestamp = date("YmdHis");
        $fileName = "watermark_{$timestamp}.png";
        $savePath = $saveFolderPath . $fileName;

        // 保存图片为PNG到指定路径
        if (!imagepng($image, $savePath)) {
            return [false, "无法保存水印图片", $savePath];
        }

        // 释放内存
        imagedestroy($image);

        return [true, '', $savePath];
    }


}

效果

效果图

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值