感觉phpword不够用的时候怎么办,例如生成目录没有页码、转换pdf格式错乱 | php 技术论坛-大发黄金版app下载
#
最近又遇到了两个需求发觉phpword已经实现起来有些困难了,我们有一项业务是每天将资讯信息从数据库中查找出来,生成一份word格式的日报文档,以附件形式发送到订阅的客户邮箱中;但是最近业务部门提了两个需求,让开发的同事犯了难,来找我帮忙。
需求一:在日报的的开头生成一份目录,用户点击可以直接跳转
需求二:日报附件的word改成pdf格式推送
这两个需求起初一看都很简单,但是实际都不太好实现。首先需求一,也就是我们在word中很常见的目录是这样的,标题部分没问题,但是红框标注的页面部分是难点:
首先这个在phpword中的示例代码中是这样实现的:
// 其他代码 略去
$phpword->getsettings()->setupdatefields(true);
// 其他代码 略去
$toc = $section->addtoc($fontstyle12);
这里面核心就这两句,首先是addtoc
方法,toc即“table of contents”,这个方法的参数很少,基本都是和样式相关的,如果直接调用这个方法,得到的目录打开后,会发现只有目录标题而没有页码的。所以这里面的另一行代码其实更关键:$phpword->getsettings()->setupdatefields(true);
,这行的功能就是,生成完word文件之后,用户首次用word程序打开会看到一个提示弹窗:
这时候用户需要选择 是,然后目录会更新,页码就能正常显示出来了,效果就如同word程序里面的【引用】【更新目录】。但是这样体验就很差, 这个弹窗默认选中的是否,极易忽略,如果不点是就没有页码。其次经过测试这个弹窗只在office word中会正常弹出,在国内是用广泛的wps上就没有了,自然也就没有页码。做这类文档生成常常需要兼容wps和office word,就像前端页面兼容不同的浏览器一样,两个软件经常会有一些微妙的区别。
所以这个时候,需要重新回想一下phpword的原理,为何会导致phpword只能生成目录标题,生成页码会这么难呢。phpword生成word的核心原理其实就是写ooxml,按照ooxml的语法生成xml而已,最终打包的文件实际是xml文件。所以我们假如打开源码中的writer目录,就能看到好多操作xml的代码,例如src/phpword/writer/word2007/element/toc.php:
// more code
class toc extends abstractelement
{
/**
* write element.
*/
public function write(): void
{
$xmlwriter = $this->getxmlwriter();
$element = $this->getelement();
if (!$element instanceof tocelement) {
return;
}
$titles = $element->gettitles();
$writefieldmark = true;
foreach ($titles as $title) {
$this->writetitle($xmlwriter, $element, $title, $writefieldmark);
if ($writefieldmark) {
$writefieldmark = false;
}
}
$xmlwriter->startelement('w:p');
$xmlwriter->startelement('w:r');
$xmlwriter->startelement('w:fldchar');
$xmlwriter->writeattribute('w:fldchartype', 'end');
$xmlwriter->endelement();
$xmlwriter->endelement();
$xmlwriter->endelement();
}
/**
* write title.
*/
private function writetitle(xmlwriter $xmlwriter, tocelement $element, title $title, bool $writefieldmark): void
{
// 略去
}
/**
* write style.
*/
private function writestyle(xmlwriter $xmlwriter, tocelement $element, int $indent): void
{
// 略去
}
/**
* write toc field.
*/
private function writefieldmark(xmlwriter $xmlwriter, tocelement $element): void
{
// 略去
}
}
仔细看也会会发现源码中有生成标题有生成样式的,根本就没有写入页码的部分。
phpword生成ooxml,再经由word渲染就成了word文档了,这个过程跟前端写入html在浏览器渲染是很像的。其实问题就是在于生成xml的时候,是一种流水账一样的写入方式,程序是无法判断文档的位置也就是页码的,也就很难得到这个页码,所以核心问题在于phpword无法渲染。众所周知渲染程序往往是最难写的,例如像css解释器等。能渲染word文档的只有word或wps,所以如果我们能像puppeteer调动浏览器一样调用word程序的话问题就简单多了。
这时候我们使用php com组件,注意这个ddl支持windows平台。官方介绍:
com 是
component object model
的缩写;它是 dce rpc(公开标准)之上的面向对象层(和相关服务),定义了通用的调用转换,任一语言编写的代码都可以与另外的任一语言(前提是这些语言可以 com 感知)编写的代码进行互相调用与交互。代码不仅可以用任何语言编写,并且不需要是同一个执行文件的一部分;代码可以从 dll 载入,或者从相同机器的另外一个进程中找到,或者使用 dcom(分布式 com),或者从远程机器的另外一个进程中找到,所有的这些都不要代码知道组件在哪里。有个 com 子集叫做 ole 自动化,包含一组允许松散绑定 com 对象的 com 接口,因此可以在运行是对其自省(introspected)和调用,而无需了解编译时这些对象的工作原理。php com 扩展利用 ole 自动化接口,允许从脚本中创建和调用兼容对象。从技术上,这应该称为“
ole automation extension for php
”(php ole 自动化扩展),因为并非所有的 com 对象用于 ole 兼容。现在,为什么以及何时应该使用 com?com 是在 windows 平台上将组件和应用结合在一起的主要方法之一;使用 com 可以启动 microsoft word,填充文档模板并将结果保存为 word 文档,然后将其发送给网站的访客。可以使用 com 为网络执行管理任务和配置 iis;这些只是最常见的用途;还可以使用 com 做更多的事情。
此外,支持使用 microsoft 提供的 com 互操作层来实例化和创建 .net 程序集。
这个是相关的api文档
这个文档非常的长,其实我们需要重点关注的是
所以关于需求一,生成动态目录,实现的代码大概是这样的:
$word = new com("word.application") or die("unable to instantiate word");
$word->visible = false;
// 创建新文档
$document = $word->documents->add();
// 创建新文档
$document = $word->documents->add();
// 插入目录
$word->selection->homekey();
$word->selection->typeparagraph();
$document->tablesofcontents->add(
$word->selection->range,
true,
1,
3
);
// 插入第一章并设置为 heading 1 样式
$word->selection->typetext("chapter 1: introduction");
$word->selection->style=-2; // 设置样式为heading 1
$word->selection->typeparagraph();
$word->selection->typetext("this is the introduction...");
$word->selection->typeparagraph();
// 插入第一章并设置为 heading 1 样式
$word->selection->typetext("chapter 2: introduction");
$word->selection->style=-2; // 设置样式为heading 1
$word->selection->typeparagraph();
$word->selection->typetext("this is the introduction...");
$word->selection->typeparagraph();
// 更新目录以确保是最新的
$document->tablesofcontents[1]->update();
// 保存文档
$savepath ="c:\\users\\win10\\dev\\code\\document_with_toc.docx";
$document->saveas2($savepath);
// 关闭word应用
$word->quit();
$word = null;
简单理解这份代码,整体的api设计跟phpword很像的也是流水账一样写入。这里可以将word想象成浏览器,就跟puppeteer调动浏览器的代码差不多,首先是第一行就是启动word应用,然后打开不同的文档,就像浏览器打开不同的标签页。其中$word->visible = false; 就是是否需要可视化,如果是true的话可以看到word程序被打开写入内容的整个过程。
有几点需要注意:
1.$word->selection->style=-2; 意思是将这句话设置为标题1,其实就是typetext()方法是写入的是纯文字,不包含任何样式的,回想phpwrod中的addtext其实也是这个类似的api设计。这里写成-2 是对应word内嵌样式的的枚举值,这个枚举值经测试只能-2才有效,直接“heading 2”不生效,具体原因还不清楚。
文档链接
2 $document->tablesofcontents[1]->update();这个就是更新目录,效果与$phpword->getsettings()->setupdatefields(true);是一样的,但是这个必须最后执行,因为内容全写完成之后再计算页码比较合理。这样最终生成的word文档,用户打开页码就都是计算好的了,也就没有弹窗了,因为我们自己渲染过了。
需求二,将word保存成pdf:
// 创建 com 对象
$word = new \com("word.application") or die("无法创建 word 对象");
// 设置 word 应用程序不可见
$word->visible = false;
// 打开 word 文档
$worddoc = $word->documents->open($path);
// 生成 pdf 文件路径
$bare_name = str_replace(".docx", "", $file_name);
$pdfpath = "./out_files/" . $bare_name . '.pdf';
// 将 word 文档保存为 pdf 格式
$worddoc->saveas2(__dir__ . $pdfpath, new \variant(17, vt_i4)); // 17 表示 pdf 格式
// 关闭 word 文档
$worddoc->close(false);
unset($worddoc); // 释放文档对象
// 关闭 word 应用程序
$word->quit();
unset($word); // 释放 word 应用程序对象
转换pdf的核心也是渲染,phpword中原生的转pdf的思路就是通过mpdf或者dompdf等渲染引擎得到pdf。但是效果都不太好,如果是简单的pdf还可以,但是像我们这种样式复杂的生成的pdf往往样式有些错乱,而且还往往需要再安装补充一些中文字体。而使用这种方式,得到的pdf和word样式是高度一致的,几乎一样。
这里面需要注意的saveas2方法,$worddoc->saveas2(dir . $pdfpath, new \variant(17, vt_i4));,第二个参数就是文件格式,它支持一下几种,pdf的枚举值是 17,但是直接写17会报错,需要转换一下 。参见
本作品采用《cc 协议》,转载必须注明作者和本文链接
com 非常不稳定 一般用 libreoffice 等支持 linux 的用 cli 处理。或者用 wps 等云处理接口
建议直接对接成熟的第三方接口服务去处理,例如wps。
不知道这个方案是否可行,提供一个思路。 搭建一个内部的在线文档平台,有那种开源的在线word或者pdf的工具。然后每天生成内容后给客户邮件里推送查看的url