mirror of
				https://gitlab.com/freepascal.org/lazarus/lazarus.git
				synced 2025-10-31 05:21:28 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			1388 lines
		
	
	
		
			42 KiB
		
	
	
	
		
			ObjectPascal
		
	
	
	
	
	
			
		
		
	
	
			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.
 | 
