使用 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];
}
}