уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

3D Humans Hello WorldВ прошлом посте я вкратце описал проблему с которой столкнулся при работе с Lazarus’ом в Ubuntu — отсутствие готовых решений по работе с OpenOffice из своих программ, написанных в Lazarus. В Windows можно было бы немного покопаться с OLE и сообразить что-нибудь более-менее подходящее к конкретной ситуации. В Linux, к сожалению, работа с OLE по определению невозможна.

Поэтому сегодня я решил более детально разобраться с форматом OpenOffice Document и разработать небольшой модуль для формирования документов OpeOffice Writer без использования каких-либо дополнительных средств — только работа с XML.

Естественно, что разработать в короткий срок подобный модуль для полноценной работы с OpenOffice невозможно в принципе, но мне это и не нужно. Начиная работу над модулем, я преследую одну единственную цель — разработать модуль, подходящий для решения поставленной передо мной задачи. А задача следующая: модуль должен «уметь» формировать документ содержащий текст, формулы и таблицы. Вполне возможно, что кому-либо из вас, уважаемые читатели, пригодится эта информация, поэтому делюсь ею с Вами.

Сегодня рассмотрим самое простое — работу с текстом и, ради соблюдения негласных правил, научимся писать «Hello World!». Причём сделаем надпись различными шрифтами и стилями.

Проект находится на sourceforge.net. Все последние исправления в модуле именно там

1. Как всё это устроено?

Частично я уже затрагивал вопрос о том, что и где располагается в документе OpenOffice в этом посте. Теперь разберемся с содержимым документа применительно к нашей задаче.

Итак, открываем OpenOffice Writer, создаем пустой текстовый документ и сохраняем его в любую папку. Теперь меняем расширение файла с odt на zip и смотрим, что у нас имеется в архиве:

open_document

Думаю, что названия файлов и папок достаточно «говорящие», чтобы понять для чего они предназначены. Чтобы не углубляться слишком далеко в формат OpenOffice Document, приведу краткое описание тех из файлов, которые мы сегодня будем использовать.
content.xml — содержит текст документа, таблицы, ссылки на рисунки и т.д.
styles.xml — содержит стили документа, список шрифтов документа и т.д.
meta.xml — мета-информация по документу, такая как автор документа, генератор документа, дата создания, статистика документа и т.д.
META-INF/manifest.xml — содержит описание всего документа, что и где располагается и т.д.
Забегая немного вперед, могу сказать, что если для Вас не имеет значения мета-информация по документу, то meta.xml можно не включать в архив документа. Также, если Вы не планируете использовать в документе стили форматирования типа «Заголовок 1«, «Заголовок 2» и т.д., а также табличные данные, то, в принципе, можно не использовать и styles.xml, но — это на ваше усмотрение, так сказать на Ваш страх и риск.
Теперь, Попробуем определиться с тем как вообще происходит обработка текстовой информации. Самый простой способ, не открывая документации по формату — это написать что-либо в документе и посмотреть как наша надпись будет выглядеть изнутри.
Открываем OpenOffice и пишем «Hello World». На следующей строке тоже самое, но другим шрифтом или другим размером шрифта и т.д.
Снова распаковываем архив и смотрим содержимое content.xml.
Вот как теперь стал выглядеть файл content.xml:

content

Попробуем разобраться, что тут к чему.

Содержимое content.xml

В секции office:font-face-decls описываются все используемые в документе шрифты. Для отдельно взятого шрифта необходимо указать его название (атрибут style:name ) и к какому семейству шрифтов он относится (атрибут svg:font-family). Далее пока не углубляемся. Двигаемся дальше.
Секция office:automatic-styles содержит автоматические стили форматирования. Здесь рассмотрим содержимое по-подробнее.
Узел style:style содержит основную информацию такую как:
Название нового стиля (style:name)
Семейство стилей к которому относится новый стили (style:family). В нашем случае семейство paragraph, т.е. стиль относится только к тексту абзацев, если необходимо применить стиль к строке таблицы, то соответственно атрибут style:family будет иметь значение table-row и т.д.
Родительский стиль (style:parent-style-name). Родительские стили описываются в style.xml. Для того, чтобы написать простой текстовый файл без заголовков, достаточно иметь в styles.xml только описание стиля Standard. На этом обязательная информация по новому стилю заканчивается. Каждый дочерний узел для style:style будет содержать дополнительные сведения по форматированию абзаца.
Например, узел style:paragraph-properties содержит информацию по расположению текста на странице (по центру, по левому краю и т.д.).
Узел style:text-properties содержит информацию по шрифту, которым будет написан абзац (гарнитура, размер и т.д.). Например, запись вида:

style:text-properties style:font-name="DejaVu Serif" fo:font-size="14pt" fo:font-weight="bold" style:font-size-asian="14pt" style:font-weight-asian="bold" style:font-size-complex="14pt" style:font-weight-complex="bold"

говорит о том, что используется шрифт DejaVu Serif, размер шрифта 14пт., полужирный.
Секция office:body содержит само тело документа. При этом отдельно взятый абзац текста записывается следующим образом:

text:p text:style-name="Standard">Hello World

Т.е. необходимо просто указывать имя стиля, который будет использован для абзаца.

Содержимое styles.xml

Сильно упрощая, можно сказать, что styles.xml содержит более детальное описание, применяемых в документе стилей, а точнее, описание семейств стилей на основании которых формируются стили в content.xml.
Например, при описании стиля в content.xml мы указывали семейство paragraph и родительский стиль Standard. Семейство paragraph имеет в styles.xml следующее описание:

style:default-style style:family="paragraph">

Выглядит может и жутко, но в принципе вполне понятно при знании английского языка на уровне школы.
Родительский стиль Standard выглядит намного проще:

style:style style:name="Standard" style:family="paragraph" style:class="text"

Т.е. здесь мы просто ссылаемся на семейство на оновани которого будет происходить форматирование текста и все. Можно потренироваться, добавив в styles.xml, например новое семейство стилей и попробовать применить его в content.xml, думаю, что особых сложностей возникнуть не должно.

Содержимое meta.xml

Содержимое этого файла никак не зависит ни от стилей, ни от семейств стилей, только информация по документу. Рассмотрим назначение некоторых узлов, которые могут нам пригодиться в будущем.
meta:initial-creator — информация о создателе документа
meta:creation-date — дата и время создания документа
meta:generator — информация о программе в которой был создан документ
dc:date — дата изменения документа
dc:creator — информация о том кто вносил изменения
Теперь, более менее определившись с содержимым простого текстового документа, попробуем разработать свой класс для формирования документа OpenOffice.

2. Первые шаги в работе с OpenOffice в Lazarus

Итак, переходим к следующему шагу — работе в Lazarus. Первое, что следует сразу отметить — разрабатывать будем класс. Т.к. работать будем с XML, то для работы нам понадобятся следующие модули:

  • XMLRead
  • XMLWrite
  • DOM

Подключаем их в uses и создаем заготовку для нашего класса:

TOoWriter = class(TObject)
    private
       FStyles   : TXMLDocument; //styles.xml
       FContent  : TXMLDocument; //content.xml
       FManifest : TXMLDocument; //META-INF/manifest.xml
       FMeta     : TXMLDocument; //meta.xml
    public
      constructor Create;
      destructor Destroy;
      property Styles : TXMLDocument read FStyles write FStyles;
      property Content: TXMLDocument read FContent write FContent;
      property Manifest: TXMLDocument read FManifest write FManifest;
      property Meta : TXMLDocument read FMeta write FMeta;
end;

В целом, конечно, можно было бы обойтись и одним полем типа TXMLDocument, но т.к. иногда может потребоваться одновременное внесение изменений в 2-3 файла, то лишние процедуры сохранения/чтения документа скорости модулю не прибавят. Поэтому я немного перестраховался и создал 4 поля — по 1 на каждый файл, необходимый в работе.
Теперь определимся с тем, на основании чего мы будем формировать наш новый документ. Думаю, что пока знаний по формату OpenOffice Documet не много, то начать лучше всего с работы на основании готовых файлов. Т.е. не писать, например styles.xml с нуля, а просто брать готовый, загружать его в поле FStyles и, по необходимости, вносить в него свои изменения. Поэтому пишем первый метод нашего класса — загрузку документа:

function TOoWriter.LoadDocument(const FileName: string; var Doc: TXMLDocument
  ): boolean;
begin
try
  ReadXMLFile(Doc, FileName);
  Result:=true;
except
  Result:=false;
end;
end;

Здесь входными параметрами будут полный путь к файлу, включая название (FileName), который будет загружаться в переменную Doc. Например, чтобы загрузить в поле FMeta содержимое meta.xml необходимо вызвать метод следующим образом:

OoWriter.LoadDocument('/home/user/meta.xml', Meta);

Теперь мы можем загружать необходимые для работы файлы в поля и работать с ними как нам потребуется. Приступим к формированию своего документа.
Во-первых нам необходимо определиться с используемыми в документе шрифтами. Для этого необходимо выполнить как минимум две операции: проверить нет ли необходимого шрифта в секциях office:font-face-decls файлов styles.xml и content.xml и, если шрифт ещё не описан — добавить необходимую запись в документ.
Функция поиска описания шрифта в styles.xml может выглядеть, например, так:

function TOoWriter.StylesFindFntFace(FontName: string): TDOMNode;
var FontList: TDOMNodeList;
    Attr: TDOMNamedNodeMap;
    i,j:integer;
begin
  Result:=nil;
  FontList:=FStyles.GetElementsByTagName('style:font-face');
  for i:=0 to FontList.Length-1 do
    begin
      Attr:=FontList.Item[i].Attributes;
      for j:=0 to Attr.Length-1 do
        if Attr.Item[j].NodeName='style:name' then
          if UpperCase(Attr.Item[j].TextContent)=UpperCase(FontName)then
            begin
              Result:=FontList.Item[i];
              Exit;
            end;
    end;
end;

На входе имеем название шрифта, на выходе — ссылку на узел в файле, если шрифт найден, либо nil.
Соответственно, чтобы добавить новый шрифт в документ можно выполнить следующий метод:

function TOoWriter.AddFont(const FontName: string; var Doc: TXMLDocument): boolean;
var FontNode: TDOMElement;
begin
  try
    FontNode:=Doc.CreateElement('style:font-face');
    FontNode.SetAttribute('style:name',FontName);
    FontNode.SetAttribute('svg:font-family',FontName);
    Doc.GetElementsByTagName('office:font-face-decls').Item[0].AppendChild(FontNode);
    Result:=true;
  except
    Result:=false;
  end;
end;

Новый шрифт будет добавлен в конец секции.
Идём далее. Теперь можно создавать свои стили для абзацев текста. Пока не будем углубляться в такие моменты как написать одну букву слова жирным шрифтом, а остальные наклонным и т.д. Будем задавать стили целиком для абзаца.
Для решения этой задачи нам необходимо знать: шрифт, размер шрифта, стиль шрифта (жирный, наклонный и т.д.) и расположение текста на странице (по центу, по левому краю и т.д.).
Добавляем в модуль следующие типы данных:

type
  TFontStyle = (ftBold, ftItalic, ftUnderline);
  TFontStyles = set of TFontStyle;
  TTextPosition = (tpCenter, tpLeft, tpRight, tpJustify);

И теперь создаем наш метод для добавления нового стиля абзаца текста:

function TOoWriter.AddParagraphStyle(StyleName, FontName: string;
  FontSize: integer; FontStyle: TFontStyles; TextPosition: TTextPosition): boolean;
var Paragraph: TDOMElement;
begin
try
 Paragraph:=FContent.CreateElement('style:style');
 Paragraph.SetAttribute('style:name',StyleName);
 Paragraph.SetAttribute('style:family','paragraph');
 Paragraph.SetAttribute('style:parent-style-name','Standard');
 //добавляем новый тэг параграфа
 FContent.GetElementsByTagName('office:automatic-styles').Item[0].AppendChild(Paragraph);
 Paragraph:=FContent.CreateElement('style:text-properties');
 Paragraph.SetAttribute('fo:font-size',IntToStr(FontSize)+'pt');
 Paragraph.SetAttribute('style:font-name',FontName);
 if ftItalic in FontStyle then
   begin
     Paragraph.SetAttribute('fo:font-style','italic');
     Paragraph.SetAttribute('style:font-style-asian','italic');
     Paragraph.SetAttribute('style:font-style-complex','italic');
   end;
 if ftBold in FontStyle then
   begin
     Paragraph.SetAttribute('fo:font-weight','bold');
     Paragraph.SetAttribute('style:font-weight-asian','bold');
     Paragraph.SetAttribute('style:font-weight-complex','bold');
   end;
 if ftUnderline in FontStyle then
   begin
     Paragraph.SetAttribute('style:text-underline-style','solid');
     Paragraph.SetAttribute('style:text-underline-width','auto');
     Paragraph.SetAttribute('style:text-underline-color','font-color');
   end;
  //добавляем описание шрифта
FContent.GetElementsByTagName('style:style').Item[FContent.GetElementsByTagName('style:style').Length-1].AppendChild(Paragraph);
  case TextPosition of
    tpCenter:begin
               Paragraph:=FContent.CreateElement('style:paragraph-properties');
               Paragraph.SetAttribute('fo:text-align','center');
               Paragraph.SetAttribute('style:justify-single-word','false');
             end;
    tpRight:begin
              Paragraph:=FContent.CreateElement('style:paragraph-properties');
              Paragraph.SetAttribute('fo:text-align','end');
              Paragraph.SetAttribute('style:justify-single-word','false');
            end;
    tpJustify:begin
                Paragraph:=FContent.CreateElement('style:paragraph-properties');
                Paragraph.SetAttribute('fo:text-align','justify');
                Paragraph.SetAttribute('style:justify-single-word','false');
              end;
  end;
//добавляем описание положения текста
FContent.GetElementsByTagName('style:style').Item[FContent.GetElementsByTagName('style:style').Length-1].AppendChild(Paragraph);
Result:=true;
except
  Result:=false;
end;
end;

Следующий шаг — написать, что-либо в документе. Текст пока будем писать последовательно абзац за абзацем без применения функций поиска, вставок и т.д. Функция добавления текста в конец документа выглядит следующим образом:

function TOoWriter.AppendText(StyleName, Text: string): boolean;
var TextNode: TDOMNode;
    SetText: TDOMNode;
begin
try
 TextNode:=FContent.CreateElement('text:p');
 TDOMElement(TextNode).SetAttribute('text:style-name',StyleName);
 SetText:=FContent.CreateTextNode(Text);
 TextNode.AppendChild(SetText);
FContent.GetElementsByTagName('office:text').Item[FContent.GetElementsByTagName('office:text').Length-1].AppendChild(TextNode);
 Result:=true;
except
 Result:=false;
end;
end;

Теперь следует обратить внимание на содержание файла manifest.xml. Дело в том, что по умолчанию в manifest вписываются пути к эскизам документа, настройкам и т.д. Нам сейчас они не нужны. Поэтому напишем небольшую процедуру генерации собственного manifest.xml:

procedure TOoWriter.GenerateManifest;
var Root,Parent: TDOMNode;
    i:integer;
begin
 if Assigned(FManifest) then FreeAndNil(FManifest);
 FManifest:=TXMLDocument.Create;

 Root:=FManifest.CreateElement('manifest:manifest');
 TDOMElement(Root).SetAttribute('xmlns:manifest','urn:oasis:names:tc:opendocument:xmlns:manifest:1.0');
 FManifest.Appendchild(Root);
 Root:=FManifest.DocumentElement;
 Parent:=FManifest.CreateElement('manifest:file-entry');
 TDOMElement(Parent).SetAttribute('manifest:media-type','application/vnd.oasis.opendocument.text');
 TDOMElement(Parent).SetAttribute('manifest:version','1.2');
 TDOMElement(Parent).SetAttribute('manifest:full-path','/');
 Root.AppendChild(Parent);
 for i:=1 to High(DocEntrys) do
   begin
     Parent:=FManifest.CreateElement('manifest:file-entry');
     TDOMElement(Parent).SetAttribute('manifest:full-path',DocEntrys[i]);
     TDOMElement(Parent).SetAttribute('manifest:media-type','text/xml');
     Root.AppendChild(Parent);
    end;
end;

После выполнения процедуры в manifest.xml будет содержаться только та информация, которая необходима для отображения текста нашего файла. Ничего больше.
Ну и наконец последний шаг к достижению цели — упаковка всех файлов в один документ *.odt:

procedure TOoWriter.GenerateDocument(const DocumentPath, DocumentName:string; ZipPath: string='');
var P:string;
    Script: TextFile;
begin
if (Not Assigned(FManifest)) or (FManifest.ChildNodes.Count=0) then
  GenerateManifest;
  if not DirectoryExists(DocumentPath) then
    CreateDir(DocumentPath);
  P:=IncludeTrailingBackslash(DocumentPath);
  //сохраняем все необходимые файлы в директорию
  CreateDir(P+'META-INF');
  WriteXMLFile(FContent, P+'content.xml');
  WriteXMLFile(FStyles, P+'styles.xml');
  WriteXMLFile(FMeta, P+'meta.xml');
  WriteXMLFile(FManifest, P+'META-INF/manifest.xml');
  //пишем скрипт для сжития в zip
  {$IFDEF Linux}
    AssignFile(Script, ExtractFilePath(Application.ExeName)+'script.sh');
    Rewrite(Script);
    WriteLn(Script, '#!/bin/bash');
    WriteLn(Script, 'cd '+P);
    WriteLn(Script, 'zip -r '+DocumentName+'.odt *');
    CloseFile(Script);
    SysUtils.ExecuteProcess('/bin/bash',[ExtractFilePath(Application.ExeName)+'script.sh']);
    DeleteFile(ExtractFilePath(Application.ExeName)+'script.sh');
  {$ENDIF}

  {$IFDEF Win32}
    AssignFile(Script, ExtractFilePath(Application.ExeName)+'script.bat');
    Rewrite(Script);
    WriteLn(Script, 'CD '+P);
    WriteLn(Script, IncludeTrailingBackslash(ZipPath)+'zip -r '+DocumentName+'.odt . -i meta.xml styles.xml content.xml META-INFmanifest.xml');
    CloseFile(Script);
    SysUtils.ExecuteProcess(Win32_Cmd,[ExtractFilePath(Application.ExeName)+'script.bat']);
    DeleteFile(ExtractFilePath(Application.ExeName)+'script.bat');
  {$ENDIF}
  DeleteFile(P+'content.xml');
  DeleteFile(P+'styles.xml');
  DeleteFile(P+'meta.xml');
  DeleteFile(P+'META-INF/manifest.xml');
  DeleteDirectory(P+'META-INF',false);
end;

Здесь входными параметрами являются:
DocumentPath — полный путь к директории где будет сохранен документ
DocumentName — имя документа без расширения
ZipPath — полный путь к утилите zip (указывается только в Win32)
Вот пожалуй всё, что касается основной части работы по созданию odt-файла с текстом. Дополнительно можно «повыделываться» и записать информацию об авторе документа:

procedure TOoWriter.SetMetaAuthor(const Author: string);
var Node, Txt: TDOMNode;
    List: TDOMNode;
begin
  if FMeta.ChildNodes.Count=0 then GenerateMeta;
  Node:=FMeta.CreateElement('meta:initial-creator');
  Txt:=FMeta.CreateTextNode(Author);
  Node.AppendChild(Txt);
  List:=FMeta.FindNode('office:meta');
  List:=FMeta.DocumentElement;
  List.ChildNodes.Item[0].AppendChild(Node);
end;

Здесь в случае отсутствия информации о файле meta.xml он создается автоматически в процедуре GenerateMeta:

procedure TOoWriter.GenerateMeta;
  function MakeDate(const ADate: TDateTime): string;
var AYear, AMonth, ADay, AHour, AMinute, ASecond, AMilliSecond: Word;
begin
  DecodeDateTime(ADate, AYear, AMonth, ADay, AHour, AMinute, ASecond, AMilliSecond);
  Result:=IntToStr(AYear)+'-';

  if AMonth<10 then
    Result:=Result+'0'+IntToStr(AMonth)+'-'
  else
    Result:=Result+IntToStr(AMonth)+'-';

  if ADay<10 then
    Result:=Result+'0'+IntToStr(ADay)
  else
    Result:=Result+IntToStr(ADay);
  Result:=Result+'T'+IntToStr(AHour)+':'+IntToStr(AMinute)+':'+IntToStr(ASecond);
end;
var Root,Parent,Txt: TDOMNode;
begin
  if Assigned(FMeta) then FreeAndNil(FMeta);
  FMeta:=TXMLDocument.Create;
  Root:=FMeta.CreateElement('office:document-meta');
TDOMElement(Root).SetAttribute('xmlns:office','urn:oasis:names:tc:opendocument:xmlns:office:1.0');
TDOMElement(Root).SetAttribute('xmlns:meta','urn:oasis:names:tc:opendocument:xmlns:meta:1.0');
  FMeta.Appendchild(Root);
  Root:=FMeta.DocumentElement;
  Parent:=FMeta.CreateElement('office:meta');
  Root.AppendChild(Parent);
  Parent:=FMeta.DocumentElement;
  Root:=FMeta.CreateElement('meta:creation-date');
  Txt:=FMeta.CreateTextNode(MakeDate(Now));
  Root.AppendChild(Txt);
  Parent.ChildNodes.Item[0].AppendChild(Root);
end;

Теперь рассмотрим небольшой пример использования класса. Как и обещал в начале — запишем в файл «Hello World!» различными стилями.

Пример записи текста в файл OpenOffice Writer

Открываем Lazarus, cоздаем новый проект и на любое событие делаем вот такой простой обработчик:

 OoWriter:=TOoWriter.Create;
 OoWriter.LoadDocument('/home/vlad/Lazarus-Work/OpenOffice/Untitled 1.odt_FILES/styles.xml',OoWriter.Styles);
 OoWriter.Generator:='MyGenerator';
 OoWriter.Author:='Ivanov Ivan Ivanovich';
 if OoWriter.LoadDocument('/home/vlad/Lazarus-Work/OpenOffice/Untitled 1.odt_FILES/content.xml',OoWriter.Content) then
   begin
     OoWriter.AddFont('Times New Roman', OoWriter.Styles);
     OoWriter.AddFont('Times New Roman', OoWriter.Content);
     OoWriter.AddParagraphStyle('MyStyle', 'Arial', 18, [ftBold], tpCenter);
     OoWriter.AddParagraphStyle('MyStyle2', 'Times New Roman', 14, [ftItalic,ftUnderline], tpLeft);
     OoWriter.AppendText('MyStyle', 'HelloWorld');
     OoWriter.AppendText('MyStyle2', 'HelloWorld, Left, Underline');
OoWriter.GenerateDocument(ExtractFilePath(Application.ExeName)+'document','MyDoc','');
   end;

Как видите, в представленном примере styles.xml и content.xml загружаются из ранее сохраненных файлов, а meta.xml и manifest.xml создаются автоматически при работе внутри класса.
В результате выполнения представленного обработчика Вы должны получить документ со следующим содержимым:

helloworld

На сегодня всё :) На всякий случай выкладываю исходник модуля. Вполне возможно, что он пригодится Вам в ваших разработках.

5 7 голоса
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
4 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
Наиль
Наиль
14/12/2009 20:59

В Linux для управления OOo используется UNO.
Как это использовать я не знаю, но кое-какие зацепки можно найти здесь:
http://www.sql.ru/forum/actualthread.aspx?bid=20&tid=405083&pg=20

Arkano
06/04/2012 02:08

Пути изменил, файлы существуют, но у меня ошибки прут:
Hint: Start of reading config file C:\lazarus\fpc\2.6.0\bin\i386-win32\fpc.cfg
Hint: End of reading config file C:\lazarus\fpc\2.6.0\bin\i386-win32\fpc.cfg
Free Pascal Compiler version 2.6.0 [2012/03/14] for i386
Copyright (c) 1993-2011 by Florian Klaempfl and others
Target OS: Win32 for i386
Compiling project1.lpr
Compiling unit1.pas
unit1.pas(36,200) Error: Can’t take the address of constant expressions
unit1.pas(39,205) Error: Can’t take the address of constant expressions
unit1.pas(41,57) Error: Can’t take the address of constant expressions
unit1.pas(42,58) Error: Can’t take the address of constant expressions
unit1.pas(53) Fatal: There were 4 errors compiling module, stopping

trackback

[…] Применительно к модулю Lazarus, который я рассматривал в предыдущем посте, функция добавления стиля таблицы в content.xml может […]