diff --git a/components/fpspreadsheet/fpolebasic.pas b/components/fpspreadsheet/fpolebasic.pas index 6e11aa420..058442aee 100644 --- a/components/fpspreadsheet/fpolebasic.pas +++ b/components/fpspreadsheet/fpolebasic.pas @@ -35,6 +35,7 @@ type public procedure WriteOLEFile(AFileName: string; AOLEDocument: TOLEDocument; const AOverwriteExisting: Boolean = False; const AStreamName: UTF8String='Book'); procedure ReadOLEFile(AFileName: string; AOLEDocument: TOLEDocument; const AStreamName: UTF8String='Book'); + procedure ReadOLEStream(AStream: TStream; AOLEDocument: TOLEDocument; const AStreamName: UTF8String='Book'); procedure FreeOLEDocumentData(AOLEDocument: TOLEDocument); end; @@ -88,12 +89,31 @@ procedure TOLEStorage.ReadOLEFile(AFileName: string; AOLEDocument: TOLEDocument; const AStreamName: UTF8String); var RealFile: TFileStream; +begin + RealFile:=TFileStream.Create(AFileName, fmOpenRead or fmShareDenyNone); + try + ReadOLEStream(RealFile, AOLEDocument, AStreamName); + finally + RealFile.Free; + end; +end; + + +procedure TOLEStorage.ReadOLEStream(AStream: TStream; AOLEDocument: TOLEDocument; + const AStreamName: UTF8String = 'Book'); +var fsOLE: TVirtualLayer_OLE; OLEStream: TStream; VLAbsolutePath: UTF8String; begin VLAbsolutePath:='/'+AStreamName; //Virtual layer always use absolute paths. + fsOLE := TVirtualLayer_OLE.Create(AStream); try + fsOLE.Initialize(); //Initialize the OLE container. + OLEStream := fsOLE.CreateStream(VLAbsolutePath, fmOpenRead); + try + + { RealFile:=nil; RealFile:=TFileStream.Create(AFileName, fmOpenRead or fmShareDenyNone); try @@ -114,12 +134,25 @@ begin finally OLEStream.Free; end; + } + if Assigned(OLEStream) then begin + if not AssigneD(AOLEDocument.Stream) then + AOLEDocument.Stream := TMemoryStream.Create + else + (AOLEDocument.Stream as TMemoryStream).Clear; + AOLEDocument.Stream.CopyFrom(OLEStream, OLEStream.Size); + end; finally - fsOLE.Free; + OLEStream.Free; end; + finally + fsOLE.Free; + end; + { finally RealFile.Free; end; + } end; {@@ diff --git a/components/fpspreadsheet/fpsnumformatparser.pas b/components/fpspreadsheet/fpsnumformatparser.pas index ac664c7b1..9db9e715c 100644 --- a/components/fpspreadsheet/fpsnumformatparser.pas +++ b/components/fpspreadsheet/fpsnumformatparser.pas @@ -1253,6 +1253,9 @@ end; procedure TsNumFormatParser.ScanFormat; var done: Boolean; + s: String; + n: Integer; + uch: Cardinal; begin done := false; while (FCurrent < FEnd) and (FStatus = psOK) and (not done) do begin @@ -1270,7 +1273,15 @@ begin '_': // Excel: Leave width of next character empty begin FToken := NextToken; - AddElement(nftEmptyCharWidth, FToken); + uch := UTF8CharacterToUnicode(FCurrent, n); + if n > 1 then + begin + AddElement(nftEmptyCharWidth, UnicodeToUTF8(uch)); + inc(FCurrent, n-1); + FToken := NextToken; + Continue; + end else + AddElement(nftEmptyCharWidth, FToken); end; '@': // Excel: Indicates text format begin @@ -1301,7 +1312,13 @@ begin Exit; end; else - AddElement(nftText, FToken); + uch := UTF8CharacterToUnicode(FCurrent, n); + if n > 1 then + begin + AddElement(nftText, UnicodeToUTF8(uch)); + inc(FCurrent, n-1); + end else + AddElement(nftText, FToken); end; FToken := NextToken; end; diff --git a/components/fpspreadsheet/fpsopendocument.pas b/components/fpspreadsheet/fpsopendocument.pas index 78010e768..8d215be28 100755 --- a/components/fpspreadsheet/fpsopendocument.pas +++ b/components/fpspreadsheet/fpsopendocument.pas @@ -149,6 +149,7 @@ type private FColumnStyleList: TFPList; FRowStyleList: TFPList; + FRichTextFontList: TStringList; FHeaderFooterFontList: TObjectList; // Routines to write parts of files @@ -163,6 +164,7 @@ type procedure WriteRowsAndCells(AStream: TStream; ASheet: TsWorksheet); procedure WriteTableSettings(AStream: TStream); procedure WriteTableStyles(AStream: TStream); + procedure WriteTextStyles(AStream: TStream); procedure WriteVirtualCells(AStream: TStream; ASheet: TsWorksheet); function WriteBackgroundColorStyleXMLAsString(const AFormat: TsCellFormat): String; @@ -232,7 +234,7 @@ type implementation uses - StrUtils, Variants, LazFileUtils, URIParser, + StrUtils, Variants, LazFileUtils, URIParser, LazUTF8, {$IFDEF FPS_VARISBOOL} fpsPatches, {$ENDIF} @@ -958,7 +960,7 @@ end; The function result is false if a style with the given name could not be found } function TsSpreadOpenDocReader.ApplyStyleToCell(ACell: PCell; AStyleName: String): Boolean; var - fmt: PsCellFormat; + fmt: TsCellFormat; styleIndex: Integer; i: Integer; begin @@ -980,8 +982,14 @@ begin exit; styleIndex := TColumnData(FColumnList[i]).DefaultCellStyleIndex; end; - fmt := FCellFormatList.Items[styleIndex]; - ACell^.FormatIndex := FWorkbook.AddCellFormat(fmt^); + fmt := FCellFormatList.Items[styleIndex]^; + if (styleIndex = 0) and FWorksheet.HasHyperlink(ACell) then + begin + // Make sure to use hyperlink font for hyperlink cells in case of default cell style + fmt.FontIndex := HYPERLINK_FONTINDEX; + Include(fmt.UsedFormattingFields, uffFont); + end; + ACell^.FormatIndex := FWorkbook.AddCellFormat(fmt); Result := true; end; @@ -1660,7 +1668,9 @@ var fntSize: Single; fntStyles: TsFontStyles; fntColor: TsColor; + fntPosition: TsFontPosition; s: String; + p: Integer; begin if ANode = nil then begin @@ -1687,9 +1697,20 @@ begin if not ((s = '') or (s = 'none')) then Include(fntStyles, fssUnderline); s := GetAttrValue(ANode, 'style:text-line-through-style'); + if s = '' then s := GetAttrValue(ANode, 'style:text-line-through-type'); if not ((s = '') or (s = 'none')) then Include(fntStyles, fssStrikeout); + fntPosition := fpNormal; + s := GetAttrValue(ANode, 'style:text-position'); + if Length(s) >= 3 then + begin + if (s[3] = 'b') or (s[1] = '-') then + fntPosition := fpSubscript + else + fntPosition := fpSuperscript; + end; + s := GetAttrValue(ANode, 'fo:color'); if s <> '' then fntColor := HTMLColorStrToColor(s) @@ -1703,13 +1724,13 @@ begin end else if (APreferredIndex > -1) then begin - FWorkbook.ReplaceFont(APreferredIndex, fntName, fntSize, fntStyles, fntColor); + FWorkbook.ReplaceFont(APreferredIndex, fntName, fntSize, fntStyles, fntColor, fntPosition); Result := APreferredIndex; end else begin - Result := FWorkbook.FindFont(fntName, fntSize, fntStyles, fntColor); + Result := FWorkbook.FindFont(fntName, fntSize, fntStyles, fntColor, fntPosition); if Result = -1 then - Result := FWorkbook.AddFont(fntName, fntSize, fntStyles, fntColor); + Result := FWorkbook.AddFont(fntName, fntSize, fntStyles, fntColor, fntPosition); end; end; @@ -1825,6 +1846,10 @@ begin Workbook.OnReadCellData(Workbook, ARow, ACol, cell); end; +{ In principle, this method could be simplified by calling ReadFromStream which + is essentially a duplication of ReadFromFile. But ReadFromStream leads to + worse memory usage. --> KEEP READFROMFILE INTACT + See fpspeedtest, ods 20k x 100 cells --> out of mem in Win7-32 bit, 4 GB} procedure TsSpreadOpenDocReader.ReadFromFile(AFileName: string); var Doc : TXMLDocument; @@ -1935,12 +1960,132 @@ begin end; procedure TsSpreadOpenDocReader.ReadFromStream(AStream: TStream); +var + Doc : TXMLDocument; +// FilePath : string; +// UnZip : TUnZipper; +// FileList : TStringList; + BodyNode, SpreadSheetNode, TableNode: TDOMNode; + StylesNode: TDOMNode; + OfficeSettingsNode: TDOMNode; + nodename: String; + pageLayout: PsPageLayout; + XMLStream: TStream; +begin + { + //unzip files into AFileName path + FilePath := GetTempDir(false); + UnZip := TUnZipper.Create; + FileList := TStringList.Create; + try + FileList.Add('styles.xml'); + FileList.Add('content.xml'); + FileList.Add('settings.xml'); + UnZip.OutputPath := FilePath; + Unzip.UnZipFiles(AFileName,FileList); + finally + FreeAndNil(FileList); + FreeAndNil(UnZip); + end; //try + } + Doc := nil; + try + // process the styles.xml file + XMLStream := TMemoryStream.Create; + try + if UnzipToStream(AStream, 'styles.xml', XMLStream) then + ReadXMLStream(Doc, XMLStream); + finally + XMLStream.Free; + end; + + StylesNode := Doc.DocumentElement.FindNode('office:styles'); + ReadNumFormats(StylesNode); + ReadStyles(StylesNode); + ReadAutomaticStyles(Doc.DocumentElement.FindNode('office:automatic-styles')); + ReadMasterStyles(Doc.DocumentElement.FindNode('office:master-styles')); + FreeAndNil(Doc); + + //process the content.xml file + XMLStream := TMemoryStream.Create; + try + if UnzipToStream(AStream, 'content.xml', XMLStream) then + ReadXMLStream(Doc, XMLStream); + finally + XMLStream.Free; + end; + + StylesNode := Doc.DocumentElement.FindNode('office:automatic-styles'); + ReadNumFormats(StylesNode); + ReadStyles(StylesNode); + + BodyNode := Doc.DocumentElement.FindNode('office:body'); + if not Assigned(BodyNode) then + raise Exception.Create('[TsSpreadOpenDocReader.ReadFromStream] Node "office:body" not found.'); + + SpreadSheetNode := BodyNode.FindNode('office:spreadsheet'); + if not Assigned(SpreadSheetNode) then + raise Exception.Create('[TsSpreadOpenDocReader.ReadFromStream] Node "office:spreadsheet" not found.'); + + ReadDateMode(SpreadSheetNode); + + //process each table (sheet) + TableNode := SpreadSheetNode.FindNode('table:table'); + while Assigned(TableNode) do + begin + nodename := TableNode.Nodename; + // These nodes occur due to leading spaces which are not skipped + // automatically any more due to PreserveWhiteSpace option applied + // to ReadXMLFile + if nodeName <> 'table:table' then + begin + TableNode := TableNode.NextSibling; + continue; + end; + FWorkSheet := FWorkbook.AddWorksheet(GetAttrValue(TableNode, 'table:name'), true); + // Collect column styles used + ReadColumns(TableNode); + // Process each row inside the sheet and process each cell of the row + ReadRowsAndCells(TableNode); + // Read page layout + pageLayout := ReadPageLayout(StylesNode, GetAttrValue(TableNode, 'table:style-name')); + if pageLayout <> nil then + FWorksheet.PageLayout := pagelayout^; + // Handle columns and rows + ApplyColWidths; + // Page layout + FixCols(FWorksheet); + FixRows(FWorksheet); + // Continue with next table + TableNode := TableNode.NextSibling; + end; //while Assigned(TableNode) + + FreeAndNil(Doc); + + // process the settings.xml file (Note: it does not always exist!) + XMLStream := TMemoryStream.Create; + try + if UnzipToStream(AStream, 'settings.xml', XMLStream) then + begin + ReadXMLStream(Doc, XMLStream); + OfficeSettingsNode := Doc.DocumentElement.FindNode('office:settings'); + ReadSettings(OfficeSettingsNode); + end; + finally + XMLStream.Free; + end; + + finally + FreeAndNil(Doc); + end; +end; +{ begin Unused(AStream); raise Exception.Create('[TsSpreadOpenDocReader.ReadFromStream] '+ 'Method not implemented. Use "ReadFromFile" instead.'); end; - + } procedure TsSpreadOpenDocReader.ReadHeaderFooterFont(ANode: TDOMNode; var AFontName: String; var AFontSize: Double; var AFontStyle: TsHeaderFooterFontStyles; var AFontColor: TsColor); @@ -1998,13 +2143,16 @@ end; procedure TsSpreadOpenDocReader.ReadLabel(ARow, ACol: Cardinal; ACellNode: TDOMNode); var - cellText: String; + cellText, spanText: String; styleName: String; childnode: TDOMNode; subnode: TDOMNode; nodeName: String; cell: PCell; hyperlink: string; + fmt: TsCellFormat; + rtParams: TsRichTextParams; + idx: Integer; procedure AddToCellText(AText: String); begin @@ -2020,6 +2168,7 @@ begin like below is much better: } cellText := ''; hyperlink := ''; + SetLength(rtParams, 0); childnode := ACellNode.FirstChild; while Assigned(childnode) do begin @@ -2041,7 +2190,21 @@ begin AddToCellText(subnode.TextContent); end; 'text:span': - AddToCellText(subnode.TextContent); + begin + spanText := subnode.TextContent; + stylename := GetAttrValue(subnode, 'text:style-name'); + if stylename <> '' then begin + idx := FCellFormatList.FindIndexOfName(stylename); + if idx > -1 then + begin + SetLength(rtParams, Length(rtParams)+1); + rtParams[High(rtParams)].FontIndex := FCellFormatList[idx]^.FontIndex; + rtParams[High(rtParams)].StartIndex := Length(cellText); + rtParams[High(rtParams)].EndIndex := Length(cellText + spanText); + end; + end; + AddToCelLText(spanText); + end; end; subnode := subnode.NextSibling; end; @@ -2056,7 +2219,7 @@ begin end else cell := FWorksheet.AddCell(ARow, ACol); - FWorkSheet.WriteUTF8Text(cell, cellText); + FWorkSheet.WriteUTF8Text(cell, cellText, rtParams); if hyperlink <> '' then begin // ODS sees relative paths relative to the internal own file structure @@ -2917,6 +3080,7 @@ var nodeName: String; family: String; styleName: String; + parentstyle: String; fmt: TsCellFormat; numFmtIndexDefault: Integer; numFmtName: String; @@ -2925,6 +3089,7 @@ var numFmtParams: TsNumFormatParams; clr: TsColor; s: String; + idx: Integer; procedure SetBorderStyle(ABorder: TsCellBorder; AStyleValue: String); const @@ -3013,6 +3178,7 @@ begin if nodeName = 'style:style' then begin family := GetAttrValue(styleNode, 'style:family'); + parentstyle := GetAttrValue(stylenode, 'style:parent-style-name'); // Column styles if family = 'table-column' then @@ -3028,6 +3194,13 @@ begin styleName := GetAttrValue(styleNode, 'style:name'); InitFormatRecord(fmt); + + if parentstyle <> '' then + begin + idx := FCellFormatList.FindIndexOfName(parentstyle); + if idx > -1 then + fmt := FCellFormatList[idx]^; + end; fmt.Name := styleName; numFmtIndex := -1; @@ -3173,8 +3346,28 @@ begin end; styleChildNode := styleChildNode.NextSibling; end; - FCellFormatList.Add(fmt); + end + else + if family = 'text' then + begin + // "Rich-text formatting run" style + styleName := GetAttrValue(styleNode, 'style:name'); + styleChildNode := styleNode.FirstChild; + while Assigned(styleChildNode) do + begin + nodeName := styleChildNode.NodeName; + if nodeName = 'style:text-properties' then + begin + InitFormatRecord(fmt); + fmt.Name := styleName; + fmt.FontIndex := ReadFont(styleChildNode); + if fmt.FontIndex > 0 then + Include(fmt.UsedFormattingFields, uffFont); + FCellFormatList.Add(fmt); + end; + styleChildNode := stylechildNode.NextSibling; + end; end; end; styleNode := styleNode.NextSibling; @@ -3410,6 +3603,9 @@ begin FSMetaInfManifest.Position := 0; end; +{ Writes the node "office:automatic-styles". Although this node occurs in both + "contents.xml" and "styles.xml" files, this method is called only for writing + to "styles.xml". } procedure TsSpreadOpenDocWriter.WriteAutomaticStyles(AStream: TStream); var i: Integer; @@ -3640,11 +3836,12 @@ begin AppendToStream(FSContent, ''); - WriteNumFormats(FSContent); - WriteColStyles(FSContent); - WriteRowStyles(FSContent); - WriteTableStyles(FSContent); - WriteCellStyles(FSContent); + WriteNumFormats(FSContent); // "N1" ... + WriteColStyles(FSContent); // "co1" ... + WriteRowStyles(FSContent); // "ro1" ... + WriteTableStyles(FSContent); // "ta1" ... + WriteCellStyles(FSContent); // "ce1" ... + WriteTextStyles(FSContent); // "T1" ... AppendToStream(FSContent, ''); @@ -4221,6 +4418,7 @@ begin FColumnStyleList := TFPList.Create; FRowStyleList := TFPList.Create; + FRichTextFontList := TStringList.Create; FHeaderFooterFontList := TObjectList.Create; FPointSeparatorSettings := SysUtils.DefaultFormatSettings; @@ -4242,6 +4440,7 @@ begin for j:=FRowStyleList.Count-1 downto 0 do TObject(FRowStyleList[j]).Free; FRowStyleList.Free; + FRichTextFontList.Free; // Do not destroy fonts, they are owned by Workbook FHeaderFooterFontList.Free; inherited Destroy; @@ -4614,6 +4813,12 @@ begin if fssStrikeout in AFont.Style then Result := Result + 'style:text-line-through-style="solid" '; + if AFont.Position = fpSubscript then + Result := Result + 'style:text-position="sub 58%" '; + + if AFont.Position = fpSuperscript then + Result := Result + 'style:text-position="super 58%" '; + if AFont.Color <> defFnt.Color then Result := Result + Format('fo:color="%s" ', [ColorToHTMLColorStr(AFont.Color)]); end; @@ -4879,9 +5084,44 @@ begin end; end; +procedure TsSpreadOpenDocWriter.WriteTextStyles(AStream: TStream); +var + cell: PCell; + rtp: TsRichTextParam; + styleCounter: Integer; + fnt: TsFont; + fntStr: String; + styleName: String; + sheet: TsWorksheet; + i: Integer; +begin + styleCounter := 0; + for i := 0 to FWorkbook.GetWorksheetCount-1 do + begin + sheet := FWorkbook.GetWorksheetByIndex(i); + for cell in sheet.Cells do + begin + if Length(cell^.RichTextParams) = 0 then + Continue; + for rtp in cell^.RichTextParams do + begin + inc(styleCounter); + stylename := Format('T%d', [stylecounter]); + fnt := FWorkbook.GetFont(rtp.FontIndex); + FRichTextFontList.AddObject(stylename, fnt); + fntStr := WriteFontStyleXMLAsString(fnt); + AppendToStream(AStream, + '' + + '' + + ''); + end; + end; + end; +end; + {@@ ---------------------------------------------------------------------------- - Creates an XML string for inclusion of the textrotation style option into the + Creates an XML string for inclusion of the text rotation style option into the written file from the textrotation setting in the format cell. Is called from WriteStyles (via WriteStylesXMLAsString). -------------------------------------------------------------------------------} @@ -5196,8 +5436,11 @@ var txt: ansistring; textp, target, bookmark, comment: String; fmt: TsCellFormat; + fnt: TsFont; hyperlink: PsHyperlink; u: TUri; + i, idx, n, len: Integer; + rtParam: TsRichTextParam; begin Unused(ARow, ACol); @@ -5254,8 +5497,52 @@ begin '', [target, txt]); end else + begin // No hyperlink, normal text only - textp := '' + txt + ''; + if Length(ACell^.RichTextParams) = 0 then + // Standard text formatting + textp := '' + txt + '' + else + begin + // "Rich-text" formatting + len := UTF8Length(AValue); + textp := ''; + rtParam := ACell^.RichTextParams[0]; + if rtParam.StartIndex > 0 then + begin + txt := UTF8Copy(AValue, 1, rtParam.StartIndex); + ValidXMLText(txt); + textp := textp + txt; + end; + for i := 0 to High(ACell^.RichTextParams) do + begin + rtParam := ACell^.RichTextParams[i]; + fnt := FWorkbook.GetFont(rtParam.FontIndex); + idx := FRichTextFontList.IndexOfObject(fnt); + n := rtParam.EndIndex - rtParam.StartIndex; + txt := UTF8Copy(AValue, rtParam.StartIndex+1, n); + ValidXMLText(txt); + textp := textp + + '' + + txt + + ''; + if (rtParam.EndIndex < len) and (i = High(ACell^.RichTextParams)) then + begin + txt := UTF8Copy(AValue, rtParam.EndIndex+1, MaxInt); + ValidXMLText(txt); + textp := textp + txt; + end else + if (i < High(ACell^.RichTextParams)) and (rtParam.EndIndex < ACell^.RichTextParams[i+1].StartIndex) + then begin + n := ACell^.RichTextParams[i+1].StartIndex - rtParam.EndIndex; + txt := UTF8Copy(AValue, rtParam.EndIndex+1, n); + ValidXMLText(txt); + textp := textp + txt; + end; + end; + textp := textp + ''; + end; + end; // Write it ... AppendToStream(AStream, Format( diff --git a/components/fpspreadsheet/fpspreadsheet.pas b/components/fpspreadsheet/fpspreadsheet.pas index 509a524af..c3bcb9ebe 100755 --- a/components/fpspreadsheet/fpspreadsheet.pas +++ b/components/fpspreadsheet/fpspreadsheet.pas @@ -1,3 +1,14 @@ +{** + Unit: fpspreadsheet + + implements **spreadsheet documents** and their properties and methods. + + AUTHORS: Felipe Monteiro de Carvalho, Reinier Olislagers, Werner Pamler + + LICENSE: See the file COPYING.modifiedLGPL.txt, included in the Lazarus + distribution, for details about the license. +} + {@@ ---------------------------------------------------------------------------- Unit fpspreadsheet implements spreadsheet documents and their properties and methods. @@ -31,6 +42,18 @@ type TsBasicSpreadReader = class; TsBasicSpreadWriter = class; + {** + Type: TRow -- record containing information about a spreadsheet row + + Members: + - Row -- The index of the row (beginning with 0) + - Height -- The height of the row (expressed as line count of the default font) + + Notes: + - Only rows with heights that cannot be derived from the font height have + a row record. + } + {@@ The record TRow contains information about a spreadsheet row: @param Row The index of the row (beginning with 0) @param Height The height of the row (expressed as lines count of the default font) @@ -239,9 +262,10 @@ type procedure WriteRPNFormula(ACell: PCell; AFormula: TsRPNFormula); overload; - function WriteUTF8Text(ARow, ACol: Cardinal; AText: ansistring): PCell; overload; -// procedure WriteUTF8Text(ACell: PCell; AText: ansistring); overload; - procedure WriteUTF8Text(ACell: PCell; AText: String; ARichTextparams: TsRichTextParams = nil); overload; + function WriteUTF8Text(ARow, ACol: Cardinal; AText: ansistring; + ARichTextParams: TsRichTextParams = nil): PCell; overload; + procedure WriteUTF8Text(ACell: PCell; AText: String; + ARichTextparams: TsRichTextParams = nil); overload; { Writing of cell attributes } function WriteBackground(ARow, ACol: Cardinal; AStyle: TsFillStyle; @@ -890,9 +914,9 @@ begin end; -{******************************************************************************* -* TsWorksheet * -*******************************************************************************} +{------------------------------------------------------------------------------} +{ TsWorksheet } +{------------------------------------------------------------------------------} {@@ ---------------------------------------------------------------------------- Constructor of the TsWorksheet class. @@ -3461,15 +3485,22 @@ end; On formats that don't support unicode, the text will be converted to ISO Latin 1. - @param ARow The row of the cell - @param ACol The column of the cell - @param AText The text to be written encoded in utf-8 + @param ARow The row of the cell + @param ACol The column of the cell + @param AText The text to be written encoded in utf-8 + @param ARichTextParams Array of formatting instructions for characters or + groups of characters (see TsRichTextParam). + @return Pointer to cell created or used + + @see TsRichTextParams + @see TsRichTextParam -------------------------------------------------------------------------------} -function TsWorksheet.WriteUTF8Text(ARow, ACol: Cardinal; AText: ansistring): PCell; +function TsWorksheet.WriteUTF8Text(ARow, ACol: Cardinal; AText: ansistring; + ARichTextParams: TsRichTextParams = nil): PCell; begin Result := GetCell(ARow, ACol); - WriteUTF8Text(Result, AText); + WriteUTF8Text(Result, AText, ARichTextParams); end; {@@ ---------------------------------------------------------------------------- @@ -3478,8 +3509,13 @@ end; On formats that don't support unicode, the text will be converted to ISO Latin 1. - @param ACell Pointer to the cell - @param AText The text to be written encoded in utf-8 + @param ACell Pointer to the cell + @param AText The text to be written encoded in utf-8 + @param ARichTextParams Array of formatting instructions for characters or + groups of characters (see TsRichTextParam). + + @see TsRichTextParams + @see TsRichTextParam -------------------------------------------------------------------------------} procedure TsWorksheet.WriteUTF8Text(ACell: PCell; AText: String; ARichTextParams: TsRichTextParams = nil); @@ -3537,6 +3573,7 @@ end; @param ARow Cell row index @param ACol Cell column index @param ANumber Number to be written + @return Pointer to cell created or used -------------------------------------------------------------------------------} function TsWorksheet.WriteNumber(ARow, ACol: Cardinal; ANumber: double): PCell; @@ -6032,9 +6069,9 @@ begin end; -{******************************************************************************* -* TsWorkbook * -*******************************************************************************} +{------------------------------------------------------------------------------} +{ TsWorkbook } +{------------------------------------------------------------------------------} {@@ ---------------------------------------------------------------------------- Helper method called before reading the workbook. Clears the error log. @@ -7664,9 +7701,9 @@ begin end; *) -{******************************************************************************* -* TsBasicSpreadReaderWriter * -*******************************************************************************} +{------------------------------------------------------------------------------} +{ TsBasicSpreadReaderWriter } +{------------------------------------------------------------------------------} {@@ ---------------------------------------------------------------------------- Constructor of the reader/writer. Has the workbook to be read/written as a @@ -7696,9 +7733,9 @@ begin end; -{******************************************************************************* -* TsBasicSpreadWriter * -*******************************************************************************} +{------------------------------------------------------------------------------} +{ TsBasicSpreadWriter } +{------------------------------------------------------------------------------} {@@ ---------------------------------------------------------------------------- Checks limitations of the writer, e.g max row/column count @@ -7724,5 +7761,4 @@ initialization finalization SetLength(GsSpreadFormats, 0); -end. - +end. {** End Unit: fpspreadsheet } diff --git a/components/fpspreadsheet/fpspreadsheetgrid.pas b/components/fpspreadsheet/fpspreadsheetgrid.pas index 0e47cb262..47142d688 100644 --- a/components/fpspreadsheet/fpspreadsheetgrid.pas +++ b/components/fpspreadsheet/fpspreadsheetgrid.pas @@ -598,17 +598,6 @@ uses fpCanvas, fpsStrings, fpsUtils, fpsVisualUtils, fpsNumFormat; const - {@@ Translation of the fpspreadsheet type of horizontal text alignment to that - used in the graphics unit. } - HOR_ALIGNMENTS: array[haLeft..haRight] of TAlignment = ( - taLeftJustify, taCenter, taRightJustify - ); - {@@ Translation of the fpspreadsheet type of vertical text alignment to that - used in the graphics unit. } - VERT_ALIGNMENTS: array[TsVertAlignment] of TTextLayout = ( - tlBottom, tlTop, tlCenter, tlBottom - ); - {@@ Default number of columns prepared for a new empty worksheet } DEFAULT_COL_COUNT = 26; {@@ Default number of rows prepared for a new empty worksheet } @@ -2100,7 +2089,7 @@ var txtRot: TsTextRotation; fntIndex: Integer; lCell: PCell; - justif: Byte; +// justif: Byte; fmt: PsCellFormat; begin if (Worksheet = nil) then @@ -2169,7 +2158,7 @@ begin txt := GetCellText(GetGridRow(lCell^.Col), GetGridCol(lCell^.Row)); if txt = '' then exit; - + { case txtRot of trHorizontal: case horAlign of @@ -2190,7 +2179,7 @@ begin vaCenter: justif := 1; vaBottom: justif := 0; end; - end; + end; } InternalDrawTextInCell(txt, ARect, horAlign, vertAlign, txtRot, wrapped, fntIndex, lCell^.RichTextParams); { diff --git a/components/fpspreadsheet/fpstypes.pas b/components/fpspreadsheet/fpstypes.pas index df6c79950..ee9121a48 100644 --- a/components/fpspreadsheet/fpstypes.pas +++ b/components/fpspreadsheet/fpstypes.pas @@ -404,7 +404,7 @@ type TsFontStyles = set of TsFontStyle; {@@ Font position (subscript or superscript) } - TsFontPosition = (fpNormal, fpSubscript, fpSuperscript); + TsFontPosition = (fpNormal, fpSuperscript, fpSubscript); // Keep order for compatibility with xls! {@@ Font record used in fpspreadsheet. Contains the font name, the font size (in points), the font style, and the font color. } @@ -432,7 +432,7 @@ type TsRichTextParams = array of TsRichTextParam; {@@ Excel rich-text formatting run } - TsRichTextFormattingRun = record + TsRichTextFormattingRun = packed record FirstIndex: Integer; FontIndex: Integer; end; diff --git a/components/fpspreadsheet/fpsvisualutils.pas b/components/fpspreadsheet/fpsvisualutils.pas index d8635b521..17cd06185 100644 --- a/components/fpspreadsheet/fpsvisualutils.pas +++ b/components/fpspreadsheet/fpsvisualutils.pas @@ -196,7 +196,7 @@ var totalHeight, linelen, stackPeriod: Integer; procedure InitFont(P: PChar; out rtState: TRtState; - PendingRtpIndex: Integer; out AHeight: Integer); + PendingRtpIndex: Integer; out AHeight: Integer; out AFontPos: TsFontPosition); var fnt: TsFont; hasRtp: Boolean; @@ -216,12 +216,13 @@ var Convert_sFont_to_Font(fnt, ACanvas.Font); AHeight := ACanvas.TextHeight('Tg'); if (fnt <> nil) and (fnt.Position <> fpNormal) then - ACanvas.Font.Size := round(ACanvas.Font.Size * SUBSCRIPT_SUPERSCRIPT_FACTOR); + ACanvas.Font.Size := round(fnt.Size * SUBSCRIPT_SUPERSCRIPT_FACTOR); + AFontPos := fnt.Position; end; procedure UpdateFont(P:PChar; var rtState: TRtState; var PendingRtpIndex: Integer; var AHeight: Integer; - out AFontPos: TsFontPosition); + var AFontPos: TsFontPosition); var hasRtp: Boolean; rtp: TsRichTextParam; @@ -238,7 +239,8 @@ var Convert_sFont_to_Font(fnt, ACanvas.Font); AHeight := ACanvas.TextHeight('Tg'); if fnt.Position <> fpNormal then - ACanvas.Font.Size := round(ACanvas.Font.Size * SUBSCRIPT_SUPERSCRIPT_FACTOR); + ACanvas.Font.Size := round(fnt.Size * SUBSCRIPT_SUPERSCRIPT_FACTOR); + AFontPos := fnt.Position; rtState := rtEnter; end else if (p - pStartText >= rtp.EndIndex) and (rtState = rtEnter) then @@ -264,10 +266,10 @@ var Convert_sFont_to_Font(fnt, ACanvas.Font); AHeight := ACanvas.TextHeight('Tg'); if fnt.Position <> fpNormal then - ACanvas.Font.Size := round(ACanvas.Font.Size * SUBSCRIPT_SUPERSCRIPT_FACTOR); + ACanvas.Font.Size := round(fnt.Size * SUBSCRIPT_SUPERSCRIPT_FACTOR); + AFontPos := fnt.Position; end; end; - AFontPos := fnt.Position; end; procedure ScanLine(var P: PChar; var NumSpaces: Integer; @@ -287,7 +289,7 @@ var begin NumSpaces := 0; - InitFont(p, rtState, PendingRtpIndex, h); + InitFont(p, rtState, PendingRtpIndex, h, fntpos); height := h; pEOL := p; @@ -365,12 +367,12 @@ var p: PChar; rtState: TRtState; h, w: Integer; - fntpos: TsFontPosition; + fntpos: TsFontPosition = fpNormal; s: utf8String; charLen: Integer; begin p := pStart; - InitFont(p, rtState, PendingRtpIndex, h); + InitFont(p, rtState, PendingRtpIndex, h, fntpos); while p^ <> #0 do begin s := UnicodeToUTF8(UTF8CharacterToUnicode(p, charLen)); UpdateFont(p, rtState, PendingRtpIndex, h, fntpos); diff --git a/components/fpspreadsheet/fpsxmlcommon.pas b/components/fpspreadsheet/fpsxmlcommon.pas index b40e1ba6a..d94ec0597 100644 --- a/components/fpspreadsheet/fpsxmlcommon.pas +++ b/components/fpspreadsheet/fpsxmlcommon.pas @@ -16,12 +16,15 @@ type TsSpreadXMLReader = class(TsCustomSpreadReader) protected procedure ReadXMLFile(out ADoc: TXMLDocument; AFileName: String); + procedure ReadXMLStream(out ADoc: TXMLDocument; AStream: TStream); end; function GetAttrValue(ANode : TDOMNode; AAttrName : string) : string; function GetNodeValue(ANode: TDOMNode): String; -procedure UnzipFile(AZipFileName, AZippedFile, ADestFolder: String); +procedure UnzipFile(AZipFileName, AZippedFile, ADestFolder: String); +function UnzipToStream(AZipStream: TStream; const AZippedFile: String; + ADestStream: TStream): Boolean; implementation @@ -33,9 +36,13 @@ uses {$ENDIF} fpsStreams; -{ Gets value for the specified attribute. Returns empty string if attribute - not found. } -function {TsSpreadXMLReader.}GetAttrValue(ANode : TDOMNode; AAttrName : string) : string; +{------------------------------------------------------------------------------} +{ Utilities } +{------------------------------------------------------------------------------} + +{ Gets value for the specified attribute of the given node. + Returns empty string if attribute is not found. } +function GetAttrValue(ANode : TDOMNode; AAttrName : string) : string; var i: LongWord; Found: Boolean; @@ -58,7 +65,7 @@ end; { Returns the text value of a node. Normally it would be sufficient to call "ANode.NodeValue", but since the DOMParser needs to preserve white space (for the spaces in date/time formats), we have to go more into detail. } -function {TsSpreadXMLReader.}GetNodeValue(ANode: TDOMNode): String; +function GetNodeValue(ANode: TDOMNode): String; var child: TDOMNode; begin @@ -68,38 +75,109 @@ begin Result := child.NodeValue; end; + +{------------------------------------------------------------------------------} +{ Unzipping } +{------------------------------------------------------------------------------} +type + TStreamUnzipper = class(TUnzipper) + private + FInputStream: TStream; + FOutputStream: TStream; + FSuccess: Boolean; + procedure CloseInputStream(Sender: TObject; var AStream: TStream); + procedure CreateStream(Sender: TObject; var AStream: TStream; + AItem: TFullZipFileEntry); + procedure DoneStream(Sender: TObject; var AStream: TStream; + AItem: TFullZipFileEntry); + procedure OpenInputStream(Sender: TObject; var AStream: TStream); + public + constructor Create(AInputStream: TStream); + function UnzipFile(const AZippedFile: string; ADestStream: TStream): Boolean; + end; + +constructor TStreamUnzipper.Create(AInputStream: TStream); +begin + inherited Create; + OnCloseInputStream := @CloseInputStream; + OnCreateStream := @CreateStream; + OnDoneStream := @DoneStream; + OnOpenInputStream := @OpenInputStream; + FInputStream := AInputStream +end; + +procedure TStreamUnzipper.CloseInputStream(Sender: TObject; var AStream: TStream); +begin + AStream := nil; +end; + +procedure TStreamUnzipper.CreateStream(Sender: TObject; var AStream: TStream; + AItem: TFullZipFileEntry); +begin + FSuccess := True; + AStream := FOutputStream; +end; + +procedure TStreamUnzipper.DoneStream(Sender: TObject; var AStream: TStream; + AItem: TFullZipFileEntry); +begin + AStream := nil; +end; + +procedure TStreamUnzipper.OpenInputStream(Sender: TObject; var AStream: TStream); +begin + AStream := FInputStream; +end; + +function TStreamUnzipper.UnzipFile(const AZippedFile: string; + ADestStream: TStream): Boolean; +begin + FOutputStream := ADestStream; + FSuccess := False; + Files.Clear; + Files.Add(AZippedFile); + UnZipAllFiles; + Result := FSuccess; +end; + { We have to use our own ReadXMLFile procedure (there is one in xmlread) because we have to preserve spaces in element text for date/time separator. As a side-effect we have to skip leading spaces by ourselves. } procedure TsSpreadXMLReader.ReadXMLFile(out ADoc: TXMLDocument; AFileName: String); var - parser: TDOMParser; - src: TXMLInputSource; stream: TStream; begin if (boBufStream in Workbook.Options) then - stream := TBufStream.Create(AFileName, fmOpenRead + fmShareDenyWrite) + stream := TBufStream.Create(AFilename, fmOpenRead + fmShareDenyWrite) else stream := TFileStream.Create(AFileName, fmOpenRead + fmShareDenyWrite); try - parser := TDOMParser.Create; - try - parser.Options.PreserveWhiteSpace := true; // This preserves spaces! - src := TXMLInputSource.Create(stream); - try - parser.Parse(src, ADoc); - finally - src.Free; - end; - finally - parser.Free; - end; + ReadXMLStream(ADoc, stream); finally stream.Free; end; end; +procedure TsSpreadXMLReader.ReadXMLStream(out ADoc: TXMLDocument; AStream: TStream); +var + parser: TDOMParser; + src: TXMLInputSource; +begin + parser := TDOMParser.Create; + try + parser.Options.PreserveWhiteSpace := true; // This preserves spaces! + src := TXMLInputSource.Create(AStream); + try + parser.Parse(src, ADoc); + finally + src.Free; + end; + finally + parser.Free; + end; +end; + procedure UnzipFile(AZipFileName, AZippedFile, ADestFolder: String); var list: TStringList; @@ -121,5 +199,21 @@ begin end; +function UnzipToStream(AZipStream: TStream; const AZippedFile: String; + ADestStream: TStream): Boolean; +var + unzip: TStreamUnzipper; + p: Int64; +begin + p := ADestStream.Position; + unzip := TStreamUnzipper.Create(AZipStream); + try + Result := unzip.UnzipFile(AZippedFile, ADestStream); + ADestStream.Position := p; + finally + unzip.Free; + end; +end; + end. diff --git a/components/fpspreadsheet/reference/BIFFExplorer/bebiffgrid.pas b/components/fpspreadsheet/reference/BIFFExplorer/bebiffgrid.pas index cce3c1636..68e765699 100644 --- a/components/fpspreadsheet/reference/BIFFExplorer/bebiffgrid.pas +++ b/components/fpspreadsheet/reference/BIFFExplorer/bebiffgrid.pas @@ -101,6 +101,7 @@ type procedure ShowRefreshAll; procedure ShowRightMargin; procedure ShowRK; + procedure ShowRString; procedure ShowRow; procedure ShowSelection; procedure ShowSharedFormula; @@ -132,7 +133,12 @@ type ACharCount: Integer; out AString: String; out ANumbytes: Integer); overload; procedure ExtractString(ABufIndex: Integer; ALenBytes: Byte; AUnicode: Boolean; out AString: String; out ANumBytes: Integer; + out ARichTextRuns: TsRichTextFormattingRuns; + out ABufIndexOfFirstRichTextRun: LongWord; IgnoreCompressedFlag: Boolean = false); overload; + procedure ExtractString(ABufIndex: Integer; ALenbytes: Byte; AUnicode: Boolean; + out AString: String; out ANumBytes: Integer; + IgnoreCompressedFlag: Boolean=False); overload; procedure PopulateGrid; procedure ShowInRow(var ARow: Integer; var AOffs: LongWord; ASize: Word; AValue,ADescr: String; ADescrOnly: Boolean = false); @@ -225,7 +231,7 @@ end; The string is assumed to be a UTF16 string if AUnicode=true, otherwise it is an ansi string. } procedure TBIFFGrid.ExtractString(ABufIndex: Integer; AUnicode: Boolean; - ACharCount: Integer;out AString: String; out ANumbytes: Integer); + ACharCount: Integer; out AString: String; out ANumbytes: Integer); var sa: AnsiString; sw: WideString; @@ -265,7 +271,21 @@ begin end; procedure TBIFFGrid.ExtractString(ABufIndex: Integer; ALenBytes: Byte; AUnicode: Boolean; - out AString: String; out ANumBytes: Integer; IgnoreCompressedFlag: Boolean = false); + out AString: String; out ANumBytes: Integer; + IgnoreCompressedFlag: Boolean = false); +var + rtfRuns: TsRichTextFormattingRuns; + rtfIndex: LongWord; +begin + ExtractString(ABufIndex, ALenbytes, AUnicode, AString, ANumBytes, + rtfRuns, rtfIndex, IgnoreCompressedFlag); +end; + +procedure TBIFFGrid.ExtractString(ABufIndex: Integer; ALenBytes: Byte; AUnicode: Boolean; + out AString: String; out ANumBytes: Integer; + out ARichTextRuns: TsRichTextFormattingRuns; + out ABufIndexOfFirstRichTextRun: LongWord; + IgnoreCompressedFlag: Boolean = false); var ls: Integer; // Character count of string w: Word; @@ -273,12 +293,17 @@ var optn: Byte; n: Integer; // Byte count in string character array asianPhoneticBytes: DWord; - richRuns: Word; + numRichRuns: Word; offs: Integer; + rtfBufIndex: Int64; + rtfIndex: Integer; begin + ABufIndexOfFirstRichTextRun := LongWord(-1); + SetLength(ARichTextRuns, 0); + if Length(FBuffer) = 0 then begin AString := ''; - ANumBytes := 0; + ANumBytes := 0; exit; end; if ALenBytes = 1 then @@ -291,13 +316,16 @@ begin offs := ALenBytes; optn := FBuffer[ABufIndex + ALenBytes]; inc(offs, 1); + if optn and $08 <> 0 then // rich text begin Move(FBuffer[ABufIndex + offs], w, 2); - richRuns := WordLEToN(w); + numRichRuns := WordLEToN(w); inc(offs, 2); end else - richRuns := 0; + numRichRuns := 0; + SetLength(ARichTextRuns, numRichRuns); + if optn and $04 <> 0 then // Asian phonetic begin Move(FBuffer[ABufIndex + offs], dw, 4); @@ -305,16 +333,31 @@ begin inc(offs, 4); end else asianPhoneticBytes := 0; + if (optn and $01 = 0) and (not IgnoreCompressedFlag) then // compressed --> 1 byte per character ExtractString(ABufIndex + offs, false, ls, AString, n) else // non-compressed unicode ExtractString(ABufIndex + offs, true, ls, AString, n); - ANumBytes := offs + n + richRuns * 4 + asianPhoneticBytes; + + ANumBytes := offs + n + numRichRuns * 4 + asianPhoneticBytes; + + rtfIndex := 0; + rtfBufIndex := ABufIndex + offs + n; + ABufIndexOfFirstRichTextRun := rtfBufIndex; + while rtfIndex < numRichRuns do begin + Move(FBuffer[rtfBufIndex], w, 2); + ARichTextRuns[rtfIndex].FirstIndex := WordLEToN(w); + Move(FBuffer[rtfBufIndex+2], w, 2); + ARichTextRuns[rtfIndex].FontIndex := WordLEToN(w); + inc(rtfIndex); + inc(rtfBufIndex, 4); + end; end else begin // ansi string + SetLength(ARichTextRuns, 0); // no rich text formatting for ansi strings ExtractString(ABufIndex + ALenBytes, false, ls, AString, n); ANumbytes := ALenBytes + n; end; @@ -471,6 +514,8 @@ begin ShowMulBlank; $00BD: ShowMulRK; + $00D6: + ShowRString; $00D7: ShowDBCell; $00DA: @@ -1235,12 +1280,14 @@ var sa: ansistring; sw: widestring; ls: Integer; - i: Integer; + i, j: Integer; w: Word; n: Integer; run: Integer; total2: Integer; optn: Byte; + rtfRuns: TsRichTextFormattingRuns; + rtfBufferIndex: LongWord; begin case FInfo of BIFFNODE_TXO_CONTINUE1: @@ -1345,9 +1392,20 @@ begin for i:=FCounterSST+1 to FTotalSST do begin FCounterSST := i; - ExtractString(FBufferIndex, 2, true, s, numBytes); + ExtractString(FBufferIndex, 2, true, s, numBytes, rtfRuns, rtfBufferIndex); ShowInRow(FCurrRow, FBufferIndex, numBytes, s, Format('Shared string #%d', [i])); inc(n); + if Length(rtfRuns) > 0 then begin + numBytes := 2; + for j:=0 to High(rtfRuns) do + begin + ShowInRow(FCurrRow, rtfBufferIndex, 2, IntToStr(rtfRuns[j].FirstIndex), + Format('Rich-Text formatting run #%d, index of first character', [j])); + ShowInRow(FCurrRow, rtfBufferIndex, 2, IntToStr(rtfRuns[j].FontIndex), + Format('Rich-Text formatting run #%d, font index', [j])); + inc(n, 2); + end; + end; if FPendingCharCount > 0 then begin FInfo := BIFFNODE_SST_CONTINUE; @@ -1838,6 +1896,7 @@ var ansiStr: AnsiString; s: String; i, n: Integer; + rtfRuns: TsRichTextFormattingRuns; begin BeginUpdate; RowCount := FixedRows + 1000; @@ -2112,7 +2171,7 @@ begin numBytes := 2; Move(FBuffer[FBufferIndex], w, numBytes); w := WordLEToN(w); - ShowInRow(FCurrRow, FBufferIndex, numBytes, IntToStr(2), 'Color index'); + ShowInRow(FCurrRow, FBufferIndex, numBytes, IntToStr(w), 'Color index'); numBytes := 2; Move(FBuffer[FBufferIndex], w, numBytes); @@ -3309,6 +3368,7 @@ begin 'Index to XF record'); end; +// Called for LABEL procedure TBIFFGrid.ShowLabelCell; var numBytes: Integer; @@ -4997,6 +5057,72 @@ begin end; +procedure TBIFFGrid.ShowRString; +var + numBytes: Integer; + b: Byte; + w: Word; + s: String; + len: Integer; + j: Integer; + wideStr: wideString; + ansiStr: ansiString; +begin + if FFormat < sfExcel5 then + exit; + + RowCount := FixedRows + 5; + + ShowRowColData(FBufferIndex); + + numBytes := 2; + Move(FBuffer[FBufferIndex], w, numBytes); + w := WordLEToN(w); + ShowInRow(FCurrRow, FBufferIndex, numBytes, Format('%d ($%.4x)', [w, w]), + 'Index of XF record'); + + // String length + Move(FBuffer[FBufferIndex], w, 2); + len := WordLEToN(w); + + if FFormat = sfExcel8 then + begin + SetLength(widestr, len); + Move(FBuffer[FBufferIndex+3], widestr[1], len*2); + s := UTF8Encode(WideStringLEToN(widestr)); + numbytes := 3 + len*2; + end else + begin + SetLength(ansistr, len); + Move(FBuffer[FBufferIndex+2], ansistr[1], len); + s := AnsiToUTF8(ansistr); + numbytes := 2 + len; + end; + + ShowInRow(FCurrRow, FBufferIndex, numbytes, s, + Format('%s string, 16-bit string length', [GetStringType])); + + // Number of rich-text formatting runs + numbytes := IfThen(FFormat = sfExcel8, 2, 1); + Move(FBuffer[FBufferIndex], w, numbytes); + len := WordLEToN(w); + ShowInRow(FCurrRow, FBufferIndex, numbytes, IntToStr(len), + 'Count of rich-text formatting runs'); + + // Formatting run data + RowCount := RowCount + 2*len; + for j:=0 to len-1 do + begin + Move(FBuffer[FBufferIndex], w, numbytes); + ShowInRow(FCurrRow, FBufferIndex, numbytes, IntToStr(WordLEToN(w)), + Format('Rich-Text formatting run #%d, index of first character', [j])); + Move(FBuffer[FBufferIndex], w, numbytes); + ShowInRow(FCurrRow, FBufferIndex, numbytes, IntToStr(WordLEToN(w)), + Format('Rich-Text formatting run #%d, font index', [j])); + end; +end; + + procedure TBIFFGrid.ShowSelection; var numBytes: Integer; @@ -5195,7 +5321,10 @@ var numBytes: Integer; s: String; total1, total2: DWord; - i, n: Integer; + i, j, n: Integer; + rtfRuns: TsRichTextFormattingRuns; + rtfIndex: LongWord; + w: Word; begin numBytes := 4; Move(FBuffer[FBufferIndex], total1, numBytes); @@ -5204,7 +5333,7 @@ begin total2 := DWordLEToN(total2); FTotalSST := total2; - RowCount := FixedRows + 2 + total2; + RowCount := FixedRows + 1000; ShowInRow(FCurrRow, FBufferIndex, numBytes, IntToStr(total1), 'Total number of shared strings in the workbook'); @@ -5215,10 +5344,22 @@ begin n := 0; for i:=1 to FTotalSST do begin FCounterSST := i; - ExtractString(FBufferIndex, 2, true, s, numBytes); // BIFF8 only --> 2 length bytes + ExtractString(FBufferIndex, 2, true, s, numBytes, rtfRuns, rtfIndex); // BIFF8 only --> 2 length bytes inc(n); - if FPendingCharCount = 0 then - ShowInRow(FCurrRow, FBufferIndex, numBytes, s, Format('Shared string #%d', [i])) + if FPendingCharCount = 0 then begin + ShowInRow(FCurrRow, FBufferIndex, numbytes, s, IfThen(Length(rtfRuns) > 0, + Format('Shared string #%d (Count of Rich-Text formatting runs: %d)', [i, Length(rtfRuns)]), + Format('Shared string #%d', [i]))); +// ShowInRow(FCurrRow, FBufferIndex, numBytes, s, Format('Shared string #%d', [i])); + for j:=0 to High(rtfRuns) do + begin + ShowInRow(FCurrRow, rtfIndex, 2, IntToStr(rtfRuns[j].FirstIndex), + Format(' Rich-Text formatting run #%d, index of first character', [j])); + ShowInRow(FCurrRow, rtfIndex, 2, IntToStr(rtfRuns[j].FontIndex), + Format(' Rich-Text formatting run #%d, font index', [j])); + inc(n, 2); + end; + end else begin ShowInRow(FCurrRow, FBufferIndex, numbytes, s, Format('Shared string #%d - partial (--> CONTINUE)', [i])); diff --git a/components/fpspreadsheet/xlsbiff5.pas b/components/fpspreadsheet/xlsbiff5.pas index e027a9727..4c88a6347 100755 --- a/components/fpspreadsheet/xlsbiff5.pas +++ b/components/fpspreadsheet/xlsbiff5.pas @@ -73,9 +73,6 @@ type { TsSpreadBIFF5Reader } TsSpreadBIFF5Reader = class(TsSpreadBIFFReader) - private - FWorksheetNames: TStringList; - FCurrentWorksheet: Integer; protected procedure PopulatePalette; override; { Record writing methods } @@ -83,15 +80,15 @@ type procedure ReadFONT(const AStream: TStream); procedure ReadFORMAT(AStream: TStream); override; procedure ReadLABEL(AStream: TStream); override; - procedure ReadWorkbookGlobals(AStream: TStream); - procedure ReadWorksheet(AStream: TStream); - procedure ReadRichString(AStream: TStream); + procedure ReadRSTRING(AStream: TStream); procedure ReadStandardWidth(AStream: TStream; ASheet: TsWorksheet); procedure ReadStringRecord(AStream: TStream); override; + procedure ReadWorkbookGlobals(AStream: TStream); override; + procedure ReadWorksheet(AStream: TStream); override; procedure ReadXF(AStream: TStream); public { General reading methods } - procedure ReadFromFile(AFileName: string); override; +// procedure ReadFromFile(AFileName: string); override; procedure ReadFromStream(AStream: TStream); override; end; @@ -323,6 +320,13 @@ type TextLen: Word; end; + TBiff5_RichTextFormattingRun = packed record + FirstIndex: Byte; + FontIndex: Byte; + end; + + TBiff5_RichTextFormattingRuns = array of TBiff5_RichTextFormattingRun; + TBIFF5_XFRecord = packed record RecordID: Word; RecordSize: Word; @@ -426,7 +430,7 @@ begin INT_EXCEL_ID_RIGHTMARGIN : ReadMargin(AStream, 1); INT_EXCEL_ID_RK : ReadRKValue(AStream); //(RK) This record represents a cell that contains an RK value (encoded integer or floating-point value). If a floating-point value cannot be encoded to an RK value, a NUMBER record will be written. This record replaces the record INTEGER written in BIFF2. INT_EXCEL_ID_ROW : ReadRowInfo(AStream); - INT_EXCEL_ID_RSTRING : ReadRichString(AStream); //(RSTRING) This record stores a formatted text cell (Rich-Text). In BIFF8 it is usually replaced by the LABELSST record. Excel still uses this record, if it copies formatted text cells to the clipboard. + INT_EXCEL_ID_RSTRING : ReadRString(AStream); //(RSTRING) This record stores a formatted text cell (Rich-Text). In BIFF8 it is usually replaced by the LABELSST record. Excel still uses this record, if it copies formatted text cells to the clipboard. INT_EXCEL_ID_SHAREDFMLA : ReadSharedFormula(AStream); INT_EXCEL_ID_SHEETPR : ReadSHEETPR(AStream); INT_EXCEL_ID_STANDARDWIDTH : ReadStandardWidth(AStream, FWorksheet); @@ -508,12 +512,11 @@ begin SetLength(s, Len); AStream.ReadBuffer(s[1], Len*SizeOf(AnsiChar)); -// sheetName := AnsiToUTF8(s); sheetName := ConvertEncoding(s, FCodePage, EncodingUTF8); FWorksheetNames.Add(sheetName); end; -procedure TsSpreadBIFF5Reader.ReadRichString(AStream: TStream); +procedure TsSpreadBIFF5Reader.ReadRSTRING(AStream: TStream); var L: Word; B, F: Byte; @@ -593,6 +596,7 @@ begin FIncompleteCell := nil; end; +(* procedure TsSpreadBIFF5Reader.ReadFromFile(AFileName: string); var MemStream: TMemoryStream; @@ -620,7 +624,7 @@ begin OLEStorage.Free; end; end; - +*) procedure TsSpreadBIFF5Reader.ReadXF(AStream: TStream); var rec: TBIFF5_XFRecord; @@ -642,15 +646,7 @@ begin AStream.ReadBuffer(rec.FontIndex, SizeOf(rec) - 2*SizeOf(Word)); // Font index - i := WordLEToN(rec.FontIndex); -// if i > 4 then dec(i); // Watch out for the nasty missing font #4... - fmt.FontIndex := FixFontIndex(i); - { - fnt := TsFont(FFontList[i]); - fmt.FontIndex := Workbook.FindFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color); - if fmt.FontIndex = -1 then - fmt.FontIndex := Workbook.AddFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color); - } + fmt.FontIndex := FixFontIndex(WordLEToN(rec.FontIndex)); if fmt.FontIndex > 1 then Include(fmt.UsedFormattingFields, uffFont); @@ -780,9 +776,94 @@ begin FCellFormatList.Add(fmt); end; +procedure TsSpreadBIFF5Reader.ReadFromStream(AStream: TStream); +var + OLEStream: TMemoryStream; + OLEStorage: TOLEStorage; + OLEDocument: TOLEDocument; +begin + OLEStream := TMemoryStream.Create; + try + OLEStorage := TOLEStorage.Create; + try + // Only one stream is necessary for any number of worksheets + OLEDocument.Stream := OLEStream; + OLEStorage.ReadOLEStream(AStream, OLEDocument, 'Book'); + finally + OLEStorage.Free; + end; + InternalReadFromStream(OLEStream); + finally + OLEStream.Free; + end; +end; + +(* procedure TsSpreadBIFF5Reader.ReadFromStream(AStream: TStream); var BIFF5EOF: Boolean; + OLEStream: TMemoryStream; + OLEStorage: TOLEStorage; + OLEDocument: TOLEDocument; +begin + OLEStream := TMemoryStream.Create; + try + OLEStorage := TOLEStorage.Create; + try + // Only one stream is necessary for any number of worksheets + OLEDocument.Stream := OLEStream; + OLEStorage.ReadOLEStream(AStream, OLEDocument); + finally + OLEStorage.Free; + end; + + // Check if the operation succeeded + if OLEStream.Size = 0 then + raise Exception.Create('[TsSpreadBIFF5Reader.ReadFromFile] Reading of OLE document failed'); + + // Rewind the stream and read from it + OLEStream.Position := 0; + + {Initializations } + FWorksheetNames := TStringList.Create; + try + FCurrentWirksheet := 0; + BIFF5EOF := false; + + { Read workbook globals } + ReadWorkbookGlobals(OLEStream); + + { Check for the end of the file } + if OLEStream.Position >= AStream.Size then + BIFF5EOF := true; + + { Now read all worksheets } + while not BIFF5EOF do + begin + ReadWorksheet(OLEStream); + + // Check for the end of the fild + if OLEStream.Position >= OLEStream.Size then + BIFF5EOF := true; + + // Final preparations + inc(FCurrentWorksheet); + // It can happen in files written by Office97 that the OLE directory is + // at the end of the file. + if FCurrentWorksheet = FWorksheetNames.Count then + BIFF5EOF := true; + end; + + finally + { Finalization } + FreeAndNil(FWorksheetNames); + end; + finally + OLEStream.Free; + end; +end; + *) +(* begin { Initializations } @@ -815,6 +896,7 @@ begin { Finalization } FWorksheetNames.Free; end; +*) procedure TsSpreadBIFF5Reader.ReadFont(const AStream: TStream); var @@ -1253,7 +1335,7 @@ begin AStream.WriteWord(WordToLE(INT_FONT_WEIGHT_NORMAL)); { Escapement type } - AStream.WriteWord(0); + AStream.WriteWord(WordToLE(ord(AFont.Position))); { Underline type } if fssUnderline in AFont.Style then @@ -1369,12 +1451,16 @@ end; procedure TsSpreadBIFF5Writer.WriteLabel(AStream: TStream; const ARow, ACol: Cardinal; const AValue: string; ACell: PCell); const - MAXBYTES = 255; //limit for this format + MAXBYTES = 255; // Limit for this BIFF5 var L: Word; AnsiValue: ansistring; rec: TBIFF5_LabelRecord; buf: array of byte; + useRTF: Boolean; + fmt: PsCellFormat; + run, j: Integer; + rtfRuns: TBiff5_RichTextformattingRuns; begin if (ARow >= FLimitations.MaxRowCount) or (ACol >= FLimitations.MaxColCount) then exit; @@ -1401,9 +1487,40 @@ begin end; L := Length(AnsiValue); + useRTF := (Length(ACell^.RichTextParams) > 0); + { BIFF record header } - rec.RecordID := WordToLE(INT_EXCEL_ID_LABEL); - rec.RecordSize := WordToLE(8 + L); + rec.RecordID := WordToLE(IfThen(useRTF, INT_EXCEL_ID_RSTRING, INT_EXCEL_ID_LABEL)); + rec.RecordSize := WordToLE(SizeOf(rec) - SizeOf(TsBIFFHeader) + L); + + { Prepare rich-text formatting runs } + if useRTF then + begin + fmt := FWorkbook.GetPointerToCellFormat(ACell^.FormatIndex); + run := 0; + for j:=0 to High(ACell^.RichTextParams) do + begin + SetLength(rtfRuns, run + 1); + rtfRuns[run].FirstIndex := ACell^.RichTextParams[j].StartIndex; + rtfRuns[run].FontIndex := ACell^.RichTextParams[j].FontIndex; + if rtfRuns[run].FontIndex >= 4 then + inc(rtfRuns[run].FontIndex); // Font #4 does not exist in BIFF + inc(run); + if (ACell^.RichTextParams[j].EndIndex < L) and + (ACell^.RichTextParams[j].EndIndex <> ACell^.RichTextParams[j+1].StartIndex) // wp: j+1 needs to be checked! + then begin + SetLength(rtfRuns, run+1); + rtfRuns[run].FirstIndex := ACell^.RichTextParams[j].EndIndex; + rtfRuns[run].FontIndex := fmt^.FontIndex; + if rtfRuns[run].FontIndex >= 4 then + inc(rtfRuns[run].FontIndex); + inc(run); + end; + end; + + // Adjust BIFF record size for appended formatting runs + inc(rec.RecordSize, SizeOf(byte) + run * SizeOf(TBiff5_RichTextFormattingRun)); + end; { BIFF record data } rec.Row := WordToLE(ARow); @@ -1416,15 +1533,25 @@ begin rec.TextLen := WordToLE(L); { Copy the text characters into a buffer immediately after rec } - SetLength(buf, SizeOf(rec) + SizeOf(ansiChar)*L); + SetLength(buf, SizeOf(rec) + L); Move(rec, buf[0], SizeOf(rec)); - Move(AnsiValue[1], buf[SizeOf(rec)], L*SizeOf(ansiChar)); + Move(AnsiValue[1], buf[SizeOf(rec)], L); - { Write out } - AStream.WriteBuffer(buf[0], SizeOf(Rec) + SizeOf(ansiChar)*L); + { Write out buffer } + AStream.WriteBuffer(buf[0], SizeOf(Rec) + L); + + { Write rich-text information in case of RSTRING record } + if useRTF then + begin + { Write number of rich-text formatting runs } + AStream.WriteByte(run); + { Write rich-text formatting runs } + AStream.WriteBuffer(rtfRuns[0], run * SizeOf(TBiff5_RichTextFormattingRun)); + end; { Clean up } SetLength(buf, 0); + SetLength(rtfRuns, 0); end; {@@ ---------------------------------------------------------------------------- diff --git a/components/fpspreadsheet/xlsbiff8.pas b/components/fpspreadsheet/xlsbiff8.pas index 1fef0cbb7..7597edf89 100755 --- a/components/fpspreadsheet/xlsbiff8.pas +++ b/components/fpspreadsheet/xlsbiff8.pas @@ -70,19 +70,19 @@ type TsSpreadBIFF8Reader = class(TsSpreadBIFFReader) private PendingRecordSize: SizeInt; - FWorksheetNames: TStringList; - FCurrentWorksheet: Integer; FSharedStringTable: TStringList; FCommentList: TObjectList; FCommentPending: Boolean; FCommentID: Integer; FCommentLen: Integer; - function ReadWideString(const AStream: TStream; const ALength: WORD): WideString; overload; - function ReadWideString(const AStream: TStream; const AUse8BitLength: Boolean): WideString; overload; - procedure ReadWorkbookGlobals(AStream: TStream); - procedure ReadWorksheet(AStream: TStream); procedure ReadBoundsheet(AStream: TStream); - function ReadString(const AStream: TStream; const ALength: WORD): String; + function ReadString(const AStream: TStream; const ALength: Word; + out ARichTextRuns: TsRichTextFormattingRuns): String; + function ReadUnformattedWideString(const AStream: TStream; + const ALength: Word): WideString; + function ReadWideString(const AStream: TStream; const ALength: Word; + out ARichTextRuns: TsRichTextFormattingRuns): WideString; overload; + function ReadWideString(const AStream: TStream; const AUse8BitLength: Boolean): WideString; overload; protected procedure PopulatePalette; override; procedure ReadCONTINUE(const AStream: TStream); @@ -96,7 +96,7 @@ type procedure ReadMergedCells(const AStream: TStream); procedure ReadNOTE(const AStream: TStream); procedure ReadOBJ(const AStream: TStream); - procedure ReadRichString(const AStream: TStream); +// procedure ReadRichString(const AStream: TStream); procedure ReadRPNCellAddress(AStream: TStream; out ARow, ACol: Cardinal; out AFlags: TsRelFlags); override; procedure ReadRPNCellAddressOffset(AStream: TStream; @@ -106,15 +106,18 @@ type procedure ReadRPNCellRangeOffset(AStream: TStream; out ARow1Offset, ACol1Offset, ARow2Offset, ACol2Offset: Integer; out AFlags: TsRelFlags); override; + procedure ReadRSTRING(AStream: TStream); procedure ReadSST(const AStream: TStream); function ReadString_8bitLen(AStream: TStream): String; override; procedure ReadStringRecord(AStream: TStream); override; procedure ReadTXO(const AStream: TStream); + procedure ReadWorkbookGlobals(AStream: TStream); override; + procedure ReadWorksheet(AStream: TStream); override; procedure ReadXF(const AStream: TStream); public destructor Destroy; override; { General reading methods } - procedure ReadFromFile(AFileName: string); override; +// procedure ReadFromFile(AFileName: string); override; procedure ReadFromStream(AStream: TStream); override; end; @@ -141,7 +144,7 @@ type procedure WriteHyperlinkToolTip(AStream: TStream; const ARow, ACol: Cardinal; const ATooltip: String); procedure WriteIndex(AStream: TStream); - procedure WriteLabel(AStream: TStream; const ARow, ACol: Cardinal; + procedure WriteLABEL(AStream: TStream; const ARow, ACol: Cardinal; const AValue: string; ACell: PCell); override; procedure WriteMergedCells(AStream: TStream; AWorksheet: TsWorksheet); procedure WriteMSODrawing1(AStream: TStream; ANumShapes: Word; AComment: PsComment); @@ -388,6 +391,13 @@ type SSTIndex: DWord; end; + TBiff8_RichTextFormattingRun = packed record + FirstIndex: Word; + FontIndex: Word; + end; + + TBiff8_RichTextFormattingRuns = array of TBiff8_RichTextFormattingRun; + TBIFF8_XFRecord = packed record RecordID: Word; RecordSize: Word; @@ -426,12 +436,24 @@ type { TsSpreadBIFF8Reader } destructor TsSpreadBIFF8Reader.Destroy; +var + j: Integer; begin - if Assigned(FSharedStringTable) then FSharedStringTable.Free; - if Assigned(FCommentList) then FCommentList.Free; + if Assigned(FSharedStringTable) then + begin + for j := FSharedStringTable.Count-1 downto 0 do + if FSharedStringTable.Objects[j] <> nil then + FSharedStringTable.Objects[j].Free; + FSharedStringTable.Free; + end; + + if Assigned(FCommentList) then + FCommentList.Free; + inherited; end; + {@@ ---------------------------------------------------------------------------- Populates the reader's default palette using the BIFF8 default colors. -------------------------------------------------------------------------------} @@ -451,9 +473,10 @@ procedure TsSpreadBIFF8Reader.ReadCONTINUE(const AStream: TStream); var commentStr: String; comment: TBIFF8Comment; + rtRuns: TsRichTextFormattingRuns; begin if FCommentPending then begin - commentStr := ReadWideString(AStream, FCommentLen); + commentStr := ReadWideString(AStream, FCommentLen, rtRuns); if commentStr <> '' then begin comment := TBIFF8Comment.Create; @@ -529,8 +552,107 @@ begin end; end; -function TsSpreadBIFF8Reader.ReadWideString(const AStream: TStream; +{ Reads a unicode string which does not contain rich-text information. + This is needed for the RSTRING record. } +function TsSpreadBIFF8Reader.ReadUnformattedWideString(const AStream: TStream; const ALength: WORD): WideString; +var + flags: Byte; + DecomprStrValue: WideString; + AnsiStrValue: ansistring; + //RunsCounter: Word; + //AsianPhoneticBytes: DWord; + i: Integer; + j: SizeUInt; + len: SizeInt; + recType: Word; + recSize: Word; + C: WideChar; +begin + flags := AStream.ReadByte; + dec(PendingRecordSize); + { + if StringFlags and 4 = 4 then begin + //Asian phonetics + //Read Asian phonetics Length (not used) + AsianPhoneticBytes := DWordLEtoN(AStream.ReadDWord); + dec(PendingRecordSize,4); + end; + if StringFlags and 8 = 8 then begin + //Rich string + RunsCounter := WordLEtoN(AStream.ReadWord); + dec(PendingRecordSize,2); + end; + } + if flags and 1 = 1 Then begin + //String is WideStringLE + if (ALength * SizeOf(WideChar)) > PendingRecordSize then begin + SetLength(Result, PendingRecordSize div 2); + AStream.ReadBuffer(Result[1], PendingRecordSize); + Dec(PendingRecordSize, PendingRecordSize); + end else begin + SetLength(Result, ALength); + AStream.ReadBuffer(Result[1], ALength * SizeOf(WideChar)); + Dec(PendingRecordSize, ALength * SizeOf(WideChar)); + end; + Result := WideStringLEToN(Result); + end else begin + // String is 1 byte per char, this is UTF-16 with the high byte ommited + // because it is zero, so decompress and then convert + len := ALength; + SetLength(DecomprStrValue, len); + for i := 1 to len do + begin + C := WideChar(AStream.ReadByte); // Read 1 byte, but put it into a 2-byte char + DecomprStrValue[i] := C; + dec(PendingRecordSize); + if (PendingRecordSize <= 0) and (i < len) then begin + //A CONTINUE may have happened here + recType := WordLEToN(AStream.ReadWord); + recSize := WordLEToN(AStream.ReadWord); + if recType <> INT_EXCEL_ID_CONTINUE then begin + raise Exception.Create('[TsSpreadBIFF8Reader.ReadWideString] Expected CONTINUE record not found.'); + end else begin + PendingRecordSize := RecordSize; + DecomprStrValue := copy(DecomprStrValue,1,i) + ReadUnformattedWideString(AStream, ALength-i); + break; + end; + end; + end; + Result := DecomprStrValue; + end; + { + if StringFlags and 8 = 8 then begin + // Rich string (This only occurs in BIFF8) + SetLength(ARichTextRuns, RunsCounter); + for j := 0 to RunsCounter - 1 do begin + if (PendingRecordSize <= 0) then begin + // A CONTINUE may happened here + RecordType := WordLEToN(AStream.ReadWord); + RecordSize := WordLEToN(AStream.ReadWord); + if RecordType <> INT_EXCEL_ID_CONTINUE then begin + Raise Exception.Create('[TsSpreadBIFF8Reader.ReadWideString] Expected CONTINUE record not found.'); + end else begin + PendingRecordSize := RecordSize; + end; + end; + ARichTextRuns[j].FirstIndex := WordLEToN(AStream.ReadWord); + ARichTextRuns[j].FontIndex := WordLEToN(AStream.ReadWord); + dec(PendingRecordSize, 2*2); + end; + end; + if StringFlags and 4 = 4 then begin + //Asian phonetics + //Read Asian phonetics, discarded as not used. + SetLength(AnsiStrValue, AsianPhoneticBytes); + AStream.ReadBuffer(AnsiStrValue[1], AsianPhoneticBytes); + dec(PendingRecordSize, AsianPhoneticBytes); + end; + } +end; + +function TsSpreadBIFF8Reader.ReadWideString(const AStream: TStream; + const ALength: WORD; out ARichTextRuns: TsRichTextFormattingRuns): WideString; var StringFlags: BYTE; DecomprStrValue: WideString; @@ -544,21 +666,21 @@ var RecordSize: WORD; C: WideChar; begin - StringFlags:=AStream.ReadByte; + StringFlags := AStream.ReadByte; Dec(PendingRecordSize); if StringFlags and 4 = 4 then begin - //Asian phonetics - //Read Asian phonetics Length (not used) + // Asian phonetics + // Read Asian phonetics Length (not used) AsianPhoneticBytes := DWordLEtoN(AStream.ReadDWord); dec(PendingRecordSize,4); end; if StringFlags and 8 = 8 then begin - //Rich string + // Rich string RunsCounter := WordLEtoN(AStream.ReadWord); dec(PendingRecordSize,2); end; if StringFlags and 1 = 1 Then begin - //String is WideStringLE + // String is WideStringLE if (ALength*SizeOf(WideChar)) > PendingRecordSize then begin SetLength(Result, PendingRecordSize div 2); AStream.ReadBuffer(Result[1], PendingRecordSize); @@ -584,10 +706,10 @@ begin RecordType := WordLEToN(AStream.ReadWord); RecordSize := WordLEToN(AStream.ReadWord); if RecordType <> INT_EXCEL_ID_CONTINUE then begin - Raise Exception.Create('[TsSpreadBIFF8Reader.ReadWideString] Expected CONTINUE record not found.'); + Raise Exception.Create('[TsSpreadBIFF8Reader.ReadWideString] CONTINUE record expected, but not found.'); end else begin PendingRecordSize := RecordSize; - DecomprStrValue := copy(DecomprStrValue,1,i) + ReadWideString(AStream, ALength-i); + DecomprStrValue := copy(DecomprStrValue,1,i) + ReadWideString(AStream, ALength-i, ARichTextRuns); break; end; end; @@ -595,26 +717,27 @@ begin Result := DecomprStrValue; end; if StringFlags and 8 = 8 then begin - //Rich string (This only happened in BIFF8) - for j := 1 to RunsCounter do begin + // Rich string (This only occurs in BIFF8) + SetLength(ARichTextRuns, RunsCounter); + for j := 0 to RunsCounter - 1 do begin if (PendingRecordSize <= 0) then begin - //A CONTINUE may happened here + // A CONTINUE may happened here RecordType := WordLEToN(AStream.ReadWord); RecordSize := WordLEToN(AStream.ReadWord); if RecordType <> INT_EXCEL_ID_CONTINUE then begin - Raise Exception.Create('[TsSpreadBIFF8Reader.ReadWideString] Expected CONTINUE record not found.'); + Raise Exception.Create('[TsSpreadBIFF8Reader.ReadWideString] CONTINUE record expected, but not found.'); end else begin PendingRecordSize := RecordSize; end; end; - AStream.ReadWord; - AStream.ReadWord; + ARichTextRuns[j].FirstIndex := WordLEToN(AStream.ReadWord); + ARichTextRuns[j].FontIndex := WordLEToN(AStream.ReadWord); dec(PendingRecordSize, 2*2); end; end; if StringFlags and 4 = 4 then begin - //Asian phonetics - //Read Asian phonetics, discarded as not used. + // Asian phonetics + // Read Asian phonetics, discarded as not used. SetLength(AnsiStrValue, AsianPhoneticBytes); AStream.ReadBuffer(AnsiStrValue[1], AsianPhoneticBytes); dec(PendingRecordSize, AsianPhoneticBytes); @@ -625,13 +748,14 @@ function TsSpreadBIFF8Reader.ReadWideString(const AStream: TStream; const AUse8BitLength: Boolean): WideString; var Len: Word; + rtRuns: TsRichTextFormattingRuns; begin if AUse8BitLength then Len := AStream.ReadByte() else Len := WordLEtoN(AStream.ReadWord()); - Result := ReadWideString(AStream, Len); + Result := ReadWideString(AStream, Len, rtRuns); end; procedure TsSpreadBIFF8Reader.ReadWorkbookGlobals(AStream: TStream); @@ -730,7 +854,7 @@ begin //(RSTRING) This record stores a formatted text cell (Rich-Text). // In BIFF8 it is usually replaced by the LABELSST record. Excel still // uses this record, if it copies formatted text cells to the clipboard. - INT_EXCEL_ID_RSTRING : ReadRichString(AStream); + INT_EXCEL_ID_RSTRING : ReadRSTRING(AStream); // (RK) This record represents a cell that contains an RK value // (encoded integer or floating-point value). If a floating-point @@ -764,6 +888,7 @@ procedure TsSpreadBIFF8Reader.ReadBoundsheet(AStream: TStream); var Len: Byte; WideName: WideString; + rtRuns: TsRichTextFormattingRuns; begin { Absolute stream position of the BOF record of the sheet represented by this record } @@ -780,15 +905,27 @@ begin Len := AStream.ReadByte(); { Read string with flags } - WideName:=ReadWideString(AStream,Len); + WideName:=ReadWideString(AStream, Len, rtRuns); FWorksheetNames.Add(UTF8Encode(WideName)); end; function TsSpreadBIFF8Reader.ReadString(const AStream: TStream; - const ALength: WORD): String; + const ALength: WORD; out ARichTextRuns: TsRichTextFormattingRuns): String; begin - Result := UTF16ToUTF8(ReadWideString(AStream, ALength)); + Result := UTF16ToUTF8(ReadWideString(AStream, ALength, ARichTextRuns)); +end; + (* +procedure TsSpreadBIFF8Reader.ReadFromFile(AFileName: String); +var + FileStream: TFileStream; +begin + FileStream := TFileStream.Create(AFilename, fmOpenRead or fmShareDenyNone); + try + ReadFromStream(FileStream); + finally + FileStream.Free; + end; end; procedure TsSpreadBIFF8Reader.ReadFromFile(AFileName: string); @@ -819,7 +956,32 @@ begin OLEStorage.Free; end; end; + *) +procedure TsSpreadBIFF8Reader.ReadFromStream(AStream: TStream); +var + OLEStream: TMemoryStream; + OLEStorage: TOLEStorage; + OLEDocument: TOLEDocument; +begin + OLEStream := TMemoryStream.Create; + try + // Only one stream is necessary for any number of worksheets + OLEStorage := TOLEStorage.Create; + try + OLEDocument.Stream := OLEStream; + OLEStorage.ReadOLEStream(AStream, OLEDocument, 'Workbook'); + finally + OLEStorage.Free; + end; + InternalReadFromStream(OLEStream); + + finally + OLEStream.Free; + end; +end; +(* + const AStrea procedure TsSpreadBIFF8Reader.ReadFromStream(AStream: TStream); var BIFF8EOF: Boolean; @@ -861,14 +1023,16 @@ begin { Finalizations } FWorksheetNames.Free; end; + *) procedure TsSpreadBIFF8Reader.ReadLABEL(AStream: TStream); var L: Word; ARow, ACol: Cardinal; XF: Word; - WideStrValue: WideString; + wideStrValue: WideString; cell: PCell; + rtfRuns: TsRichTextFormattingRuns; begin { BIFF Record data: Row, Column, XF Index } ReadRowColXF(AStream, ARow, ACol, XF); @@ -876,8 +1040,8 @@ begin { Byte String with 16-bit size } L := WordLEtoN(AStream.ReadWord()); - { Read string with flags } - WideStrValue:=ReadWideString(AStream,L); + { Read wide string with flags } + wideStrValue := ReadWideString(AStream, L, rtfRuns); { Save the data } if FIsVirtualMode then begin @@ -886,10 +1050,11 @@ begin end else cell := FWorksheet.AddCell(ARow, ACol); // "real" cell - FWorksheet.WriteUTF8Text(cell, UTF16ToUTF8(WideStrValue)); + FWorksheet.WriteUTF8Text(cell, UTF16ToUTF8(wideStrValue)); {Add attributes} ApplyCellFormatting(cell, XF); + ApplyRichTextFormattingRuns(cell, rtfRuns); if FIsVirtualMode then Workbook.OnReadCellData(Workbook, ARow, ACol, cell); @@ -915,14 +1080,14 @@ begin ); end; end; - + (* procedure TsSpreadBIFF8Reader.ReadRichString(const AStream: TStream); var L: Word; B: WORD; ARow, ACol: Cardinal; XF: Word; - AStrValue: ansistring; + strValue: string; cell: PCell; rtfRuns: TsRichTextFormattingRuns; begin @@ -930,7 +1095,7 @@ begin { Byte String with 16-bit size } L := WordLEtoN(AStream.ReadWord()); - AStrValue:=ReadString(AStream,L); // ???? shouldn't this be a unicode string ???? + strValue := ReadString(AStream, L, rtfRuns); { Create cell } if FIsVirtualMode then begin @@ -940,8 +1105,9 @@ begin cell := FWorksheet.AddCell(ARow, ACol); { Save the data } - FWorksheet.WriteUTF8Text(cell, AStrValue); + FWorksheet.WriteUTF8Text(cell, strValue); + { // Read rich-text formatting runs B := WordLEtoN(AStream.ReadWord); SetLength(rtfRuns, B); @@ -949,7 +1115,7 @@ begin rtfRuns[L].FirstIndex := WordLEToN(AStream.ReadWord); // Index of first formatted character rtfRuns[L].FontIndex := WordLEToN(AStream.ReadByte); // Index of font used end; - + } {Add attributes} ApplyCellFormatting(cell, XF); ApplyRichTextFormattingRuns(cell, rtfRuns); @@ -957,7 +1123,7 @@ begin if FIsVirtualMode then Workbook.OnReadCellData(Workbook, ARow, ACol, cell); end; - + *) { Reads the cell address used in an RPN formula element. Evaluates the corresponding bits to distinguish between absolute and relative addresses. Overriding the implementation in xlscommon. } @@ -1059,7 +1225,51 @@ begin if (c1 and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow); if (c2 and MASK_EXCEL_RELATIVE_COL <> 0) then Include(AFlags, rfRelCol2); if (c2 and MASK_EXCEL_RELATIVE_ROW <> 0) then Include(AFlags, rfRelRow2); +end; +procedure TsSpreadBIFF8Reader.ReadRSTRING(AStream: TStream); +var + j: Integer; + L: Word; + ARow, ACol: Cardinal; + XF: Word; + wideStrValue: WideString; + cell: PCell; + rtfRuns: TsRichTextFormattingRuns; +begin + { BIFF Record data: Row, Column, XF Index } + ReadRowColXF(AStream, ARow, ACol, XF); + + { Byte String with 16-bit size } + L := WordLEtoN(AStream.ReadWord()); + + { Read wide string without flags } + wideStrValue := ReadUnformattedWideString(AStream, L); + + { Rich-tech formatting runs } + L := WordLEToN(AStream.ReadWord); + SetLength(rtfRuns, L); + for j := 0 to L-1 do + begin + rtfRuns[j].FirstIndex := WordLEToN(AStream.ReadWord); + rtfRuns[j].FontIndex := WordLEToN(AStream.ReadWord); + end; + + { Save the data } + if FIsVirtualMode then begin + InitCell(ARow, ACol, FVirtualCell); // "virtual" cell + cell := @FVirtualCell; + end else + cell := FWorksheet.AddCell(ARow, ACol); // "real" cell + + FWorksheet.WriteUTF8Text(cell, UTF16ToUTF8(wideStrValue)); + + {Add attributes} + ApplyCellFormatting(cell, XF); + ApplyRichTextFormattingRuns(cell, rtfRuns); + + if FIsVirtualMode then + Workbook.OnReadCellData(Workbook, ARow, ACol, cell); end; procedure TsSpreadBIFF8Reader.ReadSST(const AStream: TStream); @@ -1068,32 +1278,38 @@ var StringLength, CurStrLen: WORD; LString: String; ContinueIndicator: WORD; + rtfRuns: TsRichTextFormattingRuns; + ms: TMemoryStream; begin //Reads the shared string table, only compatible with BIFF8 if not Assigned(FSharedStringTable) then begin //First time SST creation FSharedStringTable:=TStringList.Create; - DWordLEtoN(AStream.ReadDWord); //Apparences not used - Items:=DWordLEtoN(AStream.ReadDWord); - Dec(PendingRecordSize,8); + // Total number of strings in the workbook, not used + DWordLEtoN(AStream.ReadDWord); + + // Number of following strings + Items := DWordLEtoN(AStream.ReadDWord); + Dec(PendingRecordSize, 8); end else begin //A second record must not happend. Garbage so skip. Exit; end; - while Items>0 do begin - StringLength:=0; - StringLength:=WordLEtoN(AStream.ReadWord); - Dec(PendingRecordSize,2); - LString:=''; + + while Items > 0 do begin + StringLength := 0; + StringLength := WordLEtoN(AStream.ReadWord); + Dec(PendingRecordSize ,2); + LString := ''; // This loop takes care of the string being split between the STT and the CONTINUE, or between CONTINUE records - while PendingRecordSize>0 do + while PendingRecordSize > 0 do begin - if StringLength>0 then + if StringLength > 0 then begin //Read a stream of zero length reads all the stream. - LString:=LString+ReadString(AStream, StringLength); + LString := LString + ReadString(AStream, StringLength, rtfRuns); end else begin @@ -1104,7 +1320,7 @@ begin end; // Check if the record finished and we need a CONTINUE record to go on - if (PendingRecordSize<=0) and (Items>1) then + if (PendingRecordSize <= 0) and (Items > 1) then begin //A Continue will happend, read the //tag and continue linking... @@ -1112,19 +1328,32 @@ begin if ContinueIndicator<>INT_EXCEL_ID_CONTINUE then begin Raise Exception.Create('[TsSpreadBIFF8Reader.ReadSST] Expected CONTINUE record not found.'); end; - PendingRecordSize:=WordLEtoN(AStream.ReadWord); + PendingRecordSize := WordLEtoN(AStream.ReadWord); CurStrLen := Length(UTF8ToUTF16(LString)); - if StringLength= FSharedStringTable.Count then begin raise Exception.CreateFmt(rsIndexInSSTOutOfRange, [ - Integer(SSTIndex),FSharedStringTable.Count-1 + Integer(SSTIndex), FSharedStringTable.Count-1 ]); end; @@ -1161,9 +1393,18 @@ begin FWorksheet.WriteUTF8Text(cell, FSharedStringTable[SSTIndex]); - {Add attributes} + { Add attributes } ApplyCellFormatting(cell, XF); + { Add rich text formatting } + ms := TMemoryStream(FSharedStringTable.Objects[SSTIndex]); + if ms <> nil then begin + n := ms.ReadWord; + SetLength(rtfRuns, n); + ms.ReadBuffer(rtfRuns[0], n*SizeOf(TsRichTextFormattingRun)); + ApplyRichTextFormattingRuns(cell, rtfRuns); + end; + if FIsVirtualMode then Workbook.OnReadCellData(Workbook, ARow, ACol, cell); end; @@ -1238,12 +1479,7 @@ begin AStream.ReadBuffer(rec.FontIndex, SizeOf(rec) - 2*SizeOf(word)); // Font index - i := WordLEToN(rec.FontIndex); -// if i > 4 then dec(i); // Watch out for the nasty missing font #4... - fnt := TsFont(FFontList[i]); - fmt.FontIndex := Workbook.FindFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color); - if fmt.FontIndex = -1 then - fmt.FontIndex := Workbook.AddFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color); + fmt.FontIndex := FixFontIndex(WordLEToN(rec.FontIndex)); if fmt.FontIndex > 1 then Include(fmt.UsedFormattingFields, uffFont); @@ -1400,9 +1636,9 @@ var lOptions: Word; lColor: Word; lWeight: Word; - lEsc: Word; Len: Byte; font: TsFont; + rtfRuns: TsRichTextFormattingRuns; begin font := TsFont.Create; @@ -1438,14 +1674,8 @@ begin lWeight := WordLEToN(AStream.ReadWord); if lWeight = 700 then Include(font.Style, fssBold); - { Escape type } { Escapement type } - lEsc := WordLEToN(AStream.ReadWord); - case lEsc of - 0: ; - 1: font.Position := fpSuperscript; - 2: font.Position := fpSubscript; - end; + font.Position := TsFontPosition(WordLEToN(AStream.ReadWord)); { Underline type } if AStream.ReadByte > 0 then Include(font.Style, fssUnderline); @@ -1464,7 +1694,7 @@ begin { Font name: Unicodestring, char count in 1 byte } Len := AStream.ReadByte(); - font.FontName := ReadString(AStream, Len); + font.FontName := ReadString(AStream, Len, rtfRuns); // rtfRuns is not used here. { Add font to internal font list; will be transferred to workbook later because the font index in the internal list (= index in file) is not the same as the @@ -1495,7 +1725,8 @@ begin exit; // 2 var. Number format string (Unicode string, 16-bit string length, ➜2.5.3) - fmtString := UTF8Encode(ReadWideString(AStream, False)); +// fmtString := UTF8Encode(ReadWideString(AStream, False)); + fmtString := UTF16ToUTF8(ReadWideString(AStream, False)); // Add to the list at the specified index. If necessary insert empty strings while NumFormatList.Count <= fmtIndex do NumFormatList.Add(''); @@ -1511,12 +1742,13 @@ procedure TsSpreadBIFF8Reader.ReadHeaderFooter(AStream: TStream; var s: widestring; len: word; + rtfRuns: TsRichTextFormattingRuns; begin if RecordSize = 0 then exit; len := WordLEToN(AStream.ReadWord); - s := ReadWideString(AStream, len); + s := ReadWideString(AStream, len, rtfRuns); if AIsHeader then FWorksheet.PageLayout.Headers[1] := UTF8Encode(s) else @@ -1772,7 +2004,7 @@ var begin { Write workbook globals } WriteBOF(AStream, INT_BOF_WORKBOOK_GLOBALS); - WriteCodePage(AStream, 'ucs2le'); // = utf8 + WriteCodePage(AStream, 'ucs2le'); // = utf-16 WriteWindow1(AStream); WriteFonts(AStream); WriteNumFormats(AStream); @@ -2046,7 +2278,7 @@ begin AStream.WriteWord(WordToLE(INT_FONT_WEIGHT_NORMAL)); { Escapement type } - AStream.WriteWord(WordToLE(0)); + AStream.WriteWord(WordToLE(ord(AFont.Position))); { Underline type } if fssUnderline in AFont.Style then @@ -2081,6 +2313,10 @@ begin WriteFONT(AStream, Workbook.GetFont(i)); end; +{@@ ---------------------------------------------------------------------------- + Writes an Excel 8 FORMAT record + ("Format" is to be understood as "number format" here). +-------------------------------------------------------------------------------} procedure TsSpreadBiff8Writer.WriteFORMAT(AStream: TStream; ANumFormatStr: String; ANumFormatIndex: Integer); type @@ -2124,6 +2360,433 @@ begin SetLength(buf, 0); end; +{@@ ---------------------------------------------------------------------------- + Writes an Excel 8 HEADER or FOOTER record, depending on AIsHeader. + Overridden because of wide string +-------------------------------------------------------------------------------} +procedure TsSpreadBIFF8Writer.WriteHeaderFooter(AStream: TStream; + AIsHeader: Boolean); +var + wideStr: WideString; + len: Integer; + id: Word; +begin + with FWorksheet.PageLayout do + if AIsHeader then + begin + if (Headers[HEADER_FOOTER_INDEX_ALL] = '') then + exit; + wideStr := UTF8Decode(Headers[HEADER_FOOTER_INDEX_ALL]); + id := INT_EXCEL_ID_HEADER; + end else + begin + if (Footers[HEADER_FOOTER_INDEX_ALL] = '') then + exit; + wideStr := UTF8Decode(Footers[HEADER_FOOTER_INDEX_ALL]); + id := INT_EXCEL_ID_FOOTER; + end; + len := Length(wideStr); + + { BIFF record header } + WriteBiffHeader(AStream, id, 3 + len*sizeOf(wideChar)); + + { 16-bit string length } + AStream.WriteWord(WordToLE(len)); + + { Widestring flags, 1=regular unicode LE string } + AStream.WriteByte(1); + + { Characters } + AStream.WriteBuffer(WideStringToLE(wideStr)[1], len * SizeOf(WideChar)); +end; + +{@@ ---------------------------------------------------------------------------- + Writes an Excel 8 HYPERLINK record +-------------------------------------------------------------------------------} +procedure TsSpreadBIFF8Writer.WriteHyperlink(AStream: TStream; + AHyperlink: PsHyperlink; AWorksheet: TsWorksheet); +var + temp: TStream; + guid: TGUID; + widestr: widestring; + ansistr: ansistring; + descr: String; + fn: String; + flags: DWord; + size: Integer; + cell: PCell; + target, bookmark: String; + u: TUri; + isInternal: Boolean; + dirUpCounter: Integer; +begin + cell := AWorksheet.FindCell(AHyperlink^.Row, AHyperlink^.Col); + if (cell = nil) or (AHyperlink^.Target='') then + exit; + + descr := AWorksheet.ReadAsUTF8Text(cell); // Hyperlink description + SplitHyperlink(AHyperlink^.Target, target, bookmark); + u := ParseURI(AHyperlink^.Target); + isInternal := (target = '') and (bookmark <> ''); + fn := ''; // Name of local file + if target <> '' then + begin + if (u.Protocol='') then + fn := target + else + UriToFileName(target, fn); + ForcePathDelims(fn); + end; + + // Since the length of the record is not known in the first place we write + // the data to a temporary stream at first. + temp := TMemoryStream.Create; + try + { Cell range using the same hyperlink - we support only single cells } + temp.WriteWord(WordToLE(cell^.Row)); // first row + temp.WriteWord(WordToLE(cell^.Row)); // last row + temp.WriteWord(WordToLE(cell^.Col)); // first column + temp.WriteWord(WordToLE(cell^.Col)); // last column + + { GUID of standard link } + guid := StringToGuid('{79EAC9D0-BAF9-11CE-8C82-00AA004BA90B}'); + temp.WriteBuffer(guid, SizeOf(guid)); + + { unknown } + temp.WriteDWord(DWordToLe($00000002)); + + { option flags } + flags := 0; + if isInternal then + flags := MASK_HLINK_TEXTMARK or MASK_HLINK_DESCRIPTION + else + flags := MASK_HLINK_LINK; + if SameText(u.Protocol, 'file') then + flags := flags or MASK_HLINK_ABSOLUTE; + if descr <> AHyperlink^.Target then + flags := flags or MASK_HLINK_DESCRIPTION; // has description + if bookmark <> '' then + flags := flags or MASK_HLINK_TEXTMARK; // link contains a bookmark + temp.WriteDWord(DWordToLE(flags)); + + { description } + if flags and MASK_HLINK_DESCRIPTION <> 0 then + begin + widestr := UTF8Decode(descr); + { Character count incl trailing zero } + temp.WriteDWord(DWordToLE(Length(wideStr) + 1)); + { Character array (16-bit characters), plus trailing zeros } + temp.WriteBuffer(wideStr[1], (Length(wideStr)+1)*SizeOf(widechar)); + end; + + if target <> '' then + begin + if (fn <> '') then // URI is a local file + begin + { GUID of file moniker } + guid := StringToGuid('{00000303-0000-0000-C000-000000000046}'); + temp.WriteBuffer(guid, SizeOf(guid)); + { Convert to ansi - should be DOS 8.3, but this is not necessary } + ansistr := UTF8ToAnsi(fn); + { Directory-up level counter } + dirUpCounter := 0; + if not FileNameIsAbsolute(ansistr) then + while (pos ('..' + PathDelim, ansistr) = 1) do + begin + inc(dirUpCounter); + Delete(ansistr, 1, Length('..'+PathDelim)); + end; + temp.WriteWord(WordToLE(dirUpCounter)); + { Character count of file name incl trailing zero } + temp.WriteDWord(DWordToLe(Length(ansistr)+1)); + { Character array of file name (8-bit characters), plus trailing zero } + temp.WriteBuffer(ansistr[1], Length(ansistr)+1); + { Unknown } + temp.WriteDWord(DWordToLE($DEADFFFF)); + temp.WriteDWord(0); + temp.WriteDWord(0); + temp.WriteDWord(0); + temp.WriteDWord(0); + temp.WriteDWord(0); + { Size of following file link fields } + widestr := UTF8ToUTF16(fn); + size := 4 + 2 + Length(wideStr)*SizeOf(widechar); + temp.WriteDWord(DWordToLE(size)); + if size > 0 then + begin + { Character count of extended file name } + temp.WriteDWord(DWordToLE(Length(widestr)*SizeOf(WideChar))); + { Unknown } + temp.WriteWord(WordToLE($0003)); + { Character array, 16-bit characters, NOT ZERO-TERMINATED! } + temp.WriteBuffer(widestr[1], Length(wideStr)*SizeOf(WideChar)); + end; + end + else begin { Hyperlink target is a URL } + widestr := UTF8Decode(target); + { GUID of URL Moniker } + guid := StringToGUID('{79EAC9E0-BAF9-11CE-8C82-00AA004BA90B}'); + temp.WriteBuffer(guid, SizeOf(guid)); + { Character count incl trailing zero } + temp.WriteDWord(DWordToLE(Length(wideStr)+1)*SizeOf(wideChar)); + { Character array plus trailing zero (16-bit characters), plus trailing zeros } + temp.WriteBuffer(wideStr[1], (length(wideStr)+1)*SizeOf(wideChar)); + end; + end; // hkURI + + // Hyperlink contains a text mark (#) + if bookmark <> '' then + begin + // Convert to 16-bit characters + widestr := UTF8Decode(bookmark); + { Character count of text mark, incl trailing zero } + temp.WriteDWord(DWordToLE(Length(wideStr) + 1)); + { Character array (16-bit characters) plus trailing zeros } + temp.WriteBuffer(wideStr[1], (Length(wideStr)+1) * SizeOf(WideChar)); + end; + + { BIFF record header } + WriteBIFFHeader(AStream, INT_EXCEL_ID_HYPERLINK, temp.Size); + + { Record data } + temp.Position := 0; + AStream.CopyFrom(temp, temp.Size); + + finally + temp.Free; + end; +end; + +{@@ ---------------------------------------------------------------------------- + Writes all hyperlinks +-------------------------------------------------------------------------------} +procedure TsSpreadBIFF8Writer.WriteHyperlinks(AStream: TStream; + AWorksheet: TsWorksheet); +var + hyperlink: PsHyperlink; +begin + for hyperlink in AWorksheet.Hyperlinks do begin + { Write HYPERLINK record } + WriteHyperlink(AStream, hyperlink, AWorksheet); + { Write HYPERLINK TOOLTIP record } + if hyperlink^.Tooltip <> '' then + WriteHyperlinkTooltip(AStream, hyperlink^.Row, hyperlink^.Col, hyperlink^.Tooltip); + end; +end; + +{@@ ---------------------------------------------------------------------------- + Writes a HYPERLINK TOOLTIP record +-------------------------------------------------------------------------------} +procedure TsSpreadBIFF8Writer.WriteHyperlinkTooltip(AStream: TStream; + const ARow, ACol: Cardinal; const ATooltip: String); +var + widestr: widestring; +begin + widestr := UTF8Decode(ATooltip); + + { BIFF record header } + WriteBiffHeader(AStream, INT_EXCEL_ID_HLINKTOOLTIP, + 10 + (Length(wideStr)+1) * SizeOf(widechar)); + + { Repeated record ID } + AStream.WriteWord(WordToLe(INT_EXCEL_ID_HLINKTOOLTIP)); + + { Cell range using the same hyperlink tooltip - we support only single cells } + AStream.WriteWord(WordToLE(ARow)); // first row + AStream.WriteWord(WordToLE(ARow)); // last row + AStream.WriteWord(WordToLE(ACol)); // first column + AStream.WriteWord(WordToLE(ACol)); // last column + + { Tooltop characters, no length, but trailing zero } + AStream.WriteBuffer(wideStr[1], (Length(widestr)+1)*SizeOf(wideChar)); +end; + +{@@ ---------------------------------------------------------------------------- + Writes an Excel 8 INDEX record + + nm = (rl - rf - 1) / 32 + 1 (using integer division) +-------------------------------------------------------------------------------} +procedure TsSpreadBIFF8Writer.WriteIndex(AStream: TStream); +begin + { BIFF Record header } + WriteBIFFHeader(AStream, INT_EXCEL_ID_INDEX, 16); + + { Not used } + AStream.WriteDWord(DWordToLE(0)); + + { Index to first used row, rf, 0 based } + AStream.WriteDWord(DWordToLE(0)); + + { Index to first row of unused tail of sheet, rl, last used row + 1, 0 based } + AStream.WriteDWord(DWordToLE(0)); + + { Absolute stream position of the DEFCOLWIDTH record of the current sheet. + If it doesn't exist, the offset points to where it would occur. } + AStream.WriteDWord(DWordToLE($00)); + + { Array of nm absolute stream positions of the DBCELL record of each Row Block } + + { OBS: It seems to be no problem just ignoring this part of the record } +end; + +{@@ ---------------------------------------------------------------------------- + Depending on the presence of Rich-text formatting information in the cell + record, writes an Excel 8 LABEL record (string cell value only), or + RSTRING record (string cell value + rich-text formatting runs) + + If the string length exceeds 32758 bytes, the string will be truncated, + a note will be left in the workbooks log. +-------------------------------------------------------------------------------} +procedure TsSpreadBIFF8Writer.WriteLABEL(AStream: TStream; + const ARow, ACol: Cardinal; const AValue: String; ACell: PCell); +const + //limit for this format: 32767 bytes - header (see reclen below): + //37267-8-1=32758 + MAXBYTES = 32758; +var + L: Word; + WideValue: WideString; + rec: TBIFF8_LabelRecord; + rtfRuns: TBiff8_RichTextFormattingRuns; + buf: array of byte; + j, nRuns: Integer; + fmt: PsCellFormat; + useRTF: Boolean; + fntIndex: Word; +begin + if (ARow >= FLimitations.MaxRowCount) or (ACol >= FLimitations.MaxColCount) then + exit; + + WideValue := UTF8Decode(AValue); //to UTF16 + if WideValue = '' then begin + // Badly formatted UTF8String (maybe ANSI?) + if Length(AValue)<>0 then begin + //Quite sure it was an ANSI string written as UTF8, so raise exception. + raise Exception.CreateFmt(rsUTF8TextExpectedButANSIFoundInCell, [GetCellString(ARow,ACol)]); + end; + Exit; + end; + + if Length(WideValue) > MAXBYTES then begin + // Rather than lose data when reading it, let the application programmer deal + // with the problem or purposefully ignore it. + SetLength(WideValue, MAXBYTES); //may corrupt the string (e.g. in surrogate pairs), but... too bad. + Workbook.AddErrorMsg(rsTruncateTooLongCellText, [ + MAXBYTES, GetCellString(ARow, ACol) + ]); + end; + L := Length(WideValue); + + useRTF := (Length(ACell^.RichTextParams) > 0); + + { BIFF record header } + rec.RecordID := WordToLE(IfThen(useRTF, INT_EXCEL_ID_RSTRING, INT_EXCEL_ID_LABEL)); + rec.RecordSize := WordToLE(SizeOf(rec) - SizeOf(TsBIFFHeader) + L * SizeOf(WideChar)); + + { Prepare rich-text formatting runs } + if useRTF then + begin + fmt := FWorkbook.GetPointerToCellFormat(ACell^.FormatIndex); + nRuns := 0; + for j:=0 to High(ACell^.RichTextParams) do + begin + SetLength(rtfRuns, nRuns + 1); + fntIndex := ACell^.RichTextParams[j].FontIndex; + if fntIndex >= 4 then + inc(fntIndex); // Font #4 does not exist in BIFF + rtfRuns[nRuns].FontIndex := WordLEToN(fntIndex); + rtfRuns[nRuns].FirstIndex := WordLEToN(ACell^.RichTextParams[j].StartIndex); + inc(nRuns); + if (ACell^.RichTextParams[j].EndIndex < L) and + (ACell^.RichTextParams[j].EndIndex <> ACell^.RichTextParams[j+1].StartIndex) // wp: j+1 needs to be checked! + then begin + SetLength(rtfRuns, nRuns + 1); + fntIndex := fmt^.FontIndex; + if fntIndex >= 4 then + inc(fntIndex); + rtfRuns[nRuns].FontIndex := WordLEToN(fntIndex); + rtfRuns[nRuns].FirstIndex := WordLEToN(ACell^.RichTextParams[j].EndIndex); + inc(nRuns); + end; + end; + + // Adjust BIFF record size for appended formatting runs + inc(rec.RecordSize, SizeOf(word) + nRuns * SizeOf(TBiff8_RichTextFormattingRun)); + end; + + { BIFF record data } + rec.Row := WordToLE(ARow); + rec.Col := WordToLE(ACol); + + { Index to XF record, according to formatting } + rec.XFIndex := WordToLE(FindXFIndex(ACell)); + + { Byte String with 16-bit length } + rec.TextLen := WordToLE(L); + + { Byte flags } + rec.TextFlags := 1; // means regular unicode LE encoding + + { Copy the text characters into a buffer immediately after rec } + SetLength(buf, SizeOf(rec) + L*SizeOf(WideChar)); + Move(rec, buf[0], SizeOf(Rec)); + Move(WideStringToLE(WideValue)[1], buf[SizeOf(Rec)], L*SizeOf(WideChar)); + + { Write out buffer } + AStream.WriteBuffer(buf[0], SizeOf(rec) + L*SizeOf(WideChar)); + + { Write rich-text information in case of RSTRING record } + if useRTF then + begin + { Write number of rich-text formatting runs } + AStream.WriteWord(WordToLE(nRuns)); + + { Write array of rich-text formatting runs } + AStream.WriteBuffer(rtfRuns[0], nRuns * SizeOf(TBiff8_RichTextFormattingRun)); + end; + + { Clean up } + SetLength(buf, 0); + SetLength(rtfRuns, 0); +end; + +procedure TsSpreadBIFF8Writer.WriteMergedCells(AStream: TStream; + AWorksheet: TsWorksheet); +const + MAX_PER_RECORD = 1026; +var + n0, n: Integer; + rng: PsCellRange; + newRecord: Boolean; +begin + n0 := AWorksheet.MergedCells.Count; + n := Min(n0, MAX_PER_RECORD); + newRecord := true; + for rng in AWorksheet.MergedCells do + begin + if newRecord then + begin + newRecord := false; + { BIFF record header } + WriteBIFFHeader(AStream, INT_EXCEL_ID_MERGEDCELLS, 2 + n*8); + { Number of cell ranges in this record } + AStream.WriteWord(WordToLE(n)); + end; + { Write range data } + AStream.WriteWord(WordToLE(rng^.Row1)); + AStream.WriteWord(WordToLE(rng^.Row2)); + AStream.WriteWord(WordToLE(rng^.Col1)); + AStream.WriteWord(WordToLE(rng^.Col2)); + + dec(n); + if n = 0 then begin + newRecord := true; + dec(n0, MAX_PER_RECORD); + n := Min(n0, MAX_PER_RECORD); + end; + end; +end; + {@@ ---------------------------------------------------------------------------- Writes the first MSODRAWING record to file. It is needed for a comment attached to a cell, but also for embedded shapes (currently not supported). @@ -2439,390 +3102,13 @@ begin AStream.WriteBuffer(WideStringToLE(wideStr)[1], len * SizeOf(WideChar)); end; -{@@ ---------------------------------------------------------------------------- - Writes an Excel 8 INDEX record - - nm = (rl - rf - 1) / 32 + 1 (using integer division) --------------------------------------------------------------------------------} -procedure TsSpreadBIFF8Writer.WriteIndex(AStream: TStream); -begin - { BIFF Record header } - WriteBIFFHeader(AStream, INT_EXCEL_ID_INDEX, 16); - - { Not used } - AStream.WriteDWord(DWordToLE(0)); - - { Index to first used row, rf, 0 based } - AStream.WriteDWord(DWordToLE(0)); - - { Index to first row of unused tail of sheet, rl, last used row + 1, 0 based } - AStream.WriteDWord(DWordToLE(0)); - - { Absolute stream position of the DEFCOLWIDTH record of the current sheet. - If it doesn't exist, the offset points to where it would occur. } - AStream.WriteDWord(DWordToLE($00)); - - { Array of nm absolute stream positions of the DBCELL record of each Row Block } - - { OBS: It seems to be no problem just ignoring this part of the record } -end; - -{@@ ---------------------------------------------------------------------------- - Writes an Excel 8 HEADER or FOOTER record, depending on AIsHeader. - Overridden because of wide string --------------------------------------------------------------------------------} -procedure TsSpreadBIFF8Writer.WriteHeaderFooter(AStream: TStream; - AIsHeader: Boolean); -var - wideStr: WideString; - len: Integer; - id: Word; -begin - with FWorksheet.PageLayout do - if AIsHeader then - begin - if (Headers[HEADER_FOOTER_INDEX_ALL] = '') then - exit; - wideStr := UTF8Decode(Headers[HEADER_FOOTER_INDEX_ALL]); - id := INT_EXCEL_ID_HEADER; - end else - begin - if (Footers[HEADER_FOOTER_INDEX_ALL] = '') then - exit; - wideStr := UTF8Decode(Footers[HEADER_FOOTER_INDEX_ALL]); - id := INT_EXCEL_ID_FOOTER; - end; - len := Length(wideStr); - - { BIFF record header } - WriteBiffHeader(AStream, id, 3 + len*sizeOf(wideChar)); - - { 16-bit string length } - AStream.WriteWord(WordToLE(len)); - - { Widestring flags, 1=regular unicode LE string } - AStream.WriteByte(1); - - { Characters } - AStream.WriteBuffer(WideStringToLE(wideStr)[1], len * SizeOf(WideChar)); -end; - -{@@ ---------------------------------------------------------------------------- - Writes an Excel 8 HYPERLINK record --------------------------------------------------------------------------------} -procedure TsSpreadBIFF8Writer.WriteHyperlink(AStream: TStream; - AHyperlink: PsHyperlink; AWorksheet: TsWorksheet); -var - temp: TStream; - guid: TGUID; - widestr: widestring; - ansistr: ansistring; - descr: String; - fn: String; - flags: DWord; - size: Integer; - cell: PCell; - target, bookmark: String; - u: TUri; - isInternal: Boolean; - dirUpCounter: Integer; -begin - cell := AWorksheet.FindCell(AHyperlink^.Row, AHyperlink^.Col); - if (cell = nil) or (AHyperlink^.Target='') then - exit; - - descr := AWorksheet.ReadAsUTF8Text(cell); // Hyperlink description - SplitHyperlink(AHyperlink^.Target, target, bookmark); - u := ParseURI(AHyperlink^.Target); - isInternal := (target = '') and (bookmark <> ''); - fn := ''; // Name of local file - if target <> '' then - begin - if (u.Protocol='') then - fn := target - else - UriToFileName(target, fn); - ForcePathDelims(fn); - end; - - // Since the length of the record is not known in the first place we write - // the data to a temporary stream at first. - temp := TMemoryStream.Create; - try - { Cell range using the same hyperlink - we support only single cells } - temp.WriteWord(WordToLE(cell^.Row)); // first row - temp.WriteWord(WordToLE(cell^.Row)); // last row - temp.WriteWord(WordToLE(cell^.Col)); // first column - temp.WriteWord(WordToLE(cell^.Col)); // last column - - { GUID of standard link } - guid := StringToGuid('{79EAC9D0-BAF9-11CE-8C82-00AA004BA90B}'); - temp.WriteBuffer(guid, SizeOf(guid)); - - { unknown } - temp.WriteDWord(DWordToLe($00000002)); - - { option flags } - flags := 0; - if isInternal then - flags := MASK_HLINK_TEXTMARK or MASK_HLINK_DESCRIPTION - else - flags := MASK_HLINK_LINK; - if SameText(u.Protocol, 'file') then - flags := flags or MASK_HLINK_ABSOLUTE; - if descr <> AHyperlink^.Target then - flags := flags or MASK_HLINK_DESCRIPTION; // has description - if bookmark <> '' then - flags := flags or MASK_HLINK_TEXTMARK; // link contains a bookmark - temp.WriteDWord(DWordToLE(flags)); - - { description } - if flags and MASK_HLINK_DESCRIPTION <> 0 then - begin - widestr := UTF8Decode(descr); - { Character count incl trailing zero } - temp.WriteDWord(DWordToLE(Length(wideStr) + 1)); - { Character array (16-bit characters), plus trailing zeros } - temp.WriteBuffer(wideStr[1], (Length(wideStr)+1)*SizeOf(widechar)); - end; - - if target <> '' then - begin - if (fn <> '') then // URI is a local file - begin - { GUID of file moniker } - guid := StringToGuid('{00000303-0000-0000-C000-000000000046}'); - temp.WriteBuffer(guid, SizeOf(guid)); - { Convert to ansi - should be DOS 8.3, but this is not necessary } - ansistr := UTF8ToAnsi(fn); - { Directory-up level counter } - dirUpCounter := 0; - if not FileNameIsAbsolute(ansistr) then - while (pos ('..' + PathDelim, ansistr) = 1) do - begin - inc(dirUpCounter); - Delete(ansistr, 1, Length('..'+PathDelim)); - end; - temp.WriteWord(WordToLE(dirUpCounter)); - { Character count of file name incl trailing zero } - temp.WriteDWord(DWordToLe(Length(ansistr)+1)); - { Character array of file name (8-bit characters), plus trailing zero } - temp.WriteBuffer(ansistr[1], Length(ansistr)+1); - { Unknown } - temp.WriteDWord(DWordToLE($DEADFFFF)); - temp.WriteDWord(0); - temp.WriteDWord(0); - temp.WriteDWord(0); - temp.WriteDWord(0); - temp.WriteDWord(0); - { Size of following file link fields } - widestr := UTF8ToUTF16(fn); - size := 4 + 2 + Length(wideStr)*SizeOf(widechar); - temp.WriteDWord(DWordToLE(size)); - if size > 0 then - begin - { Character count of extended file name } - temp.WriteDWord(DWordToLE(Length(widestr)*SizeOf(WideChar))); - { Unknown } - temp.WriteWord(WordToLE($0003)); - { Character array, 16-bit characters, NOT ZERO-TERMINATED! } - temp.WriteBuffer(widestr[1], Length(wideStr)*SizeOf(WideChar)); - end; - end - else begin { Hyperlink target is a URL } - widestr := UTF8Decode(target); - { GUID of URL Moniker } - guid := StringToGUID('{79EAC9E0-BAF9-11CE-8C82-00AA004BA90B}'); - temp.WriteBuffer(guid, SizeOf(guid)); - { Character count incl trailing zero } - temp.WriteDWord(DWordToLE(Length(wideStr)+1)*SizeOf(wideChar)); - { Character array plus trailing zero (16-bit characters), plus trailing zeros } - temp.WriteBuffer(wideStr[1], (length(wideStr)+1)*SizeOf(wideChar)); - end; - end; // hkURI - - // Hyperlink contains a text mark (#) - if bookmark <> '' then - begin - // Convert to 16-bit characters - widestr := UTF8Decode(bookmark); - { Character count of text mark, incl trailing zero } - temp.WriteDWord(DWordToLE(Length(wideStr) + 1)); - { Character array (16-bit characters) plus trailing zeros } - temp.WriteBuffer(wideStr[1], (Length(wideStr)+1) * SizeOf(WideChar)); - end; - - { BIFF record header } - WriteBIFFHeader(AStream, INT_EXCEL_ID_HYPERLINK, temp.Size); - - { Record data } - temp.Position := 0; - AStream.CopyFrom(temp, temp.Size); - - finally - temp.Free; - end; -end; - -{@@ ---------------------------------------------------------------------------- - Writes all hyperlinks --------------------------------------------------------------------------------} -procedure TsSpreadBIFF8Writer.WriteHyperlinks(AStream: TStream; - AWorksheet: TsWorksheet); -var - hyperlink: PsHyperlink; -begin - for hyperlink in AWorksheet.Hyperlinks do begin - { Write HYPERLINK record } - WriteHyperlink(AStream, hyperlink, AWorksheet); - { Write HYPERLINK TOOLTIP record } - if hyperlink^.Tooltip <> '' then - WriteHyperlinkTooltip(AStream, hyperlink^.Row, hyperlink^.Col, hyperlink^.Tooltip); - end; -end; - -{@@ ---------------------------------------------------------------------------- - Writes a HYPERLINK TOOLTIP record --------------------------------------------------------------------------------} -procedure TsSpreadBIFF8Writer.WriteHyperlinkTooltip(AStream: TStream; - const ARow, ACol: Cardinal; const ATooltip: String); -var - widestr: widestring; -begin - widestr := UTF8Decode(ATooltip); - - { BIFF record header } - WriteBiffHeader(AStream, INT_EXCEL_ID_HLINKTOOLTIP, - 10 + (Length(wideStr)+1) * SizeOf(widechar)); - - { Repeated record ID } - AStream.WriteWord(WordToLe(INT_EXCEL_ID_HLINKTOOLTIP)); - - { Cell range using the same hyperlink tooltip - we support only single cells } - AStream.WriteWord(WordToLE(ARow)); // first row - AStream.WriteWord(WordToLE(ARow)); // last row - AStream.WriteWord(WordToLE(ACol)); // first column - AStream.WriteWord(WordToLE(ACol)); // last column - - { Tooltop characters, no length, but trailing zero } - AStream.WriteBuffer(wideStr[1], (Length(widestr)+1)*SizeOf(wideChar)); -end; - - -{@@ ---------------------------------------------------------------------------- - Writes an Excel 8 LABEL record (string cell value) - - If the string length exceeds 32758 bytes, the string will be truncated, - a note will be left in the workbooks log. --------------------------------------------------------------------------------} -procedure TsSpreadBIFF8Writer.WriteLabel(AStream: TStream; const ARow, - ACol: Cardinal; const AValue: string; ACell: PCell); -const - //limit for this format: 32767 bytes - header (see reclen below): - //37267-8-1=32758 - MAXBYTES = 32758; -var - L: Word; - WideValue: WideString; - rec: TBIFF8_LabelRecord; - buf: array of byte; -begin - if (ARow >= FLimitations.MaxRowCount) or (ACol >= FLimitations.MaxColCount) then - exit; - - WideValue := UTF8Decode(AValue); //to UTF16 - if WideValue = '' then begin - // Badly formatted UTF8String (maybe ANSI?) - if Length(AValue)<>0 then begin - //Quite sure it was an ANSI string written as UTF8, so raise exception. - raise Exception.CreateFmt(rsUTF8TextExpectedButANSIFoundInCell, [GetCellString(ARow,ACol)]); - end; - Exit; - end; - - if Length(WideValue) > MAXBYTES then begin - // Rather than lose data when reading it, let the application programmer deal - // with the problem or purposefully ignore it. - SetLength(WideValue, MAXBYTES); //may corrupt the string (e.g. in surrogate pairs), but... too bad. - Workbook.AddErrorMsg(rsTruncateTooLongCellText, [ - MAXBYTES, GetCellString(ARow, ACol) - ]); - end; - L := Length(WideValue); - - { BIFF record header } - rec.RecordID := WordToLE(INT_EXCEL_ID_LABEL); - rec.RecordSize := 8 + 1 + L * SizeOf(WideChar); - - { BIFF record data } - rec.Row := WordToLE(ARow); - rec.Col := WordToLE(ACol); - - { Index to XF record, according to formatting } - rec.XFIndex := WordToLE(FindXFIndex(ACell)); - - { Byte String with 16-bit length } - rec.TextLen := WordToLE(L); - - { Byte flags, 1 means regular unicode LE encoding } - rec.TextFlags := 1; - - { Copy the text characters into a buffer immediately after rec } - SetLength(buf, SizeOf(rec) + L*SizeOf(WideChar)); - Move(rec, buf[0], SizeOf(Rec)); - Move(WideStringToLE(WideValue)[1], buf[SizeOf(Rec)], L*SizeOf(WideChar)); - - { Write out } - AStream.WriteBuffer(buf[0], SizeOf(rec) + L*SizeOf(WideChar)); - - { Clean up } - SetLength(buf, 0); -end; - -procedure TsSpreadBIFF8Writer.WriteMergedCells(AStream: TStream; - AWorksheet: TsWorksheet); -const - MAX_PER_RECORD = 1026; -var - n0, n: Integer; - rng: PsCellRange; - newRecord: Boolean; -begin - n0 := AWorksheet.MergedCells.Count; - n := Min(n0, MAX_PER_RECORD); - newRecord := true; - for rng in AWorksheet.MergedCells do - begin - if newRecord then - begin - newRecord := false; - { BIFF record header } - WriteBIFFHeader(AStream, INT_EXCEL_ID_MERGEDCELLS, 2 + n*8); - { Number of cell ranges in this record } - AStream.WriteWord(WordToLE(n)); - end; - { Write range data } - AStream.WriteWord(WordToLE(rng^.Row1)); - AStream.WriteWord(WordToLE(rng^.Row2)); - AStream.WriteWord(WordToLE(rng^.Col1)); - AStream.WriteWord(WordToLE(rng^.Col2)); - - dec(n); - if n = 0 then begin - newRecord := true; - dec(n0, MAX_PER_RECORD); - n := Min(n0, MAX_PER_RECORD); - end; - end; -end; - {@@----------------------------------------------------------------------------- Writes an Excel 8 STYLE record Registers the name of a user-defined style or specific options for a built-in cell style. -------------------------------------------------------------------------------} -procedure TsSpreadBIFF8Writer.WriteStyle(AStream: TStream); +procedure TsSpreadBIFF8Writer.WriteSTYLE(AStream: TStream); begin { BIFF record header } WriteBiffHeader(AStream, INT_EXCEL_ID_STYLE, 4); diff --git a/components/fpspreadsheet/xlscommon.pas b/components/fpspreadsheet/xlscommon.pas index ba63a16ac..08bca310b 100644 --- a/components/fpspreadsheet/xlscommon.pas +++ b/components/fpspreadsheet/xlscommon.pas @@ -353,6 +353,8 @@ type FIncompleteNoteLength: Word; FFirstNumFormatIndexInFile: Integer; FPalette: TsPalette; + FWorksheetNames: TStrings; + FCurrentWorksheet: Integer; procedure AddBuiltinNumFormats; override; procedure ApplyCellFormatting(ACell: PCell; XFIndex: Word); virtual; @@ -437,6 +439,10 @@ type procedure ReadVCENTER(AStream: TStream); // Read WINDOW2 record (gridlines, sheet headers) procedure ReadWindow2(AStream: TStream); virtual; + procedure ReadWorkbookGlobals(AStream: TStream); virtual; + procedure ReadWorksheet(AStream: TStream); virtual; + + procedure InternalReadFromStream(AStream: TStream); public constructor Create(AWorkbook: TsWorkbook); override; @@ -946,10 +952,6 @@ end; everything is known. -------------------------------------------------------------------------------} procedure TsSpreadBIFFReader.FixColors; -var - i: Integer; - fnt: TsFont; - fmt: PsCellFormat; procedure FixColor(var AColor: TsColor); begin @@ -957,7 +959,17 @@ var AColor := FPalette[AColor and $00FFFFFF]; end; +var + i: Integer; + fnt: TsFont; + fmt: PsCellFormat; begin + for i:=0 to FFontList.Count-1 do + begin + fnt := TsFont(FFontList[i]); + if fnt <> nil then FixColor(fnt.Color); + end; + for i:=0 to FWorkbook.GetFontCount - 1 do begin fnt := FWorkbook.GetFont(i); @@ -2254,6 +2266,17 @@ begin FWorksheet.Options := FWorksheet.Options - [soHasFrozenPanes]; end; +{ Reads the workbook globals. } +procedure TsSpreadBIFFReader.ReadWorkbookGlobals(AStream: TStream); +begin + // To be overridden by BIFF5 and BIFF8 +end; + +procedure TsSpreadBIFFReader.ReadWorksheet(AStream: TStream); +begin + // To be overridden by BIFF5 and BIFF8 +end; + {@@ ---------------------------------------------------------------------------- Populates the reader's palette by default colors. Will be overwritten if the file contains a palette on its own @@ -2263,6 +2286,64 @@ begin FPalette.AddBuiltinColors; end; +procedure TsSpreadBIFFReader.InternalReadFromStream(AStream: TStream); +var + BIFFEOF: Boolean; +begin +{ OLEStream := TMemoryStream.Create; + try + OLEStorage := TOLEStorage.Create; + try + // Only one stream is necessary for any number of worksheets + OLEDocument.Stream := AStream; //OLEStream; + OLEStorage.ReadOLEStream(AStream, OLEDocument, AStreamName); + finally + OLEStorage.Free; + end; + } + + // Check if the operation succeeded + if AStream.Size = 0 then + raise Exception.Create('[TsSpreadBIFFReader.InternalReadFromStream] Reading of OLE document failed'); + + // Rewind the stream and read from it + AStream.Position := 0; + + {Initializations } + FWorksheetNames := TStringList.Create; + try + FCurrentWorksheet := 0; + BIFFEOF := false; + + { Read workbook globals } + ReadWorkbookGlobals(AStream); + + { Check for the end of the file } + if AStream.Position >= AStream.Size then + BIFFEOF := true; + + { Now read all worksheets } + while not BIFFEOF do + begin + ReadWorksheet(AStream); + + // Check for the end of the file + if AStream.Position >= AStream.Size then + BIFFEOF := true; + + // Final preparations + inc(FCurrentWorksheet); + // It can happen in files written by Office97 that the OLE directory is + // at the end of the file. + if FCurrentWorksheet = FWorksheetNames.Count then + BIFFEOF := true; + end; + finally + { Finalization } + FreeAndNil(FWorksheetNames); + end; +end; + {------------------------------------------------------------------------------} { TsSpreadBIFFWriter } diff --git a/components/fpspreadsheet/xlsxooxml.pas b/components/fpspreadsheet/xlsxooxml.pas index c5d91d931..5569cb961 100755 --- a/components/fpspreadsheet/xlsxooxml.pas +++ b/components/fpspreadsheet/xlsxooxml.pas @@ -130,7 +130,7 @@ type procedure WriteComments(AWorksheet: TsWorksheet); procedure WriteDimension(AStream: TStream; AWorksheet: TsWorksheet); procedure WriteFillList(AStream: TStream); - procedure WriteFont(AStream: TStream; AFont: TsFont; ATag: String); + procedure WriteFont(AStream: TStream; AFont: TsFont; UseInStyleNode: Boolean); procedure WriteFontList(AStream: TStream); procedure WriteHeaderFooter(AStream: TStream; AWorksheet: TsWorksheet); procedure WriteHyperlinks(AStream: TStream; AWorksheet: TsWorksheet); @@ -749,9 +749,9 @@ begin if (s1 <> '') and (s2 <> '0') then begin fnt := TsFont(FFontList.Items[StrToInt(s1)]); - fmt.FontIndex := Workbook.FindFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color); + fmt.FontIndex := Workbook.FindFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color, fnt.Position); if fmt.FontIndex = -1 then - fmt.FontIndex := Workbook.AddFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color); + fmt.FontIndex := Workbook.AddFont(fnt.FontName, fnt.Size, fnt.Style, fnt.Color, fnt.Position); if fmt.FontIndex > 0 then Include(fmt.UsedFormattingFields, uffFont); end; @@ -1065,9 +1065,8 @@ begin end; end; -{ Reads the font described by the specified node. If the node is already - contained in the font list the font's index is returned; otherwise the - new font is added to the list and its index is returned. } +{ Reads the font described by the specified node and stores it in the reader's + FontList. } function TsSpreadOOXMLReader.ReadFont(ANode: TDOMNode): Integer; var node: TDOMNode; @@ -1079,6 +1078,7 @@ var fntPos: TsFontPosition; nodename: String; s: String; + acceptDuplicates: Boolean; begin fnt := Workbook.GetDefaultFont; if fnt <> nil then @@ -1097,6 +1097,7 @@ begin fntPos := fpNormal; end; + acceptDuplicates := true; node := ANode.FirstChild; while node <> nil do begin @@ -1105,6 +1106,7 @@ begin begin s := GetAttrValue(node, 'val'); if s <> '' then fntName := s; + if nodename = 'rFont' then acceptDuplicates := false; end else if nodename = 'sz' then @@ -1115,26 +1117,26 @@ begin else if nodename = 'b' then begin - if GetAttrValue(node, 'val') <> 'false' - then fntStyles := fntStyles + [fssBold]; + if GetAttrValue(node, 'val') <> 'false' then + fntStyles := fntStyles + [fssBold]; end else if nodename = 'i' then begin - if GetAttrValue(node, 'val') <> 'false' - then fntStyles := fntStyles + [fssItalic]; + if GetAttrValue(node, 'val') <> 'false' then + fntStyles := fntStyles + [fssItalic]; end else if nodename = 'u' then begin - if GetAttrValue(node, 'val') <> 'false' - then fntStyles := fntStyles+ [fssUnderline] + if GetAttrValue(node, 'val') <> 'false' then + fntStyles := fntStyles+ [fssUnderline] end else if nodename = 'strike' then begin - if GetAttrValue(node, 'val') <> 'false' - then fntStyles := fntStyles + [fssStrikeout]; + if GetAttrValue(node, 'val') <> 'false' then + fntStyles := fntStyles + [fssStrikeout]; end else if nodename = 'vertAlign' then @@ -1154,20 +1156,26 @@ begin node := node.NextSibling; end; - // Check whether font is already contained in font list - for Result := 0 to FFontList.Count-1 do - begin - fnt := TsFont(FFontList[Result]); - if (fnt.FontName = fntName) and - (fnt.Size = fntSize) and - (fnt.Style = fntStyles) and - (fnt.Color = fntColor) and - (fnt.Position = fntPos) - then - exit; + // If this method is called when reading the sharedstrings.xml duplicate + // fonts should not be added to the reader's fontList. + // As a function result we return the index of the already existing font. + if not acceptDuplicates then + for Result := 0 to FFontList.Count-1 do + begin + fnt := TsFont(FFontList[Result]); + if SameText(fnt.FontName, fntName) and + (fnt.Size = fntSize) and + (fnt.Style = fntStyles) and + (fnt.Color = fntColor) and + (fnt.Position = fntPos) + then + exit; end; - // Font not yet stored --> create a new font and store it in list + // Create a font record and store it in the reader's fontlist. + // In case of fonts in styles.xml (nodename = "name"), do no look for + // duplicates because this will screw up the font index + // used in the xf records fnt := TsFont.Create; fnt.FontName := fntName; fnt.Size := fntSize; @@ -1812,9 +1820,13 @@ begin FixRows(AWorksheet); end; -procedure TsSpreadOOXMLReader.ReadFromFile(AFileName: string); +{ In principle, this method could be simplified by calling ReadFromStream which + is essentially a duplication of ReadFromFile. But ReadFromStream leads to + worse memory usage. --> KEEP READFROMFILE INTACT } +procedure TsSpreadOOXMLReader.ReadFromFile(AFilename: String); var Doc : TXMLDocument; + RelsNode: TDOMNode; FilePath : string; UnZip : TUnZipper; FileList : TStringList; @@ -1822,6 +1834,7 @@ var i: Integer; fn: String; fn_comments: String; + XMLStream: TStream; begin //unzip "content.xml" of "AFileName" to folder "FilePath" FilePath := GetTempDir(false); @@ -1927,6 +1940,7 @@ begin else // this sheet does not have any cell comments continue; + // Extract texts from the comments file found and apply to worksheet. if fn_comments <> '' then begin @@ -1939,6 +1953,7 @@ begin FreeAndNil(Doc); end; end; + // Add hyperlinks to cells ApplyHyperlinks(FWorksheet); end; // for @@ -1948,13 +1963,173 @@ begin end; end; +procedure TsSpreadOOXMLReader.ReadFromStream(AStream: TStream); +var + Doc : TXMLDocument; + RelsNode: TDOMNode; + SheetList: TStringList; + i: Integer; + fn: String; + fn_comments: String; + XMLStream: TStream; +begin + Doc := nil; + SheetList := TStringList.Create; + try + // Retrieve theme colors + XMLStream := TMemoryStream.Create; + try + if UnzipToStream(AStream, OOXML_PATH_XL_THEME, XMLStream) then + begin + ReadXMLStream(Doc, XMLStream); + ReadThemeElements(Doc.DocumentElement.FindNode('a:themeElements')); + FreeAndNil(Doc); + end; + finally + XMLStream.Free; + end; + + // process the workbook.xml file + XMLStream := TMemoryStream.Create; + try + if not UnzipToStream(AStream, OOXML_PATH_XL_WORKBOOK, XMLStream) then + raise Exception.CreateFmt(rsDefectiveInternalStructure, ['xlsx']); + ReadXMLStream(Doc, XMLStream); + ReadFileVersion(Doc.DocumentElement.FindNode('fileVersion')); + ReadDateMode(Doc.DocumentElement.FindNode('workbookPr')); + ReadSheetList(Doc.DocumentElement.FindNode('sheets'), SheetList); + FreeAndNil(Doc); + finally + XMLStream.Free; + end; + + // process the styles.xml file + XMLStream := TMemoryStream.Create; + try + // Should always exist, just to make sure... + if UnzipToStream(AStream, OOXML_PATH_XL_STYLES, XMLStream) then + begin + ReadXMLStream(Doc, XMLStream); + ReadPalette(Doc.DocumentElement.FindNode('colors')); + ReadFonts(Doc.DocumentElement.FindNode('fonts')); + ReadFills(Doc.DocumentElement.FindNode('fills')); + ReadBorders(Doc.DocumentElement.FindNode('borders')); + ReadNumFormats(Doc.DocumentElement.FindNode('numFmts')); + ReadCellXfs(Doc.DocumentElement.FindNode('cellXfs')); + FreeAndNil(Doc); + end; + finally + XMLStream.Free; + end; + + // process the sharedstrings.xml file + // To do: Use buffered stream instead since shared strings may be large + XMLStream := TMemoryStream.Create; + try + if UnzipToStream(AStream, OOXML_PATH_XL_STRINGS, XMLStream) then + begin + ReadXMLStream(Doc, XMLStream); + ReadSharedStrings(Doc.DocumentElement.FindNode('si')); + FreeAndNil(Doc); + end; + finally + XMLStream.Free; + end; + + // read worksheets + for i:=0 to SheetList.Count-1 do begin + // Create worksheet + FWorksheet := FWorkbook.AddWorksheet(SheetList[i], true); + + // unzip sheet file + XMLStream := TMemoryStream.Create; + try + fn := OOXML_PATH_XL_WORKSHEETS + Format('sheet%d.xml', [i+1]); + if not UnzipToStream(AStream, fn, XMLStream) then + Continue; + ReadXMLStream(Doc, XMLStream); + finally + XMLStream.Free; + end; + + // Sheet data, formats, etc. + ReadSheetViews(Doc.DocumentElement.FindNode('sheetViews'), FWorksheet); + ReadSheetFormatPr(Doc.DocumentElement.FindNode('sheetFormatPr'), FWorksheet); + ReadCols(Doc.DocumentElement.FindNode('cols'), FWorksheet); + ReadWorksheet(Doc.DocumentElement.FindNode('sheetData'), FWorksheet); + ReadMergedCells(Doc.DocumentElement.FindNode('mergeCells'), FWorksheet); + ReadHyperlinks(Doc.DocumentElement.FindNode('hyperlinks')); + ReadPrintOptions(Doc.DocumentElement.FindNode('printOptions'), FWorksheet); + ReadPageMargins(Doc.DocumentElement.FindNode('pageMargins'), FWorksheet); + ReadPageSetup(Doc.DocumentElement.FindNode('pageSetup'), FWorksheet); + ReadHeaderFooter(Doc.DocumentElement.FindNode('headerFooter'), FWorksheet); + + FreeAndNil(Doc); + + { Comments: + The comments are stored in separate "comments.xml" files (n = 1, 2, ...) + The relationship which comment belongs to which sheet file must be + retrieved from the "sheet.xml.rels" file (n = 1, 2, ...). + The rels file contains also the second part of the hyperlink data. } + fn := OOXML_PATH_XL_WORKSHEETS_RELS + Format('sheet%d.xml.rels', [i+1]); + XMLStream := TMemoryStream.Create; + try + if UnzipToStream(AStream, fn, XMLStream) then + begin + // Find exact name of comments.xml file + ReadXMLStream(Doc, XMLStream); + RelsNode := Doc.DocumentElement.FindNode('Relationship'); + fn_comments := FindCommentsFileName(RelsNode); + // Get hyperlink data + ReadHyperlinks(RelsNode); + FreeAndNil(Doc); + end else + if (SheetList.Count = 1) then + // If the workbook has only one sheet then the sheet.xml.rels file + // is missing + fn_comments := 'comments1.xml' + else + // This sheet does not have any cell comments at all + continue; + finally + XMLStream.Free; + end; + + // Extract texts from the comments file found and apply to worksheet. + if fn_comments <> '' then + begin + fn := OOXML_PATH_XL + fn_comments; + XMLStream := TMemoryStream.Create; + try + if UnzipToStream(AStream, fn, XMLStream) then + begin + ReadXMLStream(Doc, XMLStream); + ReadComments(Doc.DocumentElement.FindNode('commentList'), FWorksheet); + FreeAndNil(Doc); + end; + finally + XMLStream.Free; + end; + end; + + // Add hyperlinks to cells + ApplyHyperlinks(FWorksheet); + end; // for + + finally + SheetList.Free; + FreeAndNil(Doc); + end; +end; + + (* procedure TsSpreadOOXMLReader.ReadFromStream(AStream: TStream); begin Unused(AStream); raise Exception.Create('[TsSpreadOOXMLReader.ReadFromStream] '+ 'Method not implemented. Use "ReadFromFile" instead.'); end; - + *) {------------------------------------------------------------------------------} { TsSpreadOOXMLWriter } @@ -2331,15 +2506,19 @@ end; { Writes font parameters to the stream. ATag is "font" for the entry in "styles.xml", or "rPr" for the entry for - richtext parameters in the shared string list. } + richtext parameters in the shared string list. + ANameTag is "name" for the entry in "styles.xml", or "rFont" for the entry} procedure TsSpreadOOXMLWriter.WriteFont(AStream: TStream; AFont: TsFont; - ATag: String); + UseInStyleNode: Boolean); +const + TAG: Array[boolean] of string = ('rPr', 'font'); + NAME_TAG: Array[boolean] of String = ('rFont', 'name'); var s: String; begin s := ''; s := s + Format('', [AFont.Size], FPointSeparatorSettings); - s := s + Format('', [AFont.FontName]); + s := s + Format('<%s val="%s" />', [NAME_TAG[UseInStyleNode], AFont.FontName]); if (fssBold in AFont.Style) then s := s + ''; if (fssItalic in AFont.Style) then @@ -2355,7 +2534,7 @@ begin fpSuperscript: s := s + ''; end; AppendToStream(AStream, Format( - '<%s>%s', [ATag, s, ATag])); + '<%s>%s', [TAG[UseInStyleNode], s, TAG[UseInStyleNode]])); end; { Writes the fontlist of the workbook to the stream. } @@ -2368,7 +2547,7 @@ begin '', [Workbook.GetFontCount])); for i:=0 to Workbook.GetFontCount-1 do begin font := Workbook.GetFont(i); - WriteFont(AStream, font, 'font'); + WriteFont(AStream, font, true); end; AppendToStream(AStream, ''); @@ -3601,13 +3780,19 @@ end; procedure TsSpreadOOXMLWriter.WriteLabel(AStream: TStream; const ARow, ACol: Cardinal; const AValue: string; ACell: PCell); const - MAXBYTES = 32767; //limit for this format + MAXBYTES = 32767; // limit for this format var CellPosText: string; lStyleIndex: Cardinal; ResultingValue: string; + fnt: TsFont; + n: Integer; + i: Integer; + L: Integer; + rtParam: TsRichTextParam; + txt: String; begin - // Office 2007-2010 (at least) support no more characters in a cell; + // Office 2007-2010 (at least) supports no more characters in a cell; if Length(AValue) > MAXBYTES then begin ResultingValue := Copy(AValue, 1, MAXBYTES); //may chop off multicodepoint UTF8 characters but well... @@ -3618,16 +3803,79 @@ begin else ResultingValue := AValue; - if not ValidXMLText(ResultingValue) then + txt := ResultingValue; + if not ValidXMLText(txt) then Workbook.AddErrorMsg( rsInvalidCharacterInCell, [ GetCellString(ARow, ACol) ]); - AppendToStream(FSSharedStrings, - '' + - '' + ResultingValue + '' + - ''); + { Write string to SharedString table } + + if Length(ACell^.RichTextParams) = 0 then + // unformatted string + AppendToStream(FSSharedStrings, + '' + + '' + txt + '' + + '') + else + begin + // rich-text formatted string + L := UTF8Length(Resultingvalue); + AppendToStream(FSSharedStrings, + ''); + rtParam := ACell^.RichTextParams[0]; + if rtParam.StartIndex > 0 then + begin + txt := UTF8Copy(ResultingValue, 1, rtParam.StartIndex); + ValidXMLText(txt); + AppendToStream(FSSharedStrings, + '' + + '' + txt + '' + + '' + ); + end; + for i := 0 to High(ACell^.RichTextParams) do + begin + rtParam := ACell^.RichTextParams[i]; + fnt := FWorkbook.GetFont(rtParam.FontIndex); + n := rtParam.EndIndex - rtParam.StartIndex; + txt := UTF8Copy(Resultingvalue, rtParam.StartIndex+1, n); + ValidXMLText(txt); + AppendToStream(FSSharedStrings, + ''); + WriteFont(FSSharedStrings, fnt, false); // ... font data ... + AppendToStream(FSSharedStrings, + '' + txt + '' + + '' + ); + if (rtParam.EndIndex < L) and (i = High(ACell^.RichTextParams)) then + begin + txt := UTF8Copy(ResultingValue, rtParam.EndIndex+1, MaxInt); + ValidXMLText(txt); + AppendToStream(FSSharedStrings, + '' + + '' + txt + '' + + '' + ) + end else + if (i < High(ACell^.RichTextParams)) and (rtParam.EndIndex < ACell^.RichTextParams[i+1].StartIndex) + then begin + n := ACell^.RichTextParams[i+1].StartIndex - rtParam.EndIndex; + txt := UTF8Copy(Resultingvalue, rtParam.EndIndex+1, n); + ValidXMLText(txt); + AppendToStream(FSSharedStrings, + '' + + '' + txt + '' + + '' + ); + end; + end; + AppendToStream(FSSharedStrings, + ''); + end; + + { Write shared string index to cell record } CellPosText := TsWorksheet.CellPosToText(ARow, ACol); lStyleIndex := GetStyleIndex(ACell);