PHP在PDF指定位置添加数字签名/图片
基本业务需求:按指定范本填充生成word文档,word转pdf后,在指定字符位置添加数字签名。
难点:获取pdf需要插入数字签名的坐标
使用到的第三方依赖:phpoffice/phpword
tecnickcom/tcpdf
setasign/fpdi
smalot/pdfparser
代码示例:
use setasign\Fpdi\Tcpdf\Fpdi;
use Smalot\PdfParser\Parser;
/**
* 执行签署
* @param array $document
* @param array $orderInfo
* @param array $userInfo
* @param array $notarial
* @param string $tagStr 多个使用$1、$2... 一个pdf中只能签署一次
* @return bool
*/
public function doSign($document=[], $orderInfo=[], $userInfo=[], $notarial=[], $width=38, $height=15, $tagStr = "$", $snap=[], $platform='self', $verifyType=0, $signType=2)
{
$pdfPath = public_path().str_replace('docx', 'pdf', $document['url']);
//获取签名位置 暂定模版设置白色$字符,如多个则使用$1、$2...
$parser = new Parser();
$pdf = $parser->parseFile($pdfPath);
$pages = $pdf->getPages();
//考虑最后一页可能跨页导致无法签署
/*$signPage = count($pages) - 1;
$lastPageText = $pages[$signPage]->getDataTm();*/
//获取源文档页面宽高 暂不考虑横版
$mediaBox = [];
foreach ($pages as $page) {
$details = $page->getDetails();
// If Mediabox is not set in details of current $page instance, get details from the header instead
if (!isset($details['MediaBox'])) {
$pages = $pdf->getObjectsByType('Pages');
$details = reset($pages)->getHeader()->getDetails();
}
$mediaBox[] = [
'width' => $details['MediaBox'][2],
'height' => $details['MediaBox'][3]
];
}
//获取页面宽高
$sourceW = $mediaBox[0]['width']; //595.3
$sourceH = $mediaBox[0]['height']; // 841.89
//创建fpdi实例
$pdf = new Fpdi();
$pageCount = $pdf->setSourceFile($pdfPath);
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
$pdfW = $pdf->getPageWidth(); //210
$pdfH = $pdf->getPageHeight(); //297
//遍历全部页面中的文字
$positions = [];
for ($i=0; $i<count($pages); $i++){
$texts = $pages[$i]->getDataTm();
foreach ($texts as $text){
if(str_contains($text[1], $tagStr)){
$positions[] = [
'x' => $text[0][4],
'y' => $text[0][5],
'real_x' => (($text[0][4] / $sourceW) * $pdfW) + 6.36, //按源文件、输出文件比例、页边距等处理
'real_y' => ((($sourceH-$text[0][5]) / $sourceH) * $pdfH) - 5.08
];
}
}
}
//临时测试使用
/*if(!$positions){
$positions[0] = [
'x' => 10,
'y' => 10,
'real_x' => 10,
'real_y' => 10
];
}*/
//创建页面
for ($pageNumber=1; $pageNumber <= $pageCount; $pageNumber++){
$templateId = $pdf->importPage($pageNumber);
$pdf->AddPage();
$pdf->useTemplate($templateId, 0, 0, $pdfW, $pdfH);
}
//获取证书路径
if($notarial){
$certInfo = DigitalCertificate::where(['port'=>3, 'mtable'=>'notarial'])->order('id desc')->find();
$signature = [
'id' => 0,
'path'=>$notarial['signature']
];
}else{
$certInfo = DigitalCertificate::where(['port'=>$userInfo['port'], 'port_id'=>$userInfo['id']])->whereIn('mtable', ['admin', 'users'])->order('id desc')->find();
//获取用户签名图
if($userInfo['port'] == 4){
$signature = UserSignature::where(['port'=>$userInfo['port'], 'port_id'=>$userInfo['id'], 'order_id'=>$orderInfo['id']])->order('id desc')->find();
}elseif($userInfo['port'] == 2 || $userInfo['port'] == 3){
$signature = [
'id'=>0,
'path'=>$userInfo['signature']
];
}else{
$signature = [
'id' => 0,
'path'=>$userInfo['signature']
];
}
if(!$signature['path']){
throw new AdminException('用户未上传签名');
}
}
//todo 判断证书是否存在或过期
$certificate = 'file://'.root_path().$certInfo['crt_path'];
$privateKey = 'file://'.root_path().$certInfo['key_path'];
$certificateImage = public_path().substr($signature['path'], 1);
//设置证书信息
$info = [
'Name' => 'xx系统',
'Location' => '山东',
'Reason' => '禁止修改文档',
'ContactInfo' => 'r1989.com'
];
//$pdf->setTimeStamp("http://cn.pool.ntp.org", "ntp.org");
$pdf->setSignature($certificate, $privateKey, '', '', 2, $info);
// 设置签名外观
// 创建签名内容(图像和/或文本) 一个pdf文档只能添加一个数字签名
$pdf->Image($certificateImage, $positions[0]['real_x'], $positions[0]['real_y'], $width, $height, 'PNG');
// 定义签名外观的活动区域
$pdf->setSignatureAppearance($positions[0]['real_x'], $positions[0]['real_y'], $width, $height);
// 设置空签名外观
//$pdf->addEmptySignatureAppearance(180, 90, 15, 15);
// 四种模式 I输出、D下载、F保存本地、S输出二进制字符串
$signedUrl = dirname($document['url']).'/'.pathinfo($document['name_origin'], PATHINFO_FILENAME).'_'.md5($document['id'].'_'.$orderInfo['id']).'.pdf';
$pdf->Output(public_path().substr($signedUrl, 1), 'F');
//更新主表信息
$documentData = ['signature_id'=>$signature['id'], 'signed_url'=>$signedUrl, 'sign_time'=>get_date(), 'verify_type'=>$verifyType, 'sign_type'=>$signType, 'sign_platform'=>$platform];
if($snap){
$documentData['sign_snap'] = json_encode($snap, 320);
}
//更新签名版对应的hash
$documentData['signed_hash'] = hash_file('sha256', public_path().substr($signedUrl, 1));
$res = NotarialDocument::edit($documentData, $document['id'], 'id');
return $res;
}