lazarus/components/fpvectorial/docxvectorialwriter.pas
sekelsenmat 3acde3bf25 fpvectorial: Improves list rendering
git-svn-id: trunk@48985 -
2015-05-11 10:02:24 +00:00

1388 lines
42 KiB
ObjectPascal

{
docxvectorialwriter.pas
License: The same modified LGPL as the Free Pascal RTL
See the file COPYING.modifiedLGPL for more details
THIS IS A UNIT CURRENTLY UNDER DEVELOPEMENT
Current Functionality
- Text support (inc Tabs and CRs)
- Paragraph/Character Style Support
- Supports Section Breaks (via PageSequences)
- Supports Header and Footer
- Supports Portrait/Landscape
- Support Tables
TODO
- Add following to both FPVectorial AND DOCXWriter
- Add Image Support (From file and from Stream, in Paragraph and in Table)
- Add simple Work Fields (NumPage, PageCount, Filename (Full, part), DatePrinted)
- Add TOC support
- Consider Unicode Support (eek)
Writes an OOXML (Office Open XML) document
An OOXML document is a compressed ZIP file with the following files inside:
[Content_Types].xml - An index of all files, defines their content format
_rels\.rels - Relationships between high level documents
word\_rels\document.xml.rels - defines relationships to files required by document.xml (ie styles.xml)
word\document.xml - this is the main document. Conforms to WordprocessingML
word\styles.xml - The Style Library
word\header%d.xml - One file per header
word\footer%d.xml - One file per footer
word\numbering.xml - Header and List numbering details
media\*.[png, jpg, etc] - Images
Specifications and examples obtained from:
http://openxmldeveloper.org/default.aspx
http://officeopenxml.com/
http://www.ecma-international.org/publications/standards/Ecma-376.htm
- Office Open XML Part 4 - Markup Language Reference.docx (First edition)
AUTHORS: Mike Thompson, Felipe Monteiro de Carvalho
Change History
0.1 - minimal document.xml produced
- experimental support for styles (not working, hidden behind INCLUDE_STYLES define)
0.2 - Refactored for new VectorialElement TvParagraph
- Removed INCLUDE_STYLES define
- Added support for Styles - resulting .docx contains ONLY styles defined in VectorialDocument
Only Paragraph Style supported for now
- Added IndentedStringList simply to prettify the resulting XML
0.3 - Added support for Character Styles (named TextSpan Styles within FPVectorial
- If Style.Name not defined, then create one based on index
- Refactored PrepareTextRunStyle out of Prepare Styles. Resulting XML is perfectly valid
inside Document.XML and Style.xml, though FPVectorial currently does not support this
- Closed out minor TODO items
0.4 - realised <>'"& would break the resulting XML. Added EscapeHTML when
User defined text is outputted into the HTML
- Multiple PageSequences are now handled.
- Added support for Portrait/Landscape (reverse Width/Height if you want Landscape)
- Added generic support for multiple files. Link IDs and Relationships between
Files are automatically handled. Currently works for XML files only
but should be easily extended for Image Support (code tagged with TODO)
- Added support for Header and Footer. Can be defined per PageSequence resulting
in multiple headers and footers or just on the first PageSequence
(in which case the rest of the document will inherit the same Header / Footer)
Couldn't get the inline Header/Footers to work, so implemented as separate files.
- Added handling for #11 (tab), #10 (line feed), #13 (CR) and #13#10 in Text Support
- Significant refactoring of code, and formatted code with Jedi Code Format.
I intend to Jedi Code Format before each Patch from here on
- Added support for Alignment to Styles
0.5 - Support TvParagraph margins
- Added numbering.xml
0.6 - Changed #11 (vert tab) to #09 (horiz tab, what it always should have been)
- Added Table Support
- Bug fix - Margin support in Styles fixed...
0.7 - Added experimental LocalAlignment support to TvParagraph
}
Unit docxvectorialwriter;
{$mode objfpc}{$H+}
Interface
Uses
Classes, SysUtils,
zipper, {NOTE: might require zipper from FPC 2.6.2+ }
fpimage, fpcanvas,
fpvectorial, fpvutils, lazutf8, Math;
Type
TIndentOption = (indInc, indDec, indNone);
{ TIndentedStringList }
// Here to just to ensure the resulting xml files are pretty :-)
{ TODO : Replace this with a genuine XML handler }
TIndentedStringList = Class(TStringList)
Private
FIndent: String;
FIndentSteps: String;
Public
Constructor Create;
Function Add(indBefore: TIndentOption = indNone; S: String = '';
indAfter: TIndentOption = indNone): Integer;
Function Add(Const S: String): Integer; Override;
Function Add(Const S: String; indAfter: TIndentOption): Integer;
Procedure IncIndent;
Procedure DecIndent;
End;
TFileType = (ftContentTypes, ftRelationships, ftDocRelationships);
TFileTypes = Set Of TFileType;
{ TFileInformation }
TFileInformation = Class(TObject)
Private
FStream: TStream;
Function GetStream: TStream;
Public
Index: Integer;
ContentType: String;
Path: String;
FileType: String;
MentionedIn: TFileTypes;
XML: TIndentedStringList; // Free'd internally;
//Image : TFPImage; { TODO: How are we going to handle images? }
Constructor Create;
Destructor Destroy; Override;
Function ID: String;
Function Filename: String;
Procedure FreeStream;
Property Stream: TStream read GetStream;
// This creates a TSream, Call .FreeStream to free
End;
{ TFileList }
TFileList = Class(TObject)
Private
FList: TList;
Function GetFile(AIndex: Integer): TFileInformation;
Public
Constructor Create;
Destructor Destroy; Override;
Function AddXMLFile(AContentType: String; APath: String;
AFileType: String; AMentionedIn: TFileTypes): TFileInformation;
Function Count: Integer;
Property FileInformation[AIndex: Integer]: TFileInformation read GetFile; Default;
End;
{ TvDOCXVectorialWriter }
TvDOCXVectorialWriter = Class(TvCustomVectorialWriter)
Private
FData: TvVectorialDocument;
FFiles: TFileList;
Function PrepareContentTypes: String;
Function PrepareRelationships: String;
Function PrepareDocRelationships: String;
Procedure PrepareDocument;
Procedure PrepareStyles; // Only created if FData has Styles defined
Procedure PrepareNumbering; // Only created if Numbered Styles exist
Procedure PrepareTextRunStyle(ADoc: TIndentedStringList; AStyle: TvStyle);
Function StyleNameToStyleID(AStyle: TvStyle): String;
Public
{ General reading methods }
Constructor Create; Override;
Destructor Destroy; Override;
Procedure WriteToFile(AFileName: String; AData: TvVectorialDocument); Override;
Procedure WriteToStream(AStream: TStream; AData: TvVectorialDocument); Override;
End;
Implementation
Uses
strutils, htmlelements;
Const
XML_HEADER = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>';
{ OOXML general XML constants }
// Note: No leading '/'. Add where required
OOXML_PATH_TYPES = '[Content_Types].xml';
OOXML_PATH_RELS_RELS = '_rels/.rels';
OOXML_PATH_DOCUMENT_RELS = 'word/_rels/document.xml.rels';
OOXML_PATH_DOCUMENT = 'word/document.xml';
OOXML_PATH_STYLES = 'word/styles.xml';
OOXML_PATH_HEADER = 'word/header%d.xml'; // Use Format(OOXML_PATH_HEADER, [Index]);
OOXML_PATH_FOOTER = 'word/footer%d.xml'; // Use Format(OOXML_PATH_HEADER, [Index]);
OOXML_PATH_NUMBERING = 'word/numbering.xml';
OOXML_RELS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
OOXML_TYPE_DOCUMENT = OOXML_RELS + '/officeDocument';
OOXML_TYPE_STYLES = OOXML_RELS + '/styles';
OOXML_TYPE_HEADER = OOXML_RELS + '/header';
OOXML_TYPE_FOOTER = OOXML_RELS + '/footer';
OOXML_TYPE_NUMBERING = OOXML_RELS + '/numbering';
OOXML_CONTENTTYPE = 'application/vnd.openxmlformats-officedocument.wordprocessingml';
OOXML_CONTENTTYPE_DOCUMENT = OOXML_CONTENTTYPE + '.document.main+xml';
OOXML_CONTENTTYPE_STYLES = OOXML_CONTENTTYPE + '.styles+xml';
OOXML_CONTENTTYPE_HEADER = OOXML_CONTENTTYPE + '.header+xml';
OOXML_CONTENTTYPE_FOOTER = OOXML_CONTENTTYPE + '.footer+xml';
OOXML_CONTENTTYPE_NUMBERING = OOXML_CONTENTTYPE + '.numbering+xml';
// Shared between document.xml and each header.xml/footer.xml
OOXML_DOCUMENT_NAMESPACE =
'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" ' +
'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" ';
TAG_HEADER = 'hdr';
TAG_FOOTER = 'ftr';
// Lookups...
LU_ALIGN: Array [TvStyleAlignment] Of String =
('left', 'right', 'both', 'center');
LU_NUMBERFORMAT: Array [TvNumberFormat] Of String =
('decimal', 'lowerLetter', 'lowerRoman', 'upperLetter', 'upperRoman');
LU_NUMBERFORMATFORUMLA: Array [TvNumberFormat] Of String =
('Arabic', 'alphabetic', 'roman', 'ALPHABETIC', 'Roman');
LU_ON_OFF: Array[Boolean] Of String = ('off', 'on');
LU_BORDERTYPE: Array[TvTableBorderType] Of String =
('single', 'dashed', 'double', 'none', 'default');
LU_V_ALIGN: Array[TvVerticalAlignment] Of String = ('top', 'bottom', 'center', 'both');
//ONE_POINT_IN_MM = 0.35278;
// Generic Helper Units
Function PointsToTwipsS(APoints: Double): String;
Begin
// 1 Twip = 1/20 of a point
Result := IntToStr(Round(20 * APoints));
End;
Function mmToPointS(AMillimetres: Double): String;
Begin
Result := IntToStr(Round((0.0393701 * 1440 * AMillimetres) / 20));
End;
Function mmToTwipsS(AMillimetres: Double): String;
Begin
// 1 Twip = 1 / 1440 of an inch - sigh...
Result := IntToStr(Round(0.0393701 * 1440 * AMillimetres));
End;
Function DimAttribs(ADimension: TvDimension; AValueTag: String = 'w:w';
ATypeTag: String = 'w:type'): String;
Var
iValue: Integer;
sType: String;
Begin
Case ADimension.Units Of
dimMillimeter:
Begin
iValue := Round(0.0393701 * 1440 * ADimension.Value);
sType := 'dxa'; // most values in docx must be in twips
End;
dimPercent:
Begin
iValue := Round(50 * ADimension.Value);
sType := 'pct'; // 50ths of a percent
End;
dimPoint:
Begin
iValue := Round(20 * ADimension.Value);
sType := 'dxa'; // most values in docx must be in twips
End;
End;
Result := Format(' %s="%d" %s="%s" ', [AValueTag, iValue, ATypeTag, sType]);
End;
{ TFileInformation }
Constructor TFileInformation.Create;
Begin
FStream := nil;
XML := nil;
Path := '';
FileType := '';
ContentType := '';
Index := -1;
MentionedIn := [];
End;
Destructor TFileInformation.Destroy;
Begin
If Assigned(XML) Then
XML.Free;
Inherited Destroy;
End;
Function TFileInformation.ID: String;
Begin
Result := 'rId' + IntToStr(Index);
End;
Function TFileInformation.Filename: String;
Var
i: Integer;
Begin
i := RPos('/', Path);
If i > 0 Then
Result := Copy(Path, i + 1, Length(Path) - i)
Else
Result := Path;
End;
Function TFileInformation.GetStream: TStream;
Begin
If Not Assigned(FStream) Then
Begin
If Assigned(XML) Then
Begin
FStream := TMemoryStream.Create;
XML.SaveToStream(FStream);
FStream.Position := 0;
End;
(* { TODO : How are we going to handle images? }
Else If Assigned(Image) Then
Begin
end;
*)
End;
Result := FStream;
End;
Procedure TFileInformation.FreeStream;
Begin
If Assigned(FStream) Then
Begin
FStream.Free;
FStream := nil;
End;
End;
{ TFileList }
Function TFileList.GetFile(AIndex: Integer): TFileInformation;
Begin
Result := TFileInformation(FList[AIndex]);
End;
Constructor TFileList.Create;
Begin
FList := TList.Create;
End;
Destructor TFileList.Destroy;
Begin
While (FList.Count > 0) Do
Begin
TFileInformation(FList.Last).Free;
FList.Delete(FList.Count - 1);
End;
Inherited Destroy;
End;
Function TFileList.AddXMLFile(AContentType: String; APath: String;
AFileType: String; AMentionedIn: TFileTypes): TFileInformation;
Begin
Result := TFileInformation.Create;
Result.ContentType := AContentType;
Result.FileType := AFileType;
Result.Path := APath;
Result.MentionedIn := AMentionedIn;
Result.XML := TIndentedStringList.Create;
Result.Index := FList.Count;
FList.Add(Result);
End;
Function TFileList.Count: Integer;
Begin
Result := FList.Count;
End;
{ TIndentedStringList }
Constructor TIndentedStringList.Create;
Begin
FIndent := '';
FIndentSteps := ' ';
End;
Function TIndentedStringList.Add(indBefore: TIndentOption; S: String;
indAfter: TIndentOption): Integer;
Begin
If indBefore = indInc Then
IncIndent
Else If indAfter = indDec Then
DecIndent;
Result := Inherited Add(FIndent + S);
If indAfter = indInc Then
IncIndent
Else If indBefore = indDec Then
DecIndent;
End;
Function TIndentedStringList.Add(Const S: String): Integer;
Begin
Result := Inherited Add(FIndent + S);
End;
Function TIndentedStringList.Add(Const S: String; indAfter: TIndentOption): Integer;
Begin
Result := Add(indNone, S, indAfter);
End;
Procedure TIndentedStringList.DecIndent;
Begin
FIndent := Copy(FIndent, 1, Length(FIndent) - Length(FIndentSteps));
End;
Procedure TIndentedStringList.IncIndent;
Begin
FIndent := FIndent + FIndentSteps;
End;
{ TvDOCXVectorialWriter }
Constructor TvDOCXVectorialWriter.Create;
Begin
Inherited Create;
FFiles := TFileList.Create;
End;
Destructor TvDOCXVectorialWriter.Destroy;
Begin
// Free's all the XML IndentedStringLists automagically
FFiles.Free;
FFiles := nil;
Inherited Destroy;
End;
Function TvDOCXVectorialWriter.PrepareContentTypes: String;
Var
i: Integer;
Begin
Result := XML_HEADER + LineEnding +
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' +
LineEnding +
' <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
+ LineEnding + ' <Default Extension="xml" ContentType="application/xml"/>' +
LineEnding;
For i := 0 To FFiles.Count - 1 Do
If ftContentTypes In FFiles[i].MentionedIn Then
Result := Result + ' <Override PartName="/' + FFiles[i].Path +
'" ContentType="' + FFiles[i].ContentType + '"/>' + LineEnding;
Result := Result + '</Types>';
End;
Function TvDOCXVectorialWriter.PrepareRelationships: String;
Var
i: Integer;
Begin
Result := XML_HEADER + LineEnding +
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
+ LineEnding;
For i := 0 To FFiles.Count - 1 Do
If ftRelationships In FFiles[i].MentionedIn Then
Result := Result + ' <Relationship Id="' + FFiles[i].ID +
'" Type="' + FFiles[i].FileType + '" Target="/' + FFiles[i].Path +
'"/>' + LineEnding;
Result := Result + '</Relationships>';
End;
Function TvDOCXVectorialWriter.PrepareDocRelationships: String;
Var
i: Integer;
Begin
Result := XML_HEADER + LineEnding +
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
+ LineEnding;
For i := 0 To FFiles.Count - 1 Do
If ftDocRelationships In FFiles[i].MentionedIn Then
Result := Result + ' <Relationship Id="' + FFiles[i].ID +
'" Type="' + FFiles[i].FileType + '" Target="' + FFiles[i].Filename +
'"/>' + LineEnding;
Result := Result + '</Relationships>';
End;
Procedure TvDOCXVectorialWriter.PrepareDocument;
Var
// Generally this is document.xml, may also be header.xml or footer.xml though..
oDocXML: TIndentedStringList;
iPage: Integer;
Procedure ProcessRichText(ARichText: TvRichText); Forward;
Procedure AddTextRun(sText: String; AStyle: TvStyle);
Var
iLen, iStart, i: Integer;
sTemp: String;
Begin
// Don't both writing null Text Runs..
If sText <> '' Then
Begin
i := 1;
iStart := 1;
iLen := Length(sText);
// Place all text between Tabs and CRs into individual Text Runs,
// and render the Tabs and CRs appropriately
While i <= iLen Do
Begin
If (sText[i] In [#10, #09, #13]) Or (i = iLen) Then
Begin
// Add the text before this point into a single Text Run
If i > iStart Then
Begin
// If end of line AND end of line isn't a special char, then
// inc(i) to ensure the math in the Copy works :-)
If (i = iLen) And Not (sText[i] In [#10, #09, #13]) Then
Inc(i);
sTemp := Copy(sText, iStart, i - iStart);
oDocXML.Add(indInc, '<w:r>');
If Assigned(AStyle) Then
Begin
oDocXML.Add(indInc, '<w:rPr>');
oDocXML.Add(' <w:rStyle w:val="' + StyleNameToStyleID(AStyle) + '"/>');
oDocXML.Add(indDec, '</w:rPr>');
End;
oDocXML.Add(' <w:t>' + EscapeHTML(sTemp) + '</w:t>');
oDocXML.Add(indDec, '</w:r>');
End;
// Deal with the Tabs, LF and CRs appropriately
If sText[i] = #09 Then
oDocXML.Add(' <w:r><w:tab/></w:r>')
Else If sText[i] In [#10, #11, #13] Then
Begin
oDocXML.Add(' <w:r><w:br/></w:r>');
// Now deal with CRLF by skipping over any trailing LF
If (i < iLen) And (sText[i] = #13) Then
If sText[i + 1] = #10 Then
Inc(i);
End;
iStart := i + 1;
End;
Inc(i);
End;
End;
End;
Procedure AddField(AField : TvField);
var
sInstruction: String;
sDefault: String;
Begin
sInstruction := '';
sDefault := '';
Case AField.Kind of
vfkNumPages:
Begin
sInstruction := ' NUMPAGES \* '+LU_NUMBERFORMATFORUMLA[AField.NumberFormat]+' \* MERGEFORMAT ';
sDefault := IntToStr(FData.GetPageCount);
End;
vfkPage:
Begin
sInstruction := ' PAGE \* '+LU_NUMBERFORMATFORUMLA[AField.NumberFormat]+' \* MERGEFORMAT ';
sDefault := IntToStr(iPage+1);
End;
vfkAuthor:
Begin
sInstruction := ' AUTHOR \* Caps \* MERGEFORMAT ';
sDefault := 'FPVECTORIAL';
End;
vfkDateCreated:
Begin
sInstruction := ' CREATEDATE \@ "'+AField.DateFormat+'" \* MERGEFORMAT ';
sDefault := DateToStr(Now);
End;
vfkDate:
Begin
sInstruction := ' DATE \@ "'+AField.DateFormat+'" \* MERGEFORMAT ';
sDefault := DateToStr(Now);
End;
end;
If sInstruction<>'' Then
Begin
If Assigned(AField.Style) Then
Begin
oDocXML.Add(indInc, '<w:rPr>');
oDocXML.Add(' <w:rStyle w:val="' + StyleNameToStyleID(AField.Style) + '"/>');
oDocXML.Add(indDec, '</w:rPr>');
End;
// Start the Formula
oDocXML.Add('<w:r><w:fldChar w:fldCharType="begin"/></w:r>');
// Add the Instruction
oDocXML.Add('<w:r><w:instrText xml:space="preserve">'+
sInstruction+
'</w:instrText></w:r>');
// SEPARATE the Field (above) from the result (below)
oDocXML.Add('<w:r><w:fldChar w:fldCharType="separate"/></w:r>');
// Add the default text
oDocXML.Add('<w:r><w:t>'+sDefault+'</w:t></w:r>');
// End the Forumla
oDocXML.Add('<w:r><w:fldChar w:fldCharType="end"/></w:r>');
end;
end;
Procedure ProcessParagraph(AParagraph: TvParagraph; AListLevel : integer = -1; ANumID : Integer = -1);
Var
i: Integer;
oEntity: TvEntity;
sTemp: String;
Begin
oDocXML.Add(indInc, '<w:p>');
// Add the Paragraph Properties
oDocXML.Add(indInc, '<w:pPr>', indInc);
If Assigned(AParagraph.Style) Then
oDocXML.Add(Format('<w:pStyle w:val="%s"/>',
[StyleNameToStyleID(AParagraph.Style)]));
If (AListLevel<>-1) Then
Begin
oDocXML.Add('<w:numPr>');
oDocXML.Add(indInc, Format('<w:ilvl w:val="%d"/>', [AListLevel]));
oDocXML.Add(indDec, Format('<w:numId w:val="%d"/>', [ANumID]));
oDocXML.Add('</w:numPr>');
End;
oDocXML.Add(indDec, '</w:pPr>', indDec);
For i := 0 To AParagraph.GetEntitiesCount - 1 Do
Begin
oEntity := AParagraph.GetEntity(i);
// Adding the TvText like this means each line in the StringList
// will result in a <BR/> adding to the XML
If oEntity Is TvText Then
Begin
sTemp := TvText(oEntity).Value.Text;
// Strip out the trailing line break
// added by TStringList.Text
If DefaultTextLineBreakStyle = tlbsCRLF Then
sTemp := Copy(sTemp, 1, Length(sTemp) - 2)
Else
sTemp := Copy(sTemp, 1, Length(sTemp) - 1);
AddTextRun(sTemp, TvText(oEntity).Style);
End
Else If oEntity is TvField Then
AddField(TvField(oEntity))
Else
{ TODO : What other entities in TvParagraph do I need to process }
Raise Exception.Create('Unsupported Entity: ' + oEntity.ClassName);
End;
oDocXML.Add(indDec, '</w:p>');
End;
Procedure ProcessList(AList: TvList);
Var
i: Integer;
oEntity: TvEntity;
Begin
For i := 0 To AList.GetEntitiesCount - 1 Do
Begin
oEntity := AList.GetEntity(i);
If oEntity Is TvParagraph Then
Begin
If Not Assigned(TvParagraph(oEntity).Style) Then
TvParagraph(oEntity).Style := AList.Style;
ProcessParagraph(TvParagraph(oEntity), AList.GetLevel(), FData.FindListStyleIndex(AList.ListStyle) + 1);
End
Else If oEntity Is TvList Then
ProcessList(TvList(oEntity))
Else
Raise Exception.Create('Unsupported entity ' + oEntity.ClassName);
End;
End;
Procedure ProcessHeaderFooter(AElement: TvRichText; ATag: String);
Var
oTemp: TIndentedStringList;
oFile: TFileInformation;
Begin
// If Header or Footer contains no elements, don't add them...
If AElement.GetEntitiesCount > 0 Then
Begin
// Create additional XML files for each Header and Footer
If ATag = TAG_HEADER Then
oFile := FFiles.AddXMLFile(OOXML_CONTENTTYPE_HEADER,
Format(OOXML_PATH_HEADER, [FFiles.Count]), OOXML_TYPE_HEADER,
[ftContentTypes, ftDocRelationships])
Else
oFile := FFiles.AddXMLFile(OOXML_CONTENTTYPE_FOOTER,
Format(OOXML_PATH_FOOTER, [FFiles.Count]), OOXML_TYPE_FOOTER,
[ftContentTypes, ftDocRelationships]);
// For the next few steps, all the ProcessXXX routines will be working on the header or footer...
oTemp := oDocXML;
oDocXML := oFile.XML;
oDocXML.Add(XML_HEADER);
oDocXML.Add(Format('<w:%s %s>', [ATag, OOXML_DOCUMENT_NAMESPACE +
'xml:space="preserve"']));
ProcessRichText(AElement);
// Close the new xml file...
oDocXML.Add(Format('</w:%s>', [ATag]));
// ProcessXXX routines will now resume working on document.xml
oDocXML := oTemp;
{ TODO : FPVectorial currently doesn't support Odd or Even Page Header/Footers }
// Add the reference to the newly created Header or Footer into the main document
If ATag = TAG_HEADER Then
oDocXML.Add('<w:headerReference w:type="default" r:id="' + oFile.ID + '"/>')
Else
oDocXML.Add('<w:footerReference w:type="default" r:id="' + oFile.ID + '"/>');
End;
End;
Procedure FinalisePage(APageSequence: TvTextPageSequence; ALastPage: Boolean);
Var
dWidth, dHeight: Double;
sTemp: String;
Begin
// For the final pagesequence only w:sectPr shouldn't be wrapped inside w:p or w:pPr
If Not ALastPage Then
Begin
oDocXML.Add(indInc, '<w:p>');
oDocXML.Add(indInc, '<w:pPr>');
End;
oDocXML.Add(indInc, '<w:sectPr>', indInc);
ProcessHeaderFooter(APageSequence.Header, TAG_HEADER);
ProcessHeaderFooter(APageSequence.Footer, TAG_FOOTER);
// Define the Page Layout
dWidth := APageSequence.Width;
If dWidth = 0 Then
dWidth := FData.Width;
If dWidth=0 Then
dWidth := 210; // Default A4
dHeight := APageSequence.Height;
If dHeight = 0 Then
dHeight := FData.Height;
If dHeight=0 Then
dHeight := 297; // Default A4
If ((dWidth <> 0) And (dHeight <> 0)) Then
Begin
sTemp := Format('<w:pgSz w:w="%s" w:h="%s"',
[mmToTwipsS(dWidth), mmToTwipsS(dHeight)]);
If dWidth < dHeight Then
sTemp := sTemp + '/>'
Else
sTemp := sTemp + ' w:orient="landscape"/>';
oDocXML.Add(sTemp);
End;
{ TODO : Ensure these other properties can be set }
//<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="708" w:footer="708" w:gutter="0"/>
//<w:cols w:space="708"/>
//<w:docGrid w:linePitch="360"/>
oDocXML.Add(indDec, '</w:sectPr>', indDec);
If Not ALastPage Then
Begin
oDocXML.Add(indDec, '</w:pPr>');
oDocXML.Add(indDec, '</w:p>');
End;
End;
Procedure AddTableBorderProperty(ATag: String; ABorder: TvTableBorder);
Var
sAttrib: String;
Begin
If ABorder.LineType <> tbtDefault Then
Begin
sAttrib := '';
If ABorder.LineType <> tbtNone Then
sAttrib := sAttrib + 'w:val="' + LU_BORDERTYPE[ABorder.LineType] + '" ';
sAttrib := sAttrib + 'w:space="' + mmToPointS(ABorder.Spacing) + '" ';
If ABorder.Width <> 0 Then
// Eights of a Point?? Really, they're just making this up...
sAttrib := sAttrib + 'w:sz="' + mmToPointS(Min(96, Max(2, 8 * ABorder.Width))) + '" '
Else
// 4 is the minimum
sAttrib := sAttrib + 'w:sz="4" ';
If ABorder.Color <> FPColor(0, 0, 0, 0) Then
sAttrib := sAttrib + 'w:color="' + FPColorToRGBHexString(ABorder.Color) + '" '
Else
sAttrib := sAttrib + 'w:color="auto" ';
oDocXML.Add(Format('<%s %s/>', [ATag, Trim(sAttrib)]));
End;
End;
Procedure ProcessTable(ATable: TvTable);
Var
i, j, k: Integer;
oRow: TvTableRow;
oCell: TvTableCell;
//sTemp : String;
Begin
oDocXML.Add(indInc, '<w:tbl>');
// Add the table properties
oDocXML.Add(indInc, '<w:tblPr>', indInc);
If ATable.PreferredWidth.Value <> 0 Then
oDocXML.Add(Format('<w:tblW %s />', [DimAttribs(ATable.PreferredWidth)]));
oDocXML.Add(indNone, '<w:tblBorders>', indInc);
AddTableBorderProperty('w:left', ATable.Borders.Left);
AddTableBorderProperty('w:right', ATable.Borders.Right);
AddTableBorderProperty('w:top', ATable.Borders.Top);
AddTableBorderProperty('w:bottom', ATable.Borders.Bottom);
AddTableBorderProperty('w:insideH', ATable.Borders.InsideHoriz);
AddTableBorderProperty('w:insideV', ATable.Borders.InsideVert);
oDocXML.Add(indNone, '</w:tblBorders>', indDec);
If Assigned(ATable.Style) Then
oDocXML.Add('<w:tblStyle w:val="' + StyleNameToStyleID(ATable.Style) + '" />');
If ATable.SpacingBetweenCells <> 0 Then
oDocXML.Add('<w:tblCellSpacing w:w="' + mmToTwipsS(ATable.SpacingBetweenCells) +
'" w:type="dxa" />');
If ATable.BackgroundColor <> FPColor(0, 0, 0, 0) Then
oDocXML.Add(Format('<w:shd w:val="clear" w:color="auto" w:fill="%s"/>',
[FPColorToRGBHexString(ATable.BackgroundColor)]));
oDocXML.Add(indDec, '</w:tblPr>', indDec);
// Define the grid. Grid is used to determine cell widths
// and boundaries
oDocXML.Add(indInc, '<w:tblGrid>', indInc);
// Percent cannot be set here, only absolutes
If ATable.ColWidthsUnits = dimMillimeter Then
For k := Low(ATable.ColWidths) To High(ATable.ColWidths) Do
oDocXML.Add(Format('<w:gridCol w:w="%s" />', [mmToTwipsS(ATable.ColWidths[k])]))
Else If ATable.ColWidthsUnits = dimPoint Then
For k := Low(ATable.ColWidths) To High(ATable.ColWidths) Do
oDocXML.Add(Format('<w:gridCol w:w="%s" />',
[IntToStr(Round(20 * ATable.ColWidths[k]))]));
oDocXML.Add(indDec, '</w:tblGrid>', indDec);
For i := 0 To ATable.GetRowCount - 1 Do
Begin
oRow := ATable.GetRow(i);
oDocXML.Add(indInc, '<w:tr>');
// Add the Row Properties
oDocXML.Add(indInc, '<w:trPr>', indInc);
If oRow.Header Then
oDocXML.Add('<w:tblHeader />');
If Not oRow.AllowSplitAcrossPage Then
oDocXML.Add('<w:cantSplit />');
If oRow.CellSpacing <> 0 Then
oDocXML.Add('<w:tblCellSpacing w:w="' + mmToTwipsS(oRow.CellSpacing) +
'" w:type="dxa" />');
{ TODO : w:hRule="exact", "auto" }
If oRow.Height <> 0 Then
oDocXML.Add('<w:trHeight w:val="' + mmToTwipsS(oRow.Height) +
'" w:hRule="atLeast"/>');
// Row Background Colour can't be applied here, have to apply to each cell in turn...
oDocXML.Add(indDec, '</w:trPr>', indDec);
For j := 0 To oRow.GetCellCount - 1 Do
Begin
oCell := oRow.GetCell(j);
oDocXML.Add(indInc, '<w:tc>');
// Add the Cell Properties
oDocXML.Add(indInc, '<w:tcPr>', indInc);
oDocXML.Add(indNone, '<w:tcBorders>', indInc);
AddTableBorderProperty('w:left', oCell.Borders.Left);
AddTableBorderProperty('w:right', oCell.Borders.Right);
AddTableBorderProperty('w:top', oCell.Borders.Top);
AddTableBorderProperty('w:bottom', oCell.Borders.Bottom);
oDocXML.Add(indNone, '</w:tcBorders>', indDec);
// Row background color can't be applied at the row level, so we'll
// apply it to each cell in turn (only if the cell doesn't have it's
// own value assigned)
If oCell.BackgroundColor <> FPColor(0, 0, 0, 0) Then
oDocXML.Add(Format('<w:shd w:val="clear" w:color="auto" w:fill="%s"/>',
[FPColorToRGBHexString(oCell.BackgroundColor)]))
Else If oRow.BackgroundColor <> FPColor(0, 0, 0, 0) Then
oDocXML.Add(Format('<w:shd w:val="clear" w:color="auto" w:fill="%s"/>',
[FPColorToRGBHexString(oRow.BackgroundColor)]));
// Either use Cell Preferred Width, or ColWidths if defined as %
If oCell.PreferredWidth.Value <> 0 Then
oDocXML.Add(Format('<w:tcW %s />', [DimAttribs(oCell.PreferredWidth)]))
Else If (j <= High(ATable.ColWidths)) Then
oDocXML.Add(Format('<w:tcW %s />',
[DimAttribs(Dimension(ATable.ColWidths[j],
ATable.ColWidthsUnits))]));
If ATable.ColWidthsUnits <> dimPercent Then
oDocXML.Add('<w:gridSpan w:val="' + IntToStr(oCell.SpannedCols) + '" />');
oDocXML.Add('<w:vAlign w:val="' + LU_V_ALIGN[oCell.VerticalAlignment] + '" />');
oDocXML.Add(indDec, '</w:tcPr>', indDec);
ProcessRichText(oCell);
oDocXML.Add(indDec, '</w:tc>');
End;
oDocXML.Add(indDec, '</w:tr>');
End;
oDocXML.Add(indDec, '</w:tbl>');
End;
(*
Procedure ProcessImage(AImage : TvImage);
begin
end;
*)
Procedure ProcessRichText(ARichText: TvRichText);
Var
i: Integer;
oEntity: TvEntity;
Begin
For i := 0 To ARichText.GetEntitiesCount - 1 Do
Begin
oEntity := ARichText.GetEntity(i);
If oEntity Is TvParagraph Then
ProcessParagraph(TvParagraph(oEntity))
Else If oEntity Is TvList Then
ProcessList(TvList(oEntity))
Else If oEntity Is TvTable Then
ProcessTable(TvTable(oEntity))
Else If oEntity Is TvRichText Then
ProcessRichText(TvRichText(oEntity))
(*
Else If oEntity Is TvImage Then
ProcessImage(TvImage(oEntity))
*)
Else
Raise Exception.Create('Unsupported entity ' + oEntity.ClassName);
End;
End;
Var
oPage: TvPage;
oPageSequence: TvTextPageSequence;
oFile: TFileInformation;
Begin
oFile := FFiles.AddXMLFile(OOXML_CONTENTTYPE_DOCUMENT, OOXML_PATH_DOCUMENT,
OOXML_TYPE_DOCUMENT, [ftContentTypes, ftRelationships]);
oDocXML := oFile.XML;
oDocXML.Clear;
oDocXML.Add(XML_HEADER);
oDocXML.Add(Format('<w:document %s>', [OOXML_DOCUMENT_NAMESPACE +
'xml:space="preserve"']));
oDocXML.Add(indInc, '<w:body>');
For iPage := 0 To FData.GetPageCount - 1 Do
Begin
oPage := FData.GetPageAsText(iPage);
If oPage Is TvTextPageSequence Then
Begin
oPageSequence := TvTextPageSequence(oPage);
ProcessRichText(oPageSequence.MainText);
// Add any dimensions, headers, footers etc
FinalisePage(oPageSequence, iPage = FData.GetPageCount - 1);
End;
End;
oDocXML.Add(indDec, '</w:body>');
oDocXML.Add('</w:document>');
End;
Function TvDOCXVectorialWriter.StyleNameToStyleID(AStyle: TvStyle): String;
Begin
If Trim(AStyle.Name) <> '' Then
Result := StringReplace(AStyle.Name, ' ', '', [rfReplaceAll, rfIgnoreCase])
Else
Begin
Result := Format('StyleID%d', [FData.FindStyleIndex(AStyle)]);
AStyle.Name := Result; // Saves having to do the FindIndex later...
End;
End;
Procedure TvDOCXVectorialWriter.PrepareTextRunStyle(ADoc: TIndentedStringList;
AStyle: TvStyle);
Begin
ADoc.Add(indInc, '<w:rPr>', indInc);
If (spbfFontName In AStyle.SetElements) And (AStyle.Font.Name <> '') Then
ADoc.Add('<w:rFonts w:ascii="' + AStyle.Font.Name + '" w:hAnsi="' +
AStyle.Font.Name + '"/>');
If spbfFontSize In AStyle.SetElements Then
{ TODO : Where does the magic Font.Size*2 come from? Confirm... }
ADoc.Add('<w:sz w:val="' + IntToStr(2 * AStyle.Font.Size) + '"/>');
If spbfFontBold In AStyle.SetElements Then
ADoc.Add('<w:b w:val="' + LU_ON_OFF[AStyle.Font.Bold] + '"/>');
If spbfFontItalic In AStyle.SetElements Then
ADoc.Add('<w:i w:val="' + LU_ON_OFF[AStyle.Font.Italic] + '"/>');
If spbfFontUnderline In AStyle.SetElements Then
ADoc.Add('<w:u w:val="' + LU_ON_OFF[AStyle.Font.Underline] + '"/>');
If spbfFontStrikeThrough In AStyle.SetElements Then
ADoc.Add('<w:strike w:val="' + LU_ON_OFF[AStyle.Font.StrikeThrough] + '"/>');
If CompareColors(AStyle.Font.Color, FPColor(0, 0, 0, 0)) <> 0 Then
ADoc.Add('<w:color w:val="' + FPColorToRGBHexString(AStyle.Font.Color) + '"/>');
// Can Word handled re-oriented text? I hope not...
If AStyle.Font.Orientation <> 0 Then;
ADoc.DecIndent;
// Don't bother adding an empty tag..
If ADoc[ADoc.Count - 1] <> '<w:rPr>' Then
ADoc.Add(indDec, '</w:rPr>')
Else
Begin
ADoc.Delete(ADoc.Count - 1);
ADoc.DecIndent;
End;
End;
Procedure TvDOCXVectorialWriter.PrepareStyles;
Var
sType, sTemp: String;
i: Integer;
oXML: TIndentedStringList;
oStyle: TvStyle;
oFile: TFileInformation;
Begin
// Only add this file if there are any styles defined...
If FData.GetStyleCount > 0 Then
Begin
oFile := FFiles.AddXMLFile(OOXML_CONTENTTYPE_STYLES, OOXML_PATH_STYLES,
OOXML_TYPE_STYLES, [ftContentTypes, ftDocRelationships]);
oXML := oFile.XML;
oXML.Clear;
oXML.Add(XML_HEADER);
oXML.Add(Format('<w:styles %s>', [OOXML_DOCUMENT_NAMESPACE]));
For i := 0 To FData.GetStyleCount - 1 Do
Begin
oStyle := FData.GetStyle(i);
If oStyle.GetKind In [vskTextBody, vskHeading] Then
sType := 'paragraph'
Else If oStyle.GetKind In [vskTextSpan] Then
sType := 'character'
Else
{ TODO : handle the other StyleKinds }
Raise Exception.Create('Unsupported StyleKind in ' + oStyle.Name);
oXML.Add(indInc, '<w:style w:type="' + sType + '" w:styleId="' +
StyleNameToStyleID(oStyle) + '">');
// Add the name and inheritance values
oXML.Add(' <w:name w:val="' + oStyle.Name + '"/>');
If Assigned(oStyle.Parent) Then
oXML.Add(' <w:basedOn w:val="' + StyleNameToStyleID(
oStyle.Parent) + '"/> ');
{ TODO : <w:qFormat/> doesn't always need to be set, but I don't yet understand the rules... }
oXML.Add(' <w:qFormat/> '); // Latent Style Primary Style Setting.
{ TODO : Specification states you CANNOT redeclare a identical property
declared in a parent. At the moment code is relying on Styles
correctly defined up through hierarchy }
If sType = 'paragraph' Then
Begin
// Add the Paragraph Properties
oXML.Add(indInc, '<w:pPr>', indInc);
// Define Before and After spacing
If (sseMarginTop In oStyle.SetElements) Or
(sseMarginBottom In oStyle.SetElements) Then
Begin
sTemp := '';
If sseMarginTop In oStyle.SetElements Then
sTemp := sTemp + ' w:before="' + mmToTwipsS(oStyle.MarginTop) + '"';
If sseMarginBottom In oStyle.SetElements Then
sTemp := sTemp + ' w:after="' + mmToTwipsS(oStyle.MarginBottom) + '"';
oXML.Add(Format('<w:spacing %s/>', [sTemp]));
End;
If (sseMarginLeft In oStyle.SetElements) Or
(sseMarginRight In oStyle.SetElements) Then
Begin
sTemp := '';
If sseMarginLeft In oStyle.SetElements Then
sTemp := sTemp + ' w:left="' + mmToTwipsS(oStyle.MarginLeft) + '"';
If sseMarginRight In oStyle.SetElements Then
sTemp := sTemp + ' w:right="' + mmToTwipsS(oStyle.MarginRight) + '"';
oXML.Add(Format('<w:ind %s/>', [sTemp]));
End;
// Alignment
If spbfAlignment In oStyle.SetElements Then
oXML.Add('<w:jc w:val="' + LU_ALIGN[oStyle.Alignment] + '"/>');
// Suppress Spacing between identical paragraphs...
If oStyle.SuppressSpacingBetweenSameParagraphs Then
oXML.Add('<w:contextualSpacing/>');
oXML.DecIndent;
If oXML[oXML.Count - 1] <> '<w:pPr>' Then
oXML.Add(indDec, '</w:pPr>')
Else
Begin
oXML.Delete(oXML.Count - 1);
oXML.DecIndent;
End;
End;
// Now add the actual formatting (rPr = Run Properties).
PrepareTextRunStyle(oXML, oStyle);
oXML.Add(indDec, '</w:style> ');
End;
oXML.Add('</w:styles>');
End;
End;
Procedure TvDOCXVectorialWriter.PrepareNumbering;
Var
oXML: TIndentedStringList;
oStyle: TvListStyle;
oFile: TFileInformation;
i: Integer;
j: Integer;
oListLevelStyle: TvListLevelStyle;
sTotalLeader: String;
sCurrentLeader: String;
slvlText: String;
Begin
// Only add this file if there are any List styles defined...
If FData.GetListStyleCount > 0 Then
Begin
oFile := FFiles.AddXMLFile(OOXML_CONTENTTYPE_NUMBERING,
OOXML_PATH_NUMBERING, OOXML_TYPE_NUMBERING, [ftContentTypes, ftDocRelationships]);
oXML := oFile.XML;
oXML.Clear;
oXML.Add(XML_HEADER);
oXML.Add(Format('<w:numbering %s>', [OOXML_DOCUMENT_NAMESPACE]));
For i := 0 To FData.GetListStyleCount - 1 Do
Begin
oStyle := FData.GetListStyle(i);
// abstractNumID allows us to group different list styles together.
// The way fpvectorial uses it, there will be a one to one relationship
// between abstractNumID and numID.
// abstractNumId is 0 based
// numID is 1 based. Go figure...
oXML.Add(indInc, Format('<w:abstractNum w:abstractNumId="%d">', [i]), indInc);
sTotalLeader := '';
For j := 0 To oStyle.GetListLevelStyleCount-1 Do
Begin
oListLevelStyle := oStyle.GetListLevelStyle(j);
oXML.Add(Format('<w:lvl w:ilvl="%d">', [oListLevelStyle.Level]), indInc);
with oListLevelStyle do
sCurrentLeader := Format('%s%s%d%s', [Prefix, '%', Level + 1, Suffix]);
sTotalLeader := sTotalLeader + sCurrentLeader;
If oListLevelStyle.Kind=vlskBullet Then
slvlText := oListLevelStyle.Bullet
Else If oListLevelStyle.DisplayLevels Then
slvlText := sTotalLeader
Else
slvlText := sCurrentLeader;
If oListLevelStyle.Kind=vlskBullet Then
oXML.Add('<w:numFmt w:val="bullet"/>')
Else
Begin // Numbered Lists
oXML.Add(Format('<w:start w:val="%d"/>', [oListLevelStyle.Start]));
oXML.Add('<w:numFmt w:val="' + LU_NUMBERFORMAT[oListLevelStyle.NumberFormat] + '"/>');
End;
oXML.Add('<w:lvlText w:val="' + slvlText + '"/>');
oXML.Add('<w:lvlJc w:val="' + LU_ALIGN[oListLevelStyle.Alignment] + '"/>');
oXML.Add('<w:pPr>');
oXML.Add(Format(' <w:ind w:left="%s" w:hanging="%s"/>',
[mmToTwipsS(oListLevelStyle.MarginLeft), mmToTwipsS(oListLevelStyle.HangingIndent)]));
oXML.Add('</w:pPr>');
oXML.Add('<w:rPr>');
oXML.Add(Format(' <w:rFonts w:ascii="%s" w:hAnsi="%s"/>',
[oListLevelStyle.LeaderFontName, oListLevelStyle.LeaderFontName]));
oXML.Add('</w:rPr>');
oXML.Add('</w:lvl>', indDec);
end;
oXML.Add(indDec, '</w:abstractNum>', indDec);
End;
For i := 0 To FData.GetListStyleCount - 1 Do
begin
oXML.Add(indInc, Format('<w:num w:numId="%d">', [i + 1]));
oXML.Add(Format(' <w:abstractNumId w:val="%d"/>', [i]));
oXML.Add(indDec, '</w:num>');
end;
oXML.Add('</w:numbering>');
End;
End;
Procedure TvDOCXVectorialWriter.WriteToFile(AFileName: String;
AData: TvVectorialDocument);
Var
oStream: TFileStream;
Begin
If ExtractFileExt(AFilename) = '' Then
AFilename := AFilename + STR_DOCX_EXTENSION;
oStream := TFileStream.Create(AFileName, fmCreate);
Try
WriteToStream(oStream, AData);
Finally
FreeAndNil(oStream);
End;
End;
Procedure TvDOCXVectorialWriter.WriteToStream(AStream: TStream;
AData: TvVectorialDocument);
Var
oContentTypes, oRelsRels, oDocumentRels: TStringStream;
oZip: TZipper;
i: Integer;
Begin
FData := AData;
PrepareStyles;
PrepareDocument;
PrepareNumbering;
// These documents need building up with details of all included files,
// not worth the overhead of handling as StringLists
oContentTypes := TStringStream.Create(PrepareContentTypes);
oRelsRels := TStringStream.Create(PrepareRelationships);
oDocumentRels := TStringStream.Create(PrepareDocRelationships);
oZip := TZipper.Create;
Try
oZip.Entries.AddFileEntry(oContentTypes, OOXML_PATH_TYPES);
oZip.Entries.AddFileEntry(oRelsRels, OOXML_PATH_RELS_RELS);
oZip.Entries.AddFileEntry(oDocumentRels, OOXML_PATH_DOCUMENT_RELS);
For i := 0 To FFiles.Count - 1 Do
oZip.Entries.AddFileEntry(FFiles[i].Stream, FFiles[i].Path);
oZip.SaveToStream(AStream);
Finally
oZip.Free;
For i := 0 To FFiles.Count - 1 Do
FFiles[i].FreeStream;
oContentTypes.Free;
oRelsRels.Free;
oDocumentRels.Free;
End;
End;
Initialization
RegisterVectorialWriter(TvDOCXVectorialWriter, vfDOCX);
End.