diff --git a/components/fpvectorial/docxvectorialwriter.pas b/components/fpvectorial/docxvectorialwriter.pas index 6731603f12..296e4957a2 100644 --- a/components/fpvectorial/docxvectorialwriter.pas +++ b/components/fpvectorial/docxvectorialwriter.pas @@ -2,6 +2,9 @@ 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 @@ -10,10 +13,10 @@ THIS IS A UNIT CURRENTLY UNDER DEVELOPEMENT - Supports Section Breaks (via PageSequences) - Supports Header and Footer - Supports Portrait/Landscape + - Support Tables TODO - Add following to both FPVectorial AND DOCXWriter - - Add Table Support - 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 @@ -25,15 +28,20 @@ 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\document.xml - this is the main document. Conforms to WordprocessingML 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 obtained from: +Specifications and examples obtained from: http://openxmldeveloper.org/default.aspx -Office Open XML Part 4 - Markup Language Reference.docx - - First edition, downloaded from http://www.ecma-international.org/publications/standards/Ecma-376.htm +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 @@ -65,6 +73,14 @@ Change History - 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; @@ -77,21 +93,25 @@ Uses Classes, SysUtils, zipper, {NOTE: might require zipper from FPC 2.6.2+ } fpimage, fpcanvas, - fpvectorial, fpvutils, lazutf8; + 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(bIndent: Boolean; Const S: String): Integer; + Function Add(Const S: String; indAfter: TIndentOption): Integer; Procedure IncIndent; Procedure DecIndent; @@ -114,7 +134,7 @@ Type MentionedIn: TFileTypes; XML: TIndentedStringList; // Free'd internally; - //Image : TFPImage; { TODO : How are we going to handle images? } + //Image : TFPImage; { TODO: How are we going to handle images? } Constructor Create; Destructor Destroy; Override; @@ -128,8 +148,6 @@ Type // This creates a TSream, Call .FreeStream to free End; - { TODO : Can this be tidied with Generics? } - { TFileList } TFileList = Class(TObject) @@ -161,7 +179,9 @@ Type Function PrepareDocRelationships: String; Procedure PrepareDocument; - Procedure PrepareStyles; + 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 @@ -172,10 +192,6 @@ Type Procedure WriteToStream(AStream: TStream; AData: TvVectorialDocument); Override; End; -// Generic Helper Units -Function PointsToTwipsS(APoints: Double): String; -Function mmToTwipsS(AMillimetres: Double): String; - Implementation Uses @@ -193,18 +209,21 @@ Const 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 = @@ -214,18 +233,69 @@ Const TAG_HEADER = 'hdr'; TAG_FOOTER = 'ftr'; + // Lookups... + LU_ALIGN: Array [TvStyleAlignment] Of String = + ('left', 'right', 'both', 'center'); + + LU_KIND: Array [TvListStyleKind] Of String = + ('bullet', 'decimal', 'lowerLetter', 'lowerRoman', + 'upperLetter', 'upperRoman'); + + 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; @@ -345,20 +415,30 @@ Begin 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(bIndent: Boolean; Const S: String): Integer; +Function TIndentedStringList.Add(Const S: String; indAfter: TIndentOption): Integer; Begin - If bIndent Then - IncIndent; - - Result := Inherited Add(FIndent + S); - - If Not bIndent Then - DecIndent; + Result := Add(indNone, S, indAfter); End; Procedure TIndentedStringList.DecIndent; @@ -447,19 +527,7 @@ Var // Generally this is document.xml, may also be header.xml or footer.xml though.. oDocXML: TIndentedStringList; - Procedure AddParagraphProperties(AStyle: TvStyle); - Begin - oDocXML.Add(True, ''); - oDocXML.Add(' '); - oDocXML.Add(False, ''); - End; - - Procedure AddRunProperties(AStyle: TvStyle); - Begin - oDocXML.Add(True, ''); - oDocXML.Add(' '); - oDocXML.Add(False, ''); - End; + Procedure ProcessRichText(ARichText: TvRichText); Forward; Procedure AddTextRun(sText: String; AStyle: TvStyle); Var @@ -477,31 +545,35 @@ Var // and render the Tabs and CRs appropriately While i <= iLen Do Begin - If (sText[i] In [#10, #11, #13]) Or (i = iLen) Then + 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, #11, #13]) Then + If (i = iLen) And Not (sText[i] In [#10, #09, #13]) Then Inc(i); sTemp := Copy(sText, iStart, i - iStart); - oDocXML.Add(True, ''); + oDocXML.Add(indInc, ''); If Assigned(AStyle) Then - AddRunProperties(AStyle); + Begin + oDocXML.Add(indInc, ''); + oDocXML.Add(' '); + oDocXML.Add(indDec, ''); + End; oDocXML.Add(' ' + EscapeHTML(sTemp) + ''); - oDocXML.Add(False, ''); + oDocXML.Add(indDec, ''); End; // Deal with the Tabs, LF and CRs appropriately - If sText[i] = #11 Then + If sText[i] = #09 Then oDocXML.Add(' ') - Else If sText[i] In [#10, #13] Then + Else If sText[i] In [#10, #11, #13] Then Begin oDocXML.Add(' '); @@ -525,10 +597,27 @@ Var oEntity: TvEntity; sTemp: String; Begin - oDocXML.Add(True, ''); + oDocXML.Add(indInc, ''); + + // Add the Paragraph Properties + oDocXML.Add(indInc, '', indInc); If Assigned(AParagraph.Style) Then - AddParagraphProperties(AParagraph.Style); + oDocXML.Add(Format('', + [StyleNameToStyleID(AParagraph.Style)])); + + If AParagraph.UseLocalAlignment Then + oDocXML.Add(''); + + If Assigned(AParagraph.ListStyle) Then + Begin + oDocXML.Add(''); + oDocXML.Add(indInc, Format('', [AParagraph.ListStyle.Level])); + oDocXML.Add(indDec, ''); // wtf is numID?? + oDocXML.Add(''); + End; + + oDocXML.Add(indDec, '', indDec); For i := 0 To AParagraph.GetEntitiesCount - 1 Do Begin @@ -554,20 +643,25 @@ Var Raise Exception.Create('Unsupported Entity: ' + oEntity.ClassName); End; - oDocXML.Add(False, ''); + oDocXML.Add(indDec, ''); End; - Procedure ProcessRichText(ARichText: TvRichText); + Procedure ProcessBulletList(ABulletList: TvBulletList); Var i: Integer; oEntity: TvEntity; Begin - For i := 0 To ARichText.GetEntitiesCount - 1 Do + For i := 0 To ABulletList.GetEntitiesCount - 1 Do Begin - oEntity := ARichText.GetEntity(i); + oEntity := ABulletList.GetEntity(i); If oEntity Is TvParagraph Then - ProcessParagraph(TvParagraph(oEntity)) + Begin + If Not Assigned(TvParagraph(oEntity).Style) Then + TvParagraph(oEntity).Style := ABulletList.Style; + + ProcessParagraph(TvParagraph(oEntity)); + End Else Raise Exception.Create('Unsupported entity ' + oEntity.ClassName); End; @@ -624,12 +718,11 @@ Var // For the final pagesequence only w:sectPr shouldn't be wrapped inside w:p or w:pPr If Not ALastPage Then Begin - oDocXML.Add(True, ''); - oDocXML.Add(True, ''); + oDocXML.Add(indInc, ''); + oDocXML.Add(indInc, ''); End; - oDocXML.Add(True, ''); - oDocXML.IncIndent; + oDocXML.Add(indInc, '', indInc); ProcessHeaderFooter(APageSequence.Header, TAG_HEADER); ProcessHeaderFooter(APageSequence.Footer, TAG_FOOTER); @@ -660,21 +753,215 @@ Var // // // - oDocXML.DecIndent; - oDocXML.Add(False, ''); + oDocXML.Add(indDec, '', indDec); If Not ALastPage Then Begin - oDocXML.Add(False, ''); - oDocXML.Add(False, ''); + oDocXML.Add(indDec, ''); + oDocXML.Add(indDec, ''); + 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, ''); + + // Add the table properties + oDocXML.Add(indInc, '', indInc); + + If ATable.PreferredWidth.Value <> 0 Then + oDocXML.Add(Format('', [DimAttribs(ATable.PreferredWidth)])); + + oDocXML.Add(indNone, '', 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, '', indDec); + + If Assigned(ATable.Style) Then + oDocXML.Add(''); + + If ATable.CellSpacing <> 0 Then + oDocXML.Add(''); + + If ATable.BackgroundColor <> FPColor(0, 0, 0, 0) Then + oDocXML.Add(Format('', + [FPColorToRGBHexString(ATable.BackgroundColor)])); + + oDocXML.Add(indDec, '', indDec); + + // Define the grid. Grid is used to determine cell widths + // and boundaries + oDocXML.Add(indInc, '', 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('', [mmToTwipsS(ATable.ColWidths[k])])) + Else If ATable.ColWidthsUnits = dimPoint Then + For k := Low(ATable.ColWidths) To High(ATable.ColWidths) Do + oDocXML.Add(Format('', + [IntToStr(Round(20 * ATable.ColWidths[k]))])); + + oDocXML.Add(indDec, '', indDec); + + For i := 0 To ATable.GetRowCount - 1 Do + Begin + oRow := ATable.GetRow(i); + oDocXML.Add(indInc, ''); + + // Add the Row Properties + oDocXML.Add(indInc, '', indInc); + + If oRow.Header Then + oDocXML.Add(''); + + If Not oRow.AllowSplitAcrossPage Then + oDocXML.Add(''); + + If oRow.CellSpacing <> 0 Then + oDocXML.Add(''); + + { TODO : w:hRule="exact", "auto" } + If oRow.Height <> 0 Then + oDocXML.Add(''); + + // Row Background Colour can't be applied here, have to apply to each cell in turn... + + oDocXML.Add(indDec, '', indDec); + + For j := 0 To oRow.GetCellCount - 1 Do + Begin + oCell := oRow.GetCell(j); + + oDocXML.Add(indInc, ''); + + // Add the Cell Properties + oDocXML.Add(indInc, '', indInc); + + oDocXML.Add(indNone, '', 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, '', 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('', + [FPColorToRGBHexString(oCell.BackgroundColor)])) + Else If oRow.BackgroundColor <> FPColor(0, 0, 0, 0) Then + oDocXML.Add(Format('', + [FPColorToRGBHexString(oRow.BackgroundColor)])); + + // Either use Cell Preferred Width, or ColWidths if defined as % + If oCell.PreferredWidth.Value <> 0 Then + oDocXML.Add(Format('', [DimAttribs(oCell.PreferredWidth)])) + Else If (j <= High(ATable.ColWidths)) Then + oDocXML.Add(Format('', + [DimAttribs(Dimension(ATable.ColWidths[j], + ATable.ColWidthsUnits))])); + + If ATable.ColWidthsUnits <> dimPercent Then + oDocXML.Add(''); + + oDocXML.Add(''); + + oDocXML.Add(indDec, '', indDec); + + ProcessRichText(oCell); + + oDocXML.Add(indDec, ''); + End; + + oDocXML.Add(indDec, ''); + End; + + oDocXML.Add(indDec, ''); + 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 TvBulletList Then + ProcessBulletList(TvBulletList(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; - oPageEntity: TvEntity; - iPage, i: Integer; + iPage: Integer; oFile: TFileInformation; Begin oFile := FFiles.AddXMLFile(OOXML_CONTENTTYPE_DOCUMENT, OOXML_PATH_DOCUMENT, @@ -686,7 +973,7 @@ Begin oDocXML.Add(Format('', [OOXML_DOCUMENT_NAMESPACE + 'xml:space="preserve"'])); - oDocXML.Add(True, ''); + oDocXML.Add(indInc, ''); For iPage := 0 To FData.GetPageCount - 1 Do Begin @@ -696,26 +983,14 @@ Begin Begin oPageSequence := TvTextPageSequence(oPage); - // Process the page contents - For i := 0 To oPageSequence.GetEntitiesCount - 1 Do - Begin - oPageEntity := oPageSequence.GetEntity(i); - - If oPageEntity Is TvParagraph Then - ProcessParagraph(TvParagraph(oPageEntity)) - Else If oPageEntity Is TvRichText Then - ProcessRichText(TvRichText(oPageEntity)) - Else - { TODO : What other entities in TvTextPageSequence do we need to process? } - Raise Exception.Create('Unsupported Entity: ' + oPageEntity.ClassName); - End; + ProcessRichText(oPageSequence.MainText); // Add any dimensions, headers, footers etc FinalisePage(oPageSequence, iPage = FData.GetPageCount - 1); End; End; - oDocXML.Add(False, ''); + oDocXML.Add(indDec, ''); oDocXML.Add(''); End; @@ -732,12 +1007,11 @@ End; Procedure TvDOCXVectorialWriter.PrepareTextRunStyle(ADoc: TIndentedStringList; AStyle: TvStyle); -Const - BoolAsString: Array[Boolean] Of String = ('off', 'on'); +Var + sTemp: String; Begin - ADoc.Add(True, ''); - ADoc.IncIndent; + ADoc.Add(indInc, '', indInc); If (spbfFontName In AStyle.SetElements) And (AStyle.Font.Name <> '') Then ADoc.Add(''); If spbfFontBold In AStyle.SetElements Then - ADoc.Add(''); + ADoc.Add(''); If spbfFontItalic In AStyle.SetElements Then - ADoc.Add(''); + ADoc.Add(''); If spbfFontUnderline In AStyle.SetElements Then - ADoc.Add(''); + ADoc.Add(''); If spbfFontStrikeThrough In AStyle.SetElements Then - ADoc.Add(''); + ADoc.Add(''); If CompareColors(AStyle.Font.Color, FPColor(0, 0, 0, 0)) <> 0 Then ADoc.Add(''); @@ -769,7 +1043,7 @@ Begin // Don't bother adding an empty tag.. If ADoc[ADoc.Count - 1] <> '' Then - ADoc.Add(False, '') + ADoc.Add(indDec, '') Else Begin ADoc.Delete(ADoc.Count - 1); @@ -779,107 +1053,172 @@ End; Procedure TvDOCXVectorialWriter.PrepareStyles; Var - oStyleXML: TIndentedStringList; - - Procedure PrepareParagraphStyle(AStyle: TvStyle; AType: String); - Const - AlignmentAsString: Array [TvStyleAlignment] Of String = - ('left', 'right', 'both', 'center'); - Var - sTemp: String; - Begin - oStyleXML.Add(True, ''); - - // Add the name and inheritance values - oStyleXML.Add(' '); - - If Assigned(AStyle.Parent) Then - oStyleXML.Add(' '); - - { TODO : doesn't always need to be set, but I don't yet understand the rules... } - oStyleXML.Add(' '); // 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 AType = 'paragraph' Then - Begin - // Add the Paragraph Properties - oStyleXML.Add(True, ''); - oStyleXML.IncIndent; - - sTemp := ''; - If AStyle.MarginTop <> 0 Then - sTemp := sTemp + ' w:before="' + mmToTwipsS(AStyle.MarginTop) + '"'; - - If AStyle.MarginBottom <> 0 Then - sTemp := sTemp + ' w:after="' + mmToTwipsS(AStyle.MarginBottom) + '"'; - - If sTemp <> '' Then - oStyleXML.Add(''); - - sTemp := ''; - If AStyle.MarginLeft <> 0 Then - sTemp := sTemp + ' w:left="' + mmToTwipsS(AStyle.MarginLeft) + '"'; - - If AStyle.MarginRight <> 0 Then - sTemp := sTemp + ' w:right="' + mmToTwipsS(AStyle.MarginRight) + '"'; - - If sTemp <> '' Then - oStyleXML.Add(''); - - If spbfAlignment In AStyle.SetElements Then - oStyleXML.Add(''); - - oStyleXML.DecIndent; - - If oStyleXML[oStyleXML.Count - 1] <> '' Then - oStyleXML.Add(False, '') - Else - Begin - oStyleXML.Delete(oStyleXML.Count - 1); - oStyleXML.DecIndent; - End; - End; - - // Now add the actual formatting (rPr = Run Properties). - PrepareTextRunStyle(oStyleXML, AStyle); - - oStyleXML.Add(False, ' '); - End; - -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]); - oStyleXML := oFile.XML; + oXML := oFile.XML; - oStyleXML.Clear; - oStyleXML.Add(XML_HEADER); - oStyleXML.Add(Format('', [OOXML_DOCUMENT_NAMESPACE])); + oXML.Clear; + oXML.Add(XML_HEADER); + oXML.Add(Format('', [OOXML_DOCUMENT_NAMESPACE])); For i := 0 To FData.GetStyleCount - 1 Do Begin oStyle := FData.GetStyle(i); If oStyle.GetKind In [vskTextBody, vskHeading] Then - PrepareParagraphStyle(oStyle, 'paragraph') + sType := 'paragraph' Else If oStyle.GetKind In [vskTextSpan] Then - PrepareParagraphStyle(oStyle, 'character') + sType := 'character' Else { TODO : handle the other StyleKinds } Raise Exception.Create('Unsupported StyleKind in ' + oStyle.Name); + + oXML.Add(indInc, ''); + + // Add the name and inheritance values + oXML.Add(' '); + + If Assigned(oStyle.Parent) Then + oXML.Add(' '); + + { TODO : doesn't always need to be set, but I don't yet understand the rules... } + oXML.Add(' '); // 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, '', 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('', [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('', [sTemp])); + End; + + + // Alignment + If spbfAlignment In oStyle.SetElements Then + oXML.Add(''); + + // Suppress Spacing between identical paragraphs... + If oStyle.SuppressSpacingBetweenSameParagraphs Then + oXML.Add(''); + + oXML.DecIndent; + + If oXML[oXML.Count - 1] <> '' Then + oXML.Add(indDec, '') + 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, ' '); End; - oStyleXML.Add(''); + oXML.Add(''); + End; +End; + +Procedure TvDOCXVectorialWriter.PrepareNumbering; +Var + oXML: TIndentedStringList; + oStyle: TvListStyle; + oFile: TFileInformation; + i: Integer; +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('', [OOXML_DOCUMENT_NAMESPACE])); + + // wtf is abstractNumId?? + oXML.Add(indInc, '', indInc); + + // Optional + //oXML.Add(''); + + For i := 0 To FData.GetListStyleCount - 1 Do + Begin + oStyle := FData.GetListStyle(i); + + oXML.Add(Format('', [oStyle.Level]), indInc); + + oXML.Add(''); // Numbered lists only + oXML.Add(''); + oXML.Add(''); + oXML.Add(''); + + oXML.Add(''); + oXML.Add(Format(' ', + [mmToTwipsS(oStyle.MarginLeft), mmToTwipsS(oStyle.HangingIndent)])); + oXML.Add(''); + + oXML.Add(''); + oXML.Add(Format(' ', + [oStyle.PrefixFontName, oStyle.PrefixFontName])); + oXML.Add(''); + + oXML.Add('', indDec); + End; + oXML.Add(indDec, '', indDec); + + // wtf is abstrctNumID?? + // obviously related to w:abstractNum above... + oXML.Add(indInc, ''); + oXML.Add(' '); + oXML.Add(indDec, ''); + + oXML.Add(''); End; End; @@ -910,6 +1249,7 @@ Begin PrepareStyles; PrepareDocument; + PrepareNumbering; // These documents need building up with details of all included files, // not worth the overhead of handling as StringLists diff --git a/components/fpvectorial/examples/fpvtextwritetest2.lpi b/components/fpvectorial/examples/fpvtextwritetest2.lpi index 035353e92c..729dd911a4 100644 --- a/components/fpvectorial/examples/fpvtextwritetest2.lpi +++ b/components/fpvectorial/examples/fpvtextwritetest2.lpi @@ -1,4 +1,4 @@ - + @@ -39,12 +39,17 @@ - + + + + + + @@ -55,10 +60,12 @@ + + diff --git a/components/fpvectorial/examples/fpvtextwritetest2.pas b/components/fpvectorial/examples/fpvtextwritetest2.pas index b1f32ade6e..e99d26a061 100644 --- a/components/fpvectorial/examples/fpvtextwritetest2.pas +++ b/components/fpvectorial/examples/fpvtextwritetest2.pas @@ -1,7 +1,8 @@ { FPVectorial example application for writing a text document file to disk. -Author: Felipe Monteiro de Carvalho +Author: Mike Thompson + Felipe Monteiro de Carvalho License: Public Domain } @@ -14,16 +15,30 @@ Uses odtvectorialwriter, fpvutils, fpvectorialpkg, - SysUtils; + docxvectorialwriter, + SysUtils, FPImage; {$R *.res} +Const + ONE_POINT_IN_MM = 0.35278; + Var Vec: TvVectorialDocument; Page: TvTextPageSequence; CurParagraph: TvParagraph; - BoldStyle: TvStyle; - CenterStyle: TvStyle; + BoldTextStyle: TvStyle; + CenterParagraphStyle, Center2: TvStyle; + BulletList : TvBulletList; + dtTime : TDateTime; + CurText : TvText; + + CurTable : TvTable; + CurRow : TvTableRow; + CurCell : TvTableCell; + + i, j, iMax : Integer; + Begin Vec := TvVectorialDocument.Create; Try @@ -31,22 +46,25 @@ Begin Vec.Width := 210; Vec.Height := 297; - Vec.AddStandardODTTextDocumentStyles(); + // Until there is a need, we will stick with supporting ODT styles + Vec.AddStandardTextDocumentStyles(vfODT); + + Vec.StyleTextBody.MarginRight:=10; + Vec.StyleTextBody.SetElements:= Vec.StyleTextBody.SetElements + [sseMarginRight]; // Until a Template is available, create the Bold Style ourselves - BoldStyle := Vec.AddStyle(); + BoldTextStyle := Vec.AddStyle(); - // This implies this style should not be applied to Paragraphs - BoldStyle.Kind := vskTextSpan; - BoldStyle.Name := 'Bold'; - BoldStyle.Font.Bold := True; - BoldStyle.SetElements := BoldStyle.SetElements + [spbfFontBold]; + BoldTextStyle.Kind := vskTextSpan; // This implies this style should not be applied to Paragraphs + BoldTextStyle.Name := 'Bold'; + BoldTextStyle.Font.Bold := True; + BoldTextStyle.SetElements := BoldTextStyle.SetElements + [spbfFontBold]; - CenterStyle := Vec.AddStyle(); - CenterStyle.ApplyOver(Vec.StyleTextBody); - CenterStyle.Name := 'Text Body Centered'; - CenterStyle.Alignment := vsaCenter; - CenterStyle.SetElements := CenterStyle.SetElements + [spbfAlignment]; + CenterParagraphStyle := Vec.AddStyle(); + CenterParagraphStyle.ApplyOver(Vec.StyleTextBody); + CenterParagraphStyle.Name := 'Text Body Centered'; + CenterParagraphStyle.Alignment := vsaCenter; + CenterParagraphStyle.SetElements := CenterParagraphStyle.SetElements + [spbfAlignment]; // First page sequence Page := Vec.AddTextPageSequence(); @@ -55,15 +73,15 @@ Begin // Set the Header CurParagraph := Page.Header.AddParagraph; - CurParagraph.Style := CenterStyle; - CurParagraph.AddText('Introduction to Lazarus and FreePascal').Style := BoldStyle; + CurParagraph.Style := CenterParagraphStyle; + CurParagraph.AddText('Introduction to Lazarus and FreePascal').Style := BoldTextStyle; // Set the Footer CurParagraph := Page.Footer.AddParagraph; - CurParagraph.Style := CenterStyle; - CurParagraph.AddText('Confidential' + #11 + 'Page x of y' + #11 + + CurParagraph.Style := CenterParagraphStyle; + CurParagraph.AddText('Confidential' + #09 + 'Page x of y' + #09 + DateTimeToStr(Now)).Style := - BoldStyle; + BoldTextStyle; // Title CurParagraph := Page.AddParagraph(); @@ -75,7 +93,7 @@ Begin CurParagraph.Style := Vec.StyleTextBody; With CurParagraph Do Begin - AddText('Lazarus ').Style := BoldStyle; + AddText('Lazarus ').Style := BoldTextStyle; // Adding the Paragraph as a long string AddText('is a free and open source development tool for the ' + 'Free Pascal compiler, which is also free and open source.'); @@ -98,12 +116,12 @@ Begin // Adding the Paragraph as a series of TvText's // trailing space required // Each TvText gets added as it's own text run inside the Word Doc - AddText('Lazarus ').Style := BoldStyle; + AddText('Lazarus ').Style := BoldTextStyle; AddText('is a free cross-platform visual integrated development '); AddText('environment (IDE) for rapid application development (RAD) '); AddText('using the Free Pascal compiler supported dialects of Object '); AddText('Pascal. Developers use '); - AddText('Lazarus ').Style := BoldStyle; + AddText('Lazarus ').Style := BoldTextStyle; AddText('to create native code console '); AddText('and graphical user interface (GUI) applications for the desktop '); AddText('along with mobile devices, web applications, web services, '); @@ -112,6 +130,22 @@ Begin AddText('compiler supports( Mac, Unix, Linux, Windows, etc). '); End; + BulletList := Page.AddBulletList(); + BulletList.Style := Vec.StyleList; + BulletList.AddItem(0, 'A What You See Is What You Get (WYSIWYG) visual windows layout designer'); + BulletList.AddItem(1, 'An extensive set of GUI widgets or visual components such as edit boxes, buttons, dialogs, menus, etc.'); + BulletList.AddItem(2, 'An extensive set of non visual components for common behaviors such as persistence of application settings'); + BulletList.AddItem(3, 'A set of data connectivity components for MySQL, PostgresSQL, FireBird, Oracle, SQL Lite, Sybase, and others'); + BulletList.AddItem(4, 'Data aware widget set that allows the developer to see data in visual components in the designer to assist with development'); + BulletList.AddItem(5, 'Interactive code debugger'); + BulletList.AddItem(5, 'Code completion'); + BulletList.AddItem(4, 'Code templates'); + BulletList.AddItem(3, 'Syntax highlighting'); + BulletList.AddItem(2, 'Context sensitive help'); + BulletList.AddItem(1, 'Text resource manager for internationalization'); + BulletList.AddItem(0, 'Automatic code formatting'); + BulletList.AddItem(0, 'The ability to create custom components'); + // Empty line CurParagraph := Page.AddParagraph(); CurParagraph.Style := Vec.StyleTextBody; @@ -124,27 +158,255 @@ Begin // Set the Header CurParagraph := Page.Header.AddParagraph; - CurParagraph.Style := CenterStyle; - CurParagraph.AddText('Testing Concepts').Style := BoldStyle; + CurParagraph.Style := CenterParagraphStyle; + CurParagraph.AddText('Testing Concepts').Style := BoldTextStyle; // Title CurParagraph := Page.AddParagraph(); CurParagraph.Style := Vec.StyleHeading2; CurParagraph.AddText('Testing Strings'); + // Test for XML tags CurParagraph := Page.AddParagraph(); CurParagraph.Style := Vec.StyleTextBody; + CurText := CurParagraph.AddText(''); // Adding to the Paragraph by extending the TStringList inside a single TvText // Each line will be added inside a new text run inside the Word Doc // with a Soft Return inserted at the end of each line - With CurParagraph.AddText('').Value Do + With CurText.Value Do Begin - Add(#11 + '&"This shouldn''t break the resulting document."' + #11); - Add(#11 + '!@#$%^&*()_+=-~`;:{}[],./|\?' + #11); + Add(#09 + '&"This shouldn''t break the resulting document."' + #09); + Add(#09 + '!@#$%^&*()_+=-~`;:{}[],./|\?' + #09); End; + // Third page sequence + Page := Vec.AddTextPageSequence(); + Page.Height := 297; // back to Portrait + Page.Width := 210; + + // Set the Header + CurParagraph := Page.Header.AddParagraph; + CurParagraph.Style := CenterParagraphStyle; + CurParagraph.AddText('Testing Tables').Style := BoldTextStyle; + + // Title + CurParagraph := Page.AddParagraph(); + CurParagraph.Style := Vec.StyleHeading2; + CurParagraph.AddText('Manual Table'); + + CurTable := Page.AddTable; + CurTable.PreferredWidth := Dimension(100, dimPercent); + + CurTable.CellSpacing := 0; + CurTable.Borders.Left.Width := 2 * ONE_POINT_IN_MM; + CurTable.Borders.Right.Width := 2 * ONE_POINT_IN_MM; + CurTable.Borders.Top.Width := 2 * ONE_POINT_IN_MM; + CurTable.Borders.Bottom.Width := 2 * ONE_POINT_IN_MM; + CurTable.Borders.InsideHoriz.LineType:=tbtSingle; + CurTable.Borders.InsideVert.LineType :=tbtDashed; + + CurRow := CurTable.AddRow; + CurRow.BackgroundColor := RGBToFPColor(192, 192, 192); + CurRow.Header := True; + + CurCell := CurRow.AddCell; + CurParagraph := CurCell.AddParagraph; + CurParagraph.AddText('First Cell, First Row'); + + CurCell := CurRow.AddCell; + CurParagraph := CurCell.AddParagraph; + CurParagraph.AddText('Second Cell, First Row'); + + CurCell := CurRow.AddCell; + CurParagraph := CurCell.AddParagraph; + CurParagraph.AddText('Third Cell, First Row'); + + CurRow := CurTable.AddRow; + CurRow.CellSpacing := ONE_POINT_IN_MM; + CurCell := CurRow.AddCell; + CurParagraph := CurCell.AddParagraph; + CurParagraph.AddText('First Cell, Second Row'); + + CurCell := CurRow.AddCell; + CurCell.Borders.Left.LineType := tbtDouble; + CurCell.Borders.Left.Color := RGBToFPColor(255, 0, 0); + CurCell.Borders.Right.LineType := tbtDouble; + CurCell.Borders.Right.Color := RGBToFPColor(255, 0, 0); + CurCell.Borders.Top.LineType := tbtDouble; + CurCell.Borders.Top.Color := RGBToFPColor(255, 0, 0); + CurCell.Borders.Bottom.LineType := tbtDouble; + CurCell.Borders.Bottom.Color := RGBToFPColor(255, 0, 0); + + CurCell.VerticalAlignment:=vaCenter; + + CurParagraph := CurCell.AddParagraph; + CurParagraph.Style := CenterParagraphStyle; + CurParagraph.AddText('Second Cell, Second Row'+#13+'This should have a red double border'); + + CurCell := CurRow.AddCell; + CurParagraph := CurCell.AddParagraph; + CurParagraph.AddText('Third Cell, Second Row'); + + CurRow := CurTable.AddRow; + CurCell := CurRow.AddCell; + CurParagraph := CurCell.AddParagraph; + CurParagraph.AddText('First Cell, Third Row'); + + CurCell := CurRow.AddCell; + CurParagraph := CurCell.AddParagraph; + CurParagraph.AddText('Second Cell, Third Row'); + + CurCell := CurRow.AddCell; + CurParagraph := CurCell.AddParagraph; + CurParagraph.AddText('Third Cell, Third Row'); + + // Style for Subsequent Tables + Center2 := Vec.AddStyle(); + Center2.Name := 'Table Body Centered'; + Center2.Font.Name := 'Verdana'; + Center2.Font.Size := 8; + Center2.Alignment := vsaCenter; + Center2.MarginTop:=2*ONE_POINT_IN_MM; + Center2.MarginBottom:=2*ONE_POINT_IN_MM; + Center2.SetElements := [spbfFontSize, spbfFontName, spbfAlignment, sseMarginTop, sseMarginBottom]; + + // Title + CurParagraph := Page.AddParagraph(); + CurParagraph := Page.AddParagraph(); + CurParagraph.Style := Vec.StyleHeading2; + CurParagraph.AddText('Coded Table #1'); + + // Second Table + CurTable := Page.AddTable; + CurTable.PreferredWidth := Dimension(100, dimPercent); + + For i := 0 To 20 do + Begin + CurRow := CurTable.AddRow; + + // Header Row + If i=0 Then + Begin + CurRow.BackgroundColor := RGBToFPColor(192, 192, 192); + CurRow.Header := True; + end; + + for j := 0 to 4 Do + begin + CurCell := CurRow.AddCell; + + CurParagraph := CurCell.AddParagraph; + CurParagraph.Style := Center2; + + If i=0 Then + CurParagraph.AddText(Format('Header %d', [j])).Style := BoldTextStyle + Else + CurParagraph.AddText(Format('%d x %d', [i, j])); + end; + end; + + CurParagraph := Page.AddParagraph(); + CurParagraph := Page.AddParagraph(); + CurParagraph.Style := Vec.StyleHeading2; + CurParagraph.AddText('Coded Table #2'); + + // Third Table + CurTable := Page.AddTable; + CurTable.PreferredWidth := Dimension(100, dimPercent); + + CurTable.ColWidthsUnits:=dimMillimeter; + CurTable.AddColWidth(15); + CurTable.AddColWidth(20); + CurTable.AddColWidth(20); + CurTable.AddColWidth(20); + CurTable.AddColWidth(79.5); // For Word (and possibly odt), this only has to be close. + // Table.Width=100% means last col is autocalculated + // Added a close value for other renderers such as Wordpad that + // might not support the autocalculation + + // Header Row + CurRow := CurTable.AddRow; + + CurRow.BackgroundColor := RGBToFPColor($64, $95, $ED); + CurRow.Header := True; + + for j := 0 to 4 Do + begin + CurCell := CurRow.AddCell; + + CurParagraph := CurCell.AddParagraph; + CurParagraph.Style := Center2; + + Case j of + 0: CurParagraph.LocalAlignment := vsaLeft; + 1: CurParagraph.LocalAlignment := vsaRight; + end; + + CurParagraph.AddText(Format('Header %d', [j])).Style := BoldTextStyle + End; + + // Data Rows + For i := 1 To 20 do + Begin + CurRow := CurTable.AddRow; + + if (i Mod 2 = 1) Then + CurRow.BackgroundColor := RGBToFPColor(224, 224, 224); + + If (i mod 5 <> 1) Then + iMax := 4 + Else + //iMax := 4; + iMax := 3; + + for j := 0 to iMax Do + begin + CurCell := CurRow.AddCell; + + CurParagraph := CurCell.AddParagraph; + CurParagraph.Style := Center2; + + Case j of + 0: CurParagraph.LocalAlignment := vsaLeft; + 1: CurParagraph.LocalAlignment := vsaRight; + end; + + If (iMax=3) And (j=3) Then + Begin + CurCell.SpannedCols := 2; + CurParagraph.AddText(Format('Merged Cells (%d x %d) & (%d x %d)', [i, j, i, j+1])); + end + Else + CurParagraph.AddText(Format('(%d x %d)', [i, j])); + end; + end; +(* + // Fourth page sequence + Page := Vec.AddTextPageSequence(); + Page.Height := 297; + Page.Width := 210; + + // Set the Header + CurParagraph := Page.Header.AddParagraph; + CurParagraph.Style := CenterParagraphStyle; + CurParagraph.AddText('Testing Images').Style := BoldTextStyle; + + // Title + CurParagraph := Page.AddParagraph(); + CurParagraph.Style := Vec.StyleHeading2; + CurParagraph.AddText('Image 1'); +*) + + dtTime := Now; Vec.WriteToFile('text_output.docx', vfDOCX); + + WriteLn('Native docx writer: '+Format('%.1f msec', [24*60*60*1000*(Now-dtTime)])); + dtTime := Now; + + Vec.WriteToFile('text_output.odt', vfODT); + + WriteLn('Native odt writer: '+Format('%.1f msec', [24*60*60*1000*(Now-dtTime)])); Finally Vec.Free; End; diff --git a/components/fpvectorial/fpvectorial.pas b/components/fpvectorial/fpvectorial.pas index 8aaddafe31..6b8dddc371 100644 --- a/components/fpvectorial/fpvectorial.pas +++ b/components/fpvectorial/fpvectorial.pas @@ -30,6 +30,7 @@ uses Classes, SysUtils, Math, TypInfo, // FCL-Image fpcanvas, fpimage, + // LCL lazutf8 {$ifdef USE_LCL_CANVAS} @@ -88,6 +89,8 @@ const STR_FPVECTORIAL_TEXT_HEIGHT_SAMPLE = 'Ćą'; + NUM_MAX_LISTSTYLES = 8; + type TvCustomVectorialWriter = class; TvCustomVectorialReader = class; @@ -163,16 +166,18 @@ type Kind: TvStyleKind; Alignment: TvStyleAlignment; HeadingLevel: Integer; - ListLevel: Integer; // Only utilized if it is inside a TvBulletList. zero is the first level, 1 the second, and so on // Pen: TvPen; Brush: TvBrush; Font: TvFont; // MarginTop, MarginBottom, MarginLeft, MarginRight: Double; // in mm + SuppressSpacingBetweenSameParagraphs : Boolean; // SetElements: TvSetStyleElements; // + Constructor Create; + function GetKind: TvStyleKind; // takes care of parenting procedure Clear(); procedure CopyFrom(AFrom: TvStyle); @@ -180,6 +185,25 @@ type function CreateStyleCombinedWithParent: TvStyle; end; + TvListStyleKind = (vlskBullet, + vlskDecimal, // 0, 1, 2, 3... + vlskLowerLetter, // a, b, c, d... + vlsLowerRoman, // i, ii, iii, iv.... + vlskUpperLetter, // A, B, C, D... + vlsUpperRoman // I, II, III, IV.... + ); + + TvListStyle = Class + Kind : TvListStyleKind; + Level : Integer; + // Start : Integer; // For numbered lists ?? + Prefix : String; // Suspect this can be more complex than a single char + PrefixFontName : String; // Not used by odt... + MarginLeft : Double; // mm + HangingIndent : Double; //mm + Alignment : TvStyleAlignment; + end; + { Coordinates and polyline segments } T3DPoint = record @@ -837,9 +861,14 @@ type { TvParagraph } TvParagraph = class(TvEntityWithSubEntities) - public + FLocalAlignment : TvStyleAlignment; // Provides localised overwrite of style alignment + private + procedure SetLocalAlignment(AValue: TvStyleAlignment); + public // TODO: LocalAlignment subject to approval by Felipe + UseLocalAlignment : Boolean; // Provides localised overwrite of style alignment Width, Height: Double; AutoExpand: TvRichTextAutoExpand; + ListStyle : TvListStyle; // For Bulleted or Numbered Lists... constructor Create(APage: TvPage); override; destructor Destroy; override; function AddText(AText: string): TvText; @@ -847,6 +876,8 @@ type procedure Render(ADest: TFPCustomCanvas; ARenderInfo: TvRenderInfo; ADestX: Integer = 0; ADestY: Integer = 0; AMulX: Double = 1.0; AMulY: Double = 1.0); override; function GenerateDebugTree(ADestRoutine: TvDebugAddItemProc; APageItem: Pointer): Pointer; override; + + Property LocalAlignment : TvStyleAlignment Read FLocalAlignment Write SetLocalAlignment; end; {@@ @@ -863,7 +894,7 @@ type TvBulletList = class(TvEntityWithSubEntities) public - constructor Create(APage: TvPage); + constructor Create(APage: TvPage); override; // MJT 31/08 added override; destructor Destroy; override; function AddItem(ALevel: Integer; ASimpleText: string): TvParagraph; {function TryToSelect(APos: TPoint; var ASubpart: Cardinal): TvFindEntityResult; override; @@ -879,6 +910,12 @@ type of elements will be all adjusted to fit the TvRichText area } + // Forward reference as Table Cells are TvRichText which in turn + // can also contain tables... + TvTable = Class; +(* + TvImage = Class; +*) { TvRichText } TvRichText = class(TvEntityWithSubEntities) @@ -890,6 +927,8 @@ type // Data writing methods function AddParagraph: TvParagraph; function AddBulletList: TvBulletList; + function AddTable: TvTable; + //function AddImage: TvImage; // function TryToSelect(APos: TPoint; var ASubpart: Cardinal): TvFindEntityResult; override; procedure Render(ADest: TFPCustomCanvas; ARenderInfo: TvRenderInfo; ADestX: Integer = 0; @@ -897,6 +936,129 @@ type function GenerateDebugTree(ADestRoutine: TvDebugAddItemProc; APageItem: Pointer): Pointer; override; end; + (* Support for Adding Tables to the document + Each Cell is a TvRichText to allow full formatted text contents + *) + + TvUnits = (dimMillimeter, dimPercent, dimPoint); + + TvDimension = record + Value : Double; + Units : TvUnits; + end; + + // Use tbtDefault if you don't want the Border settings to be written out + TvTableBorderType = (tbtSingle, tbtDashed, tbtDouble, tbtNone, tbtDefault); + + TvTableBorder = record + LineType : TvTableBorderType; + Spacing : Double; // mm, default 0 + Color : TFPColor; // Ignored if (0, 0, 0, 0) + Width : Double; // mm, default 0. Should really be in point for fine control + end; + + // Can be applied to Tables AND Cells + TvTableBorders = record + Left : TvTableBorder; + Right : TvTableBorder; + Top : TvTableBorder; + Bottom : TvTableBorder; + InsideHoriz : TvTableBorder; // InsideXXX not normally applied to cells + InsideVert : TvTableBorder; // (MS Word Table Styles has an exception) + end; + + { TvTableCell } + + TvVerticalAlignment = (vaTop, vaBottom, vaCenter, cvaBoth); + // Horizontal alignment taken from Paragraph Style + + TvTableCell = Class(TvRichText) + Public + // MJT to Felipe: It may be that Borders can be + // added to TvRichText if odt supports paragraph + // borders, in which case we can refactor a little and + // rename TvTableBorders + Borders : TvTableBorders; // Defaults to be ignored (tbtDefault) + PreferredWidth : TvDimension; // Optional + VerticalAlignment : TvVerticalAlignment; // Defaults to vaTop + BackgroundColor : TFPColor; // Optional + SpannedCols : Integer; // For merging horiz cells. Default 1. + // See diagram above TvTable Class + + constructor Create(APage: TvPage); override; + end; + + { TvTableRow } + + TvTableRow = Class(TvNamedEntity) + private + Cells : TFPList; + Public + Height : Double; // Units mm. Use 0 for default height + CellSpacing : Double; // Units mm. Gap between Cells. + + Header : Boolean; // Repeat row across pages + AllowSplitAcrossPage : Boolean; // Can this Row split across multiple pages? + BackgroundColor : TFPColor; // Optional + + constructor create(APage : TvPage); override; + destructor destroy; override; + + function AddCell : TvTableCell; + function GetCellCount: Integer; + function GetCell(AIndex: Integer) : TvTableCell; + end; + + (* + Note on the grid used for the table + + For the table shown below, three ColWidths must be defined. + + First row should only have 2 cells. First cell spans 2 columns. + Second row should only have 2 cells. Second cell spans 2 columns. + Third row should have 3 cells. Each cell only spans 1 column (default) + + +-----+------+---------+ + | | | + +-----+----------------+ + | | | + +-----+------+---------+ + | | | | + +-----+------+---------+ + *) + + // TvTable.Style should be a Table Style, not a Paragraph Style + // and is optional. + TvTable = class(TvEntityWithStyle) + private + Rows: TFPList; + public + ColWidths: array of Double; // Can be left empty for simple tables + // MUST be fully defined for merging cells + ColWidthsUnits : TvUnits; // Cannot mix ColWidth Units. + Borders : TvTableBorders; // Defaults: single/black/inside and out + PreferredWidth : TvDimension; // Optional. Units mm. + CellSpacing : Double; // Units mm. Gap between Cells. + BackgroundColor : TFPColor; // Optional. Units mm. + + constructor create(APage : TvPage); override; + destructor destroy; override; + + function AddRow : TvTableRow; + function GetRowCount : Integer; + function GetRow(AIndex: Integer) : TvTableRow; + + function AddColWidth(AValue : Double) : Integer; + end; + +(* + TvImage = class(TvEntityWithStyle) // ClassName subject to change... + public + Filename : String; + + Width, Height : Double; // mm + end; +*) { TvVectorialDocument } TvVectorialDocument = class @@ -904,6 +1066,7 @@ type FOnProgress: TvProgressEvent; FPages: TFPList; FStyles: TFPList; + FListStyles: TFPList; FCurrentPageIndex: Integer; function CreateVectorialWriter(AFormat: TvVectorialFormat): TvCustomVectorialWriter; function CreateVectorialReader(AFormat: TvVectorialFormat): TvCustomVectorialReader; @@ -918,7 +1081,8 @@ type SelectedElement: TvEntity; // List of common styles, for conveniently finding them StyleTextBody, StyleHeading1, StyleHeading2, StyleHeading3: TvStyle; - StyleBulletList1, StyleBulletList2, StyleBulletList3: TvStyle; + StyleList : TvStyle; + ListStyles : Array[0..NUM_MAX_LISTSTYLES-1] Of TvListStyle; { Base methods } constructor Create; virtual; destructor Destroy; override; @@ -948,11 +1112,15 @@ type function AddTextPageSequence(): TvTextPageSequence; { Style methods } function AddStyle(): TvStyle; + function AddListStyle: TvListStyle; procedure AddStandardTextDocumentStyles(AFormat: TvVectorialFormat); - function GetBulletListStyle(ALevel: Integer): TvStyle; + function GetListStyleByLevel(ALevel: Integer): TvListStyle; function GetStyleCount: Integer; function GetStyle(AIndex: Integer): TvStyle; function FindStyleIndex(AStyle: TvStyle): Integer; + function GetListStyleCount: Integer; + function GetListStyle(AIndex: Integer): TvListStyle; + function FindListStyleIndex(AListStyle: TvListStyle): Integer; { Data removing methods } procedure Clear; virtual; { Debug methods } @@ -1103,6 +1271,8 @@ type { Data writing methods } function AddParagraph: TvParagraph; function AddBulletList: TvBulletList; + function AddTable: TvTable; + //function AddImage: TvImage; end; {@@ TvVectorialReader class reference type } @@ -1157,6 +1327,7 @@ procedure RegisterVectorialWriter( AWriterClass: TvVectorialWriterClass; AFormat: TvVectorialFormat); function Make2DPoint(AX, AY: Double): T3DPoint; +function Dimension(AValue : Double; AUnits : TvUnits) : TvDimension; implementation @@ -1256,8 +1427,83 @@ begin Result.Z := 0; end; +function Dimension(AValue: Double; AUnits: TvUnits): TvDimension; +begin + Result.Value := AValue; + Result.Units := AUnits; +end; + +{ TvTableCell } + +constructor TvTableCell.Create(APage: TvPage); +begin + inherited Create(APage); + + Borders.Left.LineType:=tbtDefault; + Borders.Right.LineType:=tbtDefault; + Borders.Top.LineType:=tbtDefault; + Borders.Bottom.LineType:=tbtDefault; + Borders.InsideHoriz.LineType:=tbtDefault; + Borders.InsideVert.LineType:=tbtDefault; + + SpannedCols := 1; +end; + +{ TvTable } + +constructor TvTable.Create(APage: TvPage); +begin + inherited Create(APage); + + Rows := TFPList.Create; +end; + +destructor TvTable.destroy; +var + i: Integer; +begin + for i := Rows.Count-1 downto 0 do + begin + TvTableRow(Rows.Last).Free; + Rows.Delete(Rows.Count-1); + end; + + Rows.Free; + Rows := nil; + + inherited destroy; +end; + +function TvTable.AddRow: TvTableRow; +begin + Result := TvTableRow.create(FPage); + Rows.Add(result); +end; + +function TvTable.GetRowCount: Integer; +begin + Result := Rows.Count; +end; + +function TvTable.GetRow(AIndex: Integer): TvTableRow; +begin + Result := TvTableRow(Rows[AIndex]); +end; + +function TvTable.AddColWidth(AValue: Double): Integer; +begin + SetLength(ColWidths, Length(ColWidths) + 1); + ColWidths[High(ColWidths)] := AValue; +end; + { TvStyle } +constructor TvStyle.Create; +begin + // Defaults + SuppressSpacingBetweenSameParagraphs:=False; +end; + function TvStyle.GetKind: TvStyleKind; begin if Parent = nil then Result := Kind @@ -1342,6 +1588,9 @@ begin If sseMarginRight in AFrom.SetElements then MarginRight := AFrom.MarginRight; + // Other + SuppressSpacingBetweenSameParagraphs:=AFrom.SuppressSpacingBetweenSameParagraphs; + SetElements := AFrom.SetElements + SetElements; end; @@ -1352,6 +1601,49 @@ begin if Parent <> nil then Result.ApplyOver(Parent); end; +{ TvTableRow } + +constructor TvTableRow.create(APage: TvPage); +begin + inherited create(APage); + + Cells := TFPList.Create; + + Header := False; +end; + +destructor TvTableRow.destroy; +Var + i : Integer; +begin + for i := Cells.Count-1 downto 0 do + begin + TvTableCell(Cells.Last).Free; + Cells.Delete(Cells.Count-1); + end; + + Cells.Free; + Cells := Nil; + + inherited destroy; +end; + +function TvTableRow.AddCell : TvTableCell; +begin + Result := TvTableCell.Create(FPage); + Cells.Add(Result); +end; + +function TvTableRow.GetCellCount: Integer; +begin + Result := Cells.Count; +end; + +function TvTableRow.GetCell(AIndex: Integer): TvTableCell; +begin + Result := TvTableCell(Cells[AIndex]); +end; + { T2DEllipticalArcSegment } function T2DEllipticalArcSegment.AlignedEllipseCenterEquationT1( @@ -4376,9 +4668,17 @@ end; { TvParagraph } +procedure TvParagraph.SetLocalAlignment(AValue: TvStyleAlignment); +begin + UseLocalAlignment:=True; + FLocalAlignment:=AValue; +end; + constructor TvParagraph.Create(APage: TvPage); begin inherited Create(APage); + + UseLocalAlignment:=False; end; destructor TvParagraph.Destroy; @@ -4427,7 +4727,7 @@ function TvBulletList.AddItem(ALevel: Integer; ASimpleText: string): TvParagraph begin Result := TvParagraph.Create(FPage); if FPage <> nil then - Result.Style := FPage.FOwner.GetBulletListStyle(ALevel); + Result.ListStyle := FPage.FOwner.GetListStyleByLevel(ALevel); if ASimpleText <> '' then Result.AddText(ASimpleText); AddEntity(Result); @@ -4457,6 +4757,19 @@ begin AddEntity(Result); end; +function TvRichText.AddTable: TvTable; +begin + Result := TvTable.Create(FPage); + AddEntity(Result); +end; + +(* +function TvRichText.AddImage: TvImage; +begin + Result := TvImage.Create(FPage); + AddEntity(Result); +end; +*) function TvRichText.TryToSelect(APos: TPoint; var ASubpart: Cardinal ): TvFindEntityResult; begin @@ -5251,6 +5564,17 @@ begin Result := MainText.AddBulletList(); end; +function TvTextPageSequence.AddTable: TvTable; +begin + Result := MainText.AddTable; +end; + +(* +function TvTextPageSequence.AddImage: TvImage; +begin + Result := MainText.AddImage; +end; +*) { TvVectorialDocument } {@@ @@ -5263,6 +5587,7 @@ begin FPages := TFPList.Create; FCurrentPageIndex := -1; FStyles := TFPList.Create; + FListStyles := TFPList.Create; end; {@@ @@ -5276,6 +5601,8 @@ begin FPages := nil; FStyles.Free; FStyles := nil; + FListStyles.Free; + FListStyles := nil; inherited Destroy; end; @@ -5598,9 +5925,17 @@ begin FStyles.Add(Result); end; +function TvVectorialDocument.AddListStyle: TvListStyle; +begin + Result := TvListStyle.Create; + FListStyles.Add(Result); +end; + procedure TvVectorialDocument.AddStandardTextDocumentStyles(AFormat: TvVectorialFormat); var lTextBody, lBaseHeading, lCurStyle: TvStyle; + lCurListStyle : TvListStyle; + i: Integer; begin // // @@ -5693,22 +6028,45 @@ begin // --------------------------------- lCurStyle := AddStyle(); - lCurStyle.Name := 'Bullet List Item 1'; + lCurStyle.Name := 'List Style'; //lCurStyle.Parent := ; - lCurStyle.ListLevel := 1; - StyleBulletList1 := lCurStyle; + lCurStyle.MarginTop := 0.5; + lCurStyle.MarginBottom := 0.5; + lCurStyle.SetElements:=[sseMarginBottom, sseMarginTop]; + lCurStyle.SuppressSpacingBetweenSameParagraphs:=True; + StyleList := lCurStyle; - //, StyleBulletList2, StyleBulletList3 + // --------------------------------- + // List Style Items + // --------------------------------- + + for i := 0 To NUM_MAX_LISTSTYLES-1 Do + begin + lCurListStyle := AddListStyle; + lCurListStyle.Kind := vlskDecimal; + lCurListStyle.Level := i; + lCurListStyle.Prefix := '·'; + lCurListStyle.PrefixFontName := 'Symbol'; + lCurListStyle.MarginLeft := 6.35*(i + 1); + lCurListStyle.HangingIndent := 6.35; + lCurListStyle.Alignment := vsaLeft; + + ListStyles[i] := lCurListStyle; + end; end; -function TvVectorialDocument.GetBulletListStyle(ALevel: Integer): TvStyle; +function TvVectorialDocument.GetListStyleByLevel(ALevel: Integer): TvListStyle; +var + i: Integer; + oListStyle : TvListStyle; begin - case ALevel of - 0: Result := StyleBulletList1; - 1: Result := StyleBulletList2; - 2: Result := StyleBulletList3; - else - Result := nil; + Result := Nil; + for i := 0 to GetListStyleCount-1 do + begin + oListStyle := GetListStyle(i); + + if oListStyle.Level = ALevel then + Exit(oListStyle); end; end; @@ -5731,6 +6089,25 @@ begin if GetStyle(i) = AStyle then Exit(i); end; +function TvVectorialDocument.GetListStyleCount: Integer; +begin + Result := FListStyles.Count; +end; + +function TvVectorialDocument.GetListStyle(AIndex: Integer): TvListStyle; +begin + Result := TvListStyle(FListStyles.Items[AIndex]); +end; + +function TvVectorialDocument.FindListStyleIndex(AListStyle: TvListStyle): Integer; +var + i: Integer; +begin + Result := -1; + for i := 0 to GetListStyleCount()-1 do + if GetListStyle(i) = AListStyle then Exit(i); +end; + {@@ Clears all data in the document } diff --git a/components/fpvectorial/odtvectorialwriter.pas b/components/fpvectorial/odtvectorialwriter.pas index 50a0c9a2b3..24a7c72b56 100644 --- a/components/fpvectorial/odtvectorialwriter.pas +++ b/components/fpvectorial/odtvectorialwriter.pas @@ -99,6 +99,9 @@ type implementation +uses + strutils, htmlelements; + const { OpenDocument general XML constants } XML_HEADER = ''; @@ -666,8 +669,10 @@ end; procedure TvODTVectorialWriter.WriteDocument(AData: TvVectorialDocument); var i: Integer; + CurLevel: String; CurPage: TvPage; CurTextPage: TvTextPageSequence absolute CurPage; + CurListStyle : TvListStyle; begin FContent := XML_HEADER + LineEnding + @@ -716,7 +721,7 @@ begin ' ' + LineEnding + ' ' + LineEnding; FContent := FContent + - ' ' + LineEnding + + ' ' + LineEnding; { ' ' + LineEnding + ' ' + LineEnding + //officeooo:rsid="00072f3e" officeooo:paragraph-rsid="00072f3e" ' ' + LineEnding + @@ -732,15 +737,38 @@ begin ' ' + LineEnding + ' ' + LineEnding + ' ' + LineEnding +} - // - ' ' + LineEnding + - ' ' + LineEnding + - ' ' + LineEnding + - ' ' + LineEnding + - ' ' + LineEnding + - ' ' + LineEnding + - ' ' + LineEnding + + // MJT 2013-08-24 - This is the code to cycle over the ListStyles. + // - This is verified working for Level 0 + // - TvBulletList needs re-architecting to be a tree + // to get deeper levels working + // (see note in WriteBulletStyle) + // - As I understand tOpenDocument-v1.1.pdf the following list style + // should work once we get nesting happening + FContent := FContent + ' ' + LineEnding; + For i := 0 To AData.GetListStyleCount-1 Do + begin + CurListStyle := AData.GetListStyle(i); + CurLevel := IntToStr(CurListStyle.Level+1); // Note the +1... + + If CurListStyle.Kind=vlskBullet Then + FContent := FContent + ' ' + LineEnding + + ' ' + LineEnding + + ' ' + LineEnding + + ' ' + LineEnding + + ' ' + LineEnding; + end; + FContent := FContent + ' ' + LineEnding; + + // Pre MJT code... + //FContent := FContent + + // ' ' + LineEnding + + // ' ' + LineEnding + + // ' ' + LineEnding + + // ' ' + LineEnding + + // ' ' + LineEnding + + // ' ' + LineEnding + + // ' ' + LineEnding; { @@ -788,7 +816,8 @@ begin } - ' ' + LineEnding; + FContent := FContent + + ' ' + LineEnding; FContent := FContent + ' ' + LineEnding; @@ -939,6 +968,8 @@ procedure TvODTVectorialWriter.WriteTextSpan(AEntity: TvText; AParagraph: TvPara var AEntityStyleName: string; lStyle: TvStyle; + sText: String; + i : Integer; begin lStyle := AEntity.GetCombinedStyle(AParagraph); if lStyle = nil then @@ -960,8 +991,23 @@ begin } // Note that here we write only text spans! - FContent := FContent + - ''+AEntity.Value.Text+''; + // MJT 2013-08-24 ODT Writer and DOCX writer were treating TvText.Value differently... + // This code synchronises handling between the two writers... + + sText := EscapeHTML(AEntity.Value.Text); + + // Trim extra CRLF appended by TStringList.Text + If DefaultTextLineBreakStyle = tlbsCRLF Then + sText := Copy(sText, 1, Length(sText) - 2) + Else + sText := Copy(sText, 1, Length(sText) - 1); + + sText := StringReplace(sText, #11, '', [rfReplaceAll]); + sText := StringReplace(sText, #13, '', [rfReplaceAll]); + sText := StringReplace(sText, #10, '', [rfReplaceAll]); + + FContent := FContent + '' + + sText + ''; end; procedure TvODTVectorialWriter.WriteBulletList(AEntity: TvBulletList; @@ -971,8 +1017,17 @@ var lCurEntity, lCurSubEntity: TvEntity; lCurParagraph: TvParagraph; begin + // MJT 2013-08-24 + // Different levels are handled by nesting inside parent + // Only way we can handle this is by treating TvBulletLists as a Tree + // .Level then becomes a function returning the number of steps to root. + // The code below there currently adds everything at level 0 + + // See http://docs.oasis-open.org/office/v1.1/OS/OpenDocument-v1.1.pdf + // page 75 "Example: Lists and sublists" + FContent := FContent + - ' ' + LineEnding; // xml:id="list14840052221" + ' ' + LineEnding; // xml:id="list14840052221" for i := 0 to AEntity.GetEntitiesCount()-1 do begin @@ -981,18 +1036,10 @@ begin if (lCurEntity is TvParagraph) then begin lCurParagraph := lCurEntity as TvParagraph; - if lCurParagraph.Style <> nil then - begin - FContent := FContent + + + FContent := FContent + ' ' + LineEnding + - ' '; - end - else - begin - FContent := FContent + - ' ' + LineEnding + - ' '; - end; + ' '; for j := 0 to lCurParagraph.GetEntitiesCount()-1 do begin @@ -1002,7 +1049,8 @@ begin WriteTextSpan(TvText(lCurSubEntity), lCurParagraph, ACurPage, AData); end; - FContent := FContent + '' + LineEnding + + FContent := FContent + + '' + LineEnding + ' ' + LineEnding; end; end;