Создание документов в формате PDF средствами .NET

Автор: Сергей Алексеев
Источник: RSDN Magazine #1-2007
Опубликовано: 24.02.2007
Версия текста: 1.0
FOP
nFOP
NfopHelper
Ссылки

Исходный текст класса NfopHelper
Исправленная библиотека nFOP

FOP

Библиотека FOP давно и успешно используется Java-разработчиками для программной генерации отчетов в формате-PDF. FOP расшифровывается как Formated Object Processor и представляет собой компонент, на вход которого подается документ в формате xsl:fo, а на выходе получается документ в одном из поддерживаемых процессором форматов (PDF, PS, TXT, и т. д.). Я не буду углубляться в описание формата xsl:fo, в сети и так немало ресурсов, ему посвященных [см. 3. 4]. Вкратце замечу, что он является подмножеством языка xml и служит для описания как содержимого, так и представления (верстки) документа. Вот кусок документа в формате xsl:fo:

<?xml version="1.0" encoding="utf-8" ?>
<fo:root xmlns:fo="http://www.w3.org/1999/XSL/Format">
  <fo:layout-master-set>
    <fo:simple-page-master master-name="first" 
      page-height="29.7cm" page-width="21cm" 
      margin-top="1cm" margin-bottom="2cm"
      margin-left="2cm" margin-right="2cm">

      <fo:region-body margin-top="3cm" margin-bottom="1.5cm" />
      <fo:region-before extent="3cm" />
      <fo:region-after extent="1.5cm" />
    </fo:simple-page-master>
  </fo:layout-master-set>
  <fo:page-sequence master-reference="first">
    <fo:flow flow-name="xsl-region-body">
      <fo:block font-family="Arial">
                Здравствуй, мир!
      </fo:block>
    </fo:flow>
  </fo:page-sequence>
</fo:root>

Получить документ в формате xsl:fo можно разными способами – сгенерировать его вручную либо, если исходные данные представлены в формате XML, применить к ним xslt-преобразование.

nFOP

Но вернемся к замечательной библиотеке FOP. Как и множество других open source-разработок, она была перенесена на платформу .NET (в данном случае рассматривается проект nFOP). Вот пример использования этой библиотеки (здесь и далее приведены примеры на языке C#):

java.io.FileInputStream inputStream = 
  new java.io.FileInputStream("c:\\document.fo");
org.xml.sax.InputSource inputSource = 
  new org.xml.sax.InputSource(inputStream);
java.io.FileOutputStream outputStream = 
  new java.io.FileOutputStream("c:\\document.pdf");

try
{
  org.apache.fop.apps.Driver driver =
    new org.apache.fop.apps.Driver(inputSource, outputStream);
  driver.setRenderer(1);
  driver.run();
}
finally
{
  outputStream.close();    
}

Во время испытания этой библиотеки я сразу же столкнулся с проблемой при отображении русских букв – какие бы шрифты я не использовал, Acrobat Reader упорно показывал пустые квадраты вместо русских букв.

После непродолжительного поиска в Google стало ясно, что нужно внедрить шрифт в документ, благо FOP поддерживает такую возможность. FOP-то поддерживает, а вот в nFOP эта возможность реализована криво. Попытка программно указать шрифт для внедрения успеха не принесла – во время выполнения метода driver.run() все настройки шрифтов инициализируются заново. Второй способ, посредством конфигурационного файла, тоже с наскоку не прошел.

Файл конфигурации у меня выглядел примерно так:

<configuration>
  <fonts>
    <font metrics-file="C:\Temp\Arial.xml" 
          embed-file="C:\WINDOWS\Fonts\Arial.ttf" kerning="yes">
      <font-triplet name="Arial" style="normal" weight="normal" />
    </font>
    <font metrics-file="C:\Temp\Arialbd.xml" 
          embed-file="C:\WINDOWS\Fonts\Arialbd.ttf" kerning="yes">
      <font-triplet name="Arial" style="normal" weight="bold" />
    </font>
    <font metrics-file="C:\Temp\Verdana.xml" 
          embed-file="C:\WINDOWS\Fonts\Verdana.ttf" kerning="yes">
      <font-triplet name="Verdana" style="normal" weight="normal" />
    </font>
    <font metrics-file="C:\Temp\Verdanab.xml" 
          embed-file="C:\WINDOWS\Fonts\Verdanab.ttf" kerning="yes">
      <font-triplet name="Verdana" style="normal" weight="bold" />
    </font>
</configuration>

Здесь каждый тег font описывает шрифт, который следует внедрить в документ. Атрибут embed-file определяет расположение файла шрифта, а атрибут metrics-file – расположение файла с метриками шрифта. Вложенный тег font-triplet определяет начертание и наименование шрифта, для которого будут использоваться указанные файлы шрифта и метрик. Пример конфигурационного файла также можно найти в дистрибутиве оригинальной библиотеки FOP – это файл userconfig.xml.

Что представляет собой файл метрик, я разбираться не стал, однако выяснил, что его можно получить с помощью того же самого nFOP. Для этого служит класс org.apache.fop.fonts.apps.TTFReader. Но увы, метод этого класса writeFontXML в библиотеке nFOP реализован не был, а именно он отвечает за сохранение файла метрик. Пришлось писать свою реализацию этого метода:

      using java.io;
using org.w3c.dom;
using org.apache.xml.serialize;
using org.apache.fop.fonts.apps;
...

publicstaticvoid CreateFontMetrics(
  string fontPath, string metricsPath, bool cid)
{
  TTFReader ttfReader = new TTFReader();

  TTFFile ttfFile = ttfReader.loadTTF(fontPath, null);

  Document doc =
    ttfReader.constructFontXML(ttfFile, null, null, null, null, cid, null);

  OutputFormat format = new OutputFormat(doc);

  FileWriter writer = new FileWriter(metricsPath); 
  XHTMLSerializer serializer = new XHTMLSerializer(writer, format);

  serializer.asDOMSerializer();
  format.setEncoding("UTF-8");
  format.setOmitXMLDeclaration(false);
  format.setOmitDocumentType(true);

  writer.write("<?xml version='1.0' encoding='UTF-8'?>");
  serializer.serialize(doc.getDocumentElement());
  writer.close();
}  

Итак, конфигурационный файл и файлы метрик готовы, можно попробовать заставить nFOP их использовать. Чтобы заставить nFOP прочитать файл конфигурации, нужно где-нибудь при старте программы выполнить следующий код:

org.apache.fop.configuration.ConfigurationReader reader = 
  new org.apache.fop.configuration.ConfigurationReader(
    new org.xml.sax.InputSource(new java.io.FileInputStream(configPath)));

reader.start();

Однако в момент выполнения метода reader.start() библиотека выбрасывала странное исключение: "Resource Bundle Not Found". Разбор полетов показал, что библиотека пыталась сообщить мне об ошибке в конфигурационном файле, для чего ей потребовалось достать строку с сообщением об ошибке из ресурсов, но ресурсы не были обнаружены. Пришлось взяться за Google, и вскоре решение нашлось – хотя ресурсные файлы и присутствовали среди исходников nFOP (файлы XMLMessages.properties и XMLSchemaMessages.properties в папке xerces-2_0_2\src\org\apache\xerces\impl\msg), но они были в формате, непонятном J#. Нужно было достать утилитку vjsresgen.exe, сконвертировать с ее помощью ресурсные файлы в нужный формат, подключить полученный файл к проекту и перекомпилировать библиотеку, не забыв перед этим заменить в исходниках строки org.apache.xerces.impl.msg.XMLMessages и org.apache.xerces.impl.xs.XMLSchemaMessages на XMLMessages и XMLSchemaMessages соответственно.

После этой операции я попытался вновь прочитать конфигурационный файл и получил внятное сообщение об ошибке – что-то вроде "не найден соответствующий закрывающий тег для тега <fonts>". Ага, причина ошибки выяснена, ошибка – исправлена. Запускаем приложение еще раз и получаем еще одно исключение – "MalformedURLException". Где-то внутри библиотеки (метод buildURL класса org.apache.fop.tools. URLBuilder) живет следующий код – "return new URL(f.toString());", где f – это экземпляр класса File. Наверное, в исходной библиотеке этот код выглядел как "return new URL(f.toURL());", но в J# метод toURL в классе File отсутствует, и при портировании исходников этот метод, видимо, просто был заменен на toString. Пришлось переписать этот метод как "return new URL("file://" + f.toString());". Такая же проблема была обнаружена еще в одном месте – в методе buildBaseURL класса org.apache.fop.configuration. Configuration.

И наконец, после внесения в библиотеку всех этих изменений, она заработала!

NfopHelper

Для более комфортной работы с библиотекой и ее конфигурирования я написал небольшой вспомогательный класс – NfopHelper. С его использованием генерация PDF выглядит следующим образом:

      string srcPath = "c:\\temp\\fopTest";
string fontPath = Tools.GetSpecialFolder(Tools.SpecialFolders.CSIDL_FONTS, false);
string tempPath = Path.GetTempPath();

//Создаем файл конфигурации
XmlDocument xmlConfig = NfopHelper.CreateConfigFile();

//Создаем метрики для используемых шрифтов
NfopHelper.CreateFontMetrics(Path.Combine(fontPath, "Arial.ttf"), 
  Path.Combine(tempPath, "Arial.xml"), true);
//Добавляем информацию о шрифте в файл конфигурации 
NfopHelper.AddFontToConfig(xmlConfig, Path.Combine(fontPath, "Arial.ttf"), 
  Path.Combine(tempPath, "Arial.xml"), "Arial", "normal", "normal");

NfopHelper.CreateFontMetrics(Path.Combine(fontPath, "Arialbd.ttf"), 
  Path.Combine(tempPath, "Arialbd.xml"), true);
NfopHelper.AddFontToConfig(xmlConfig, Path.Combine(fontPath, "Arialbd.ttf"), 
  Path.Combine(tempPath, "Arialbd.xml"), "Arial", "normal", "bold");

//Сохраняем конфигурационный файл
xmlConfig.Save(Path.Combine(tempPath, "userconfig.xml"));

//Запускаем процесс генерации PDF документа
NfopHelper.CreatePdf(Path.Combine(srcPath, "report.fo"), 
  Path.Combine(tempPath, "userconfig.xml"), Path.Combine(srcPath, "report.pdf"));

NB. Здесь файлы метрик и конфигурационный файл создаются во временном каталоге, и после окончания работы программы их желательно удалить.

Ссылки

  1. http://xmlgraphics.apache.org/fop/ - страница проекта FOP
  2. http://sourceforge.net/projects/nfop/ - страница проекта nFOP
  3. http://en.wikipedia.org/wiki/XSL_Formatting_Objects - страница википедии, посвященная xsl:fo, содержит ссылки на стандарты w3c
  4. http://www.renderx.com/tutorial.html - хороший учебник по xsl:fo


Эта статья опубликована в журнале RSDN Magazine #1-2007. Информацию о журнале можно найти здесь