使用 LaTeX 和 Perl 构建 Impress 和 PowerPoint 幻灯片
让我们从一个故事开始。事情是这样的:我的第二本书,与 Michael Moorhouse 博士合著,终于完成了。我为此额外花了六个月的时间,这意味着它现在至少晚了六个月。我花了每一分钟的空闲时间进行排版、校对、写作,手动将 Michael 的 Microsoft Word 文件转换为 LaTeX,阅读,然后再重新阅读。然后,我又校对了所有内容。当一切尘埃落定时,我感到疲惫不堪。不久之后,我收到了封面的最终校样。封底上印着——承诺在网站上提供 Microsoft PowerPoint 幻灯片,以配合本书使用。要更改封面已经太晚了,这意味着我不得不以某种方式提供幻灯片。我忘记了我们在项目开始时,也就是 18 个多月前,就决定这样做了。
十八个月前,PowerPoint 是学术界事实上的标准幻灯片制作技术。今天,PDF 也非常流行。与 Linux 社区中的许多人一样,我已经转向 OpenOffice.org,放弃了 PowerPoint。这本书有 20 章,我估计手动制作幻灯片至少需要 20 天的工作量。想到要用 PowerPoint 做这项工作,我感到很不情愿。我当然可以在 OpenOffice.org Impress 中工作,然后在完成后导出到 PowerPoint,但这个想法也让我感到不舒服。基本问题是我知道所有内容已经在 LaTeX 文件中了,而不得不使用幻灯片制作应用程序重新制作这些内容,让我感到比已经疲惫不堪还要更加疲惫。如果我能找到一种方法,以编程方式从我的 LaTeX 文件中提取内容,并用它填充 PowerPoint 幻灯片——那将大大改善情况。
在 Google 上搜索结果令人沮丧。也许不足为奇,PowerPoint 文件格式的详细信息很难找到。我确实在 Microsoft Windows 帮助格式中找到一个文件,其中描述了 Microsoft Office 文档的 XML 标准,PowerPoint 文档可以导出为这种格式。不幸的是,这是一篇篇幅巨大、内容复杂的文章。在确定我无法在 Google 上取得任何进展后,我转到 Comprehensive Perl Archive Network (CPAN)。Perl,我选择的编程语言,已经与所有类型的文件格式和其他计算形式连接起来。如果有人使用 Perl 和 PowerPoint,那么这项工作的详细信息将在 CPAN 上提供。不幸的是,这次搜索也一无所获。
然后我想到了:如果我可以使用开放且广泛发布的 OpenOffice.org Impress 文档格式,那么我可以将我的 Impress 幻灯片导出到 PowerPoint 作为最后一步。快速浏览 OpenOffice.org 网站,我找到了 OpenOffice.org 文件格式的官方 XML 描述。这个标准超过 600 页,比我的书还厚!
XML 文档写得很好,但内容非常繁重。我回到 CPAN 查看是否有其他程序员花时间使用 OpenOffice.org 格式,并且慷慨地将他们的工作上传到 CPAN。这次我没有失望。Genicorp 的 Jean-Marie Gouarne 最近发布了 OpenOffice::OODoc 模块,这是一个 OpenOffice.org 格式的 Perl 接口。给定一个现有文档,OpenOffice::OODoc 可以操作内容,根据需要添加、删除和更新磁盘文件。
我从一个简单的过滤器开始,用 Perl 编写,它以 LaTeX 文件作为输入,并以自定义文本形式输出幻灯片内容。通过生成文本文件,我确保可以使用任何文本编辑器编辑过滤器的输出,根据需要微调文本内容。一旦对文本内容感到满意,另一个过滤器(也是用 Perl 编写的)将使用文本内容创建 Impress 演示文稿。然后可以在 Impress 中打开 Impress 演示文稿,并导出为 PowerPoint 和/或 PDF 格式。
我自觉地努力使我的演示文稿尽可能简单,并决定只使用三种幻灯片类型。title_slide 将包含演示文稿文件开头的章节标题。在演示文稿中,title_slide 将兼作与章节相关的任何图形图像的占位符,每个图形图像创建一个 title_slide。bullet_slide 将包含节标题作为其幻灯片标题,子节标题作为项目符号。最后,sourcecode_slide 将提供一个等宽、逐字记录的幻灯片,用于程序列表。
我使用 Impress 手动创建了一个三张幻灯片的演示文稿,我称之为 blank.sxi。创建的每张幻灯片都对应于上一段中描述的三种幻灯片类型中的每一种。我计划每次以编程方式为我的每个章节创建演示文稿时,都克隆此演示文稿。通过克隆,我将确保所有演示文稿都符合标准化的外观。
getcontent 脚本是 Perl 程序员通常创建、使用然后丢弃的那种脚本。(请参阅在线资源以下载本文中提到的文件。)它循环处理标准输入,一次读取一行,并尝试对感兴趣的内容进行模式匹配。如果发生匹配,则会生成相应的输出。作为 getcontent 功能的一个示例,以下是处理 LaTeX 文件中章节标题的代码
if ( /\\chapter\{(.*)\}/ ) { print "CHAPTERTITLE: $1\n"; next; }
一个简单的正则表达式尝试匹配 LaTeX 章节宏;如果找到匹配项,则提取章节标题并生成输出。当找到匹配项时,对 next 的调用会短路循环,允许从标准输入中读取下一行。通过这种方式,以下 LaTeX 代码片段
\chapter{Working with Regular Expressions}
也就是说,LaTeX 标记被删除,并替换为更简单的标记。节和子节 LaTeX 宏的处理方式类似。以下是代码
if ( /\\section\{(.*)\}/ ) { print "BULLETTITLE: $1\n"; next; } if ( /\\subsection\{(.*)\}/ ) { print "BULLETCONTENT: $1\n"; next; }
处理源代码列表稍微复杂一些,因为需要识别何时输入和退出逐字文本块。以下是处理进入 LaTeX 逐字块的代码
if ( /\\begin\{verbatim\}/ ) { print "STARTCODE\n"; $in_verbatim = TRUE; next; }
以下是用于处理退出逐字块的代码
if ( $in_verbatim ) { if ( /\\end\{verbatim\}/ ) { print "STOPCODE\n"; $in_verbatim = FALSE; } else { print; } next; }
一个简单的布尔值 $in_verbatim 标量有助于确定脚本当前是否在逐字块中工作。类似的代码提取了整本书章节中出现的格言,一些 if 块处理了图形、它们的标题和其他感兴趣的内容。例如,考虑以下 LaTeX 标记块
\chapter{The Basics} \textit{Getting started with Perl.} \section{Let's Get Started!} There is no substitute for practical experience when first learning how to program. So, here is the first Perl program \index{welcome@\texttt{welcome}, and the first program, called \texttt{welcome}: \begin{verbatim} print "Welcome to the World of Perl!\n"; \end{verbatim} \noindent When executed by \texttt{perl} \footnote{We will learn how to do this is in just a moment.}, this small program displays the following, perhaps rather not unexpected, message on screen: \begin{verbatim} Welcome to the World of Perl! \end{verbatim}
getcontent 脚本将上面的 LaTeX 转换为此文本内容
CHAPTERTITLE: The Basics CHAPTERCONTENT: Getting started with Perl. BULLETTITLE: Let's Get Started! STARTCODE print "Welcome to the World of Perl!\n"; STOPCODE STARTCODE Welcome to the World of Perl! STOPCODE
请注意,所有 LaTeX 标记都消失了,取而代之的是更简单的标记语言,该语言将用于以编程方式生成幻灯片。假设 LaTeX 代码块位于名为 chapter3.tex 的文件中,则 getcontent 脚本的执行方式如下,将转换结果通过管道传输到适当命名的文件中
perl getcontent chapter3.tex > chapter3.input
chapter3.input 文件现在包含文本内容,并且可以在生成幻灯片之前使用任何文本编辑器对其进行微调。
在 Impress 文档中生成幻灯片的过程因许多因素而变得复杂。首先,OpenOffice::OODoc 模块不能用于创建新的 OpenOffice.org 文件;它只能操作现有文件。此外,创建该模块的目的是主要处理 OpenOffice.org Writer 文件(文字处理器文档),而不是 Impress 演示文稿。举例来说,这是一个名为 appendpara 的简短程序,它将一些文本添加到已有的 Writer 文档中
#! /usr/bin/perl -w use strict; use OpenOffice::OODoc; my $document = ooDocument( file => 'blank.sxw' ); $document->appendParagraph ( text => 'Some new text', style => 'Text body' ); $document->save;
这个小程序使用 OpenOffice::OODoc 模块,并从现有的 Writer 文件创建一个文档对象。然后,该程序调用 appendParagraph 方法添加一些文本,然后再调用 save 方法将更改后的文档提交到磁盘。
除了 appendParagraph 方法外,OpenOffice::OODoc 模块还提供了 insertElement 方法,该方法允许将指定类型的新页面添加到文档中。页面可以是现有页面的克隆,也可以是实际的原始 XML。
在阅读了 600 多页的 OpenOffice.org XML 文件格式文档的第 6 页后,我发现 Impress 使用 //draw:page XML 类型来表示演示文稿中的幻灯片。不幸的是,OpenOffice::OODoc 模块无法直接处理这种类型的对象,所以我不得不提出一些其他机制来操作数据。具体来说,我想获取 blank.sxi 文档中包含的空白模板幻灯片,并在我需要时克隆每张幻灯片,用 getcontent 脚本生成的文本内容填充幻灯片的内容。为此,我需要更多地了解 Impress XML 格式。
我有两个选择:继续阅读 600 多页的标准文档,或者查看实际文件,看看我是否可以学到足够的东西来完成这项工作。我选择了后者。回忆起以前的 Linux Journal 文章中提到 OpenOffice.org 使用流行的 ZIP 算法压缩其多部分文件,我创建了一个临时目录并解压缩了 blank.sxi 文件
mkdir unzipped cd unzipped unzip ../blank.sxi
这产生了一堆文件和目录
content.xml META-INF meta.xml mimetype settings.xml styles.xml
最令人感兴趣的是 content.xml 文件,其中包含构成文档的实际内容。在屏幕上或编辑器中查看此文件会产生大量难以辨认的 XML。为了使各个部分尽可能小,没有以任何有意义的方式注意格式化 XML,在压缩容器的任何部分中都没有。通常,XML 被转储/存储为非缩进、非空格文本流。为了尝试理解它,我需要能够以清晰的方式打印 XML。在我认为只能用一时灵感来形容的时刻,我进入命令行并输入xml然后按了两次 Tab 键。屏幕上出现了一个以字母 xml 开头的预安装工具列表
xml2-config xml-config xmllint xmlto xml2man xml-i18n-toolize xmlproc_parse xmlwf xml2pot xmlif xmlproc_val xmlcatalog xmlizer xmltex
xmllint 工具立即引起了我的注意。阅读其手册页,我发现了 --format 选项,是的,你猜对了——它漂亮地打印了提供给该工具的 XML。因此,输入xmllint --format content.xml产生了我可以管道传输到 less 并实际阅读而不会发疯的输出。以下是漂亮打印的 content.xml 的节选片段,显示了来自 blank.sxi Impress 文档的 title_slide 的 XML
<draw:page draw:name="page1" draw:style- ... <draw:text-box presentation:style-name= ... <text:p text:style-name="P1"> <text:span text:style-name="T1"> ChapterTitleSlide </text:span> </text:p> </draw:text-box> <draw:text-box presentation:style-name= ... <text:p text:style-name="P3"> <text:span text:style-name="T2"> ChapterTitleSlideText </text:span> </text:p> </draw:text-box> <presentation:notes> <draw:page-thumbnail draw:style-name= ... <draw:text-box presentation:style-name ... </presentation:notes> </draw:page>
请注意 ChapterTitleSlide 和 ChapterTitleSlideText 内容,这是我在使用 Impress 创建 blank.sxi 时键入的内容。如果我可以使用 insertElement 方法添加基于此摘录的原始 XML,并将空内容替换为我的文本内容,我就成功了。
举例来说,考虑一下演示文稿的标题及其副标题被 produce_slides 处理后会发生什么。insertElement 方法的调用方式如下,创建一个新的幻灯片
$presentation->insertElement( '//draw:page', $last_slide++, title_slide( $title_title, $title_content ), position => 'after' );
title_slide 子例程返回原始 XML,该 XML 被插入到文档中。
给定一个符合 getcontent 生成的文本内容的文件,produce_slides 脚本会克隆 blank.sxi Impress 文件,并以编程方式填充任意数量的幻灯片,从而生成演示文稿。该脚本在结构上与 getcontent 非常相似,唯一的缺点是逐字包含 blank.sxi 中包含的三种幻灯片类型中每一种所需的 XML。要创建演示文稿,请按如下方式调用 produce_slides
perl produce_slides 3 chapter3.input
这会在磁盘上生成一个新的名为 chapter3.sxi 的 Impress 文档。
创建 Impress 文件后,我需要用实际图像替换我的图形图像占位符。getcontent 脚本提取了图像文件名,但没有提取实际图像。将图像导入 Impress 应该很简单,但与最终出现在书中的图像相比,我拥有的原始图像质量非常差。最终图像在出版商的最终排版阶段得到了极大的改进。而且,当然,我没有最终的图像文件。
然后我记得出版商发送了最终校样 PDF,其中包含了所有高质量的图形图像。我使用 xpdf 以 200% 的比例查看校样,然后启动 GIMP 来屏幕截图 xpdf 显示窗口。然后我剪切出图形图像并将其保存为 JPEG。这花了一点时间,但完成后,我获得了一组精美的书籍质量的图像,可以导入到我的 Impress 演示文稿中。完成此任务后,我将 Impress 文档导出为 PowerPoint 格式,工作就完成了。我最初估计的 20 天工作量减少到大约 20 小时的实际工作。
当然,现在,如果我需要快速制作一些幻灯片,我可以在 vi 中手动创建我的文本内容,通过 produce_slides 脚本运行它,我就完成了。
最初看起来似乎不可能完成的任务——以编程方式制作 PowerPoint 演示文稿——结果证明是完全可能的,这要归功于开源。我需要的所有工具都与我的标准 Red Hat 9 发行版一起开箱即用:vi、unzip、Perl、xmllint、xpdf、GIMP 和 OpenOffice.org 套件。
本文的资源: /article/8055。
Paul Barry (paul.barry@itcarlow.ie) 在爱尔兰卡洛理工学院任教。有关他教授的课程以及他撰写的书籍和文章的信息,可以在他的网站 glasnost.itcarlow.ie/~barryp 上找到。