感觉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 协议》,转载必须注明作者和本文链接
讨论数量: 9

com 非常不稳定 一般用 libreoffice 等支持 linux 的用 cli 处理。或者用 wps 等云处理接口

1个月前
(楼主) 1个月前
(作者) 1个月前
(作者) 1个月前
(楼主) 1个月前

建议直接对接成熟的第三方接口服务去处理,例如wps。

libreoffice基本功能可以,但有些样式和颜色pdf转出来有一点问题。

1个月前
(楼主) 1个月前
lovewei (作者) 1个月前

不知道这个方案是否可行,提供一个思路。 搭建一个内部的在线文档平台,有那种开源的在线word或者pdf的工具。然后每天生成内容后给客户邮件里推送查看的url

1周前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
网站地图