{ fpsxmlcommon.pas Unit shared by all xml-type reader/writer classes } unit fpsxmlcommon; {$mode objfpc}{$H+} interface uses Classes, SysUtils, laz2_xmlread, laz2_DOM, {$IF FPC_FULLVERSION >= 20701} zipper, {$ELSE} fpszipper, {$ENDIF} fpSpreadsheet, fpsreaderwriter; type TsSpreadXMLReader = class(TsCustomSpreadReader) protected procedure ReadXMLFile(out ADoc: TXMLDocument; AFileName: String); procedure ReadXMLStream(out ADoc: TXMLDocument; AStream: TStream); end; 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; function GetAttrValue(ANode : TDOMNode; AAttrName : string) : string; function GetNodeValue(ANode: TDOMNode): String; function LineEndingToBR(const AText: String): String; function UTF8TextToXMLText(AText: string; ProcessLineEndings: Boolean = false): string; function ValidXMLText(var AText: string; ReplaceSpecialChars: Boolean = true; ProcessLineEndings: Boolean = false): Boolean; procedure UnzipFile(AZipFileName, AZippedFile, ADestFolder: String); function UnzipToStream(AZipStream: TStream; const AZippedFile: String; ADestStream: TStream): Boolean; function CreateTempStream(AWorkbook: TsWorkbook; AFileNameBase: String): TStream; procedure DestroyTempStream(AStream: TStream); implementation uses (* {$IF FPC_FULLVERSION >= 20701} zipper, {$ELSE} fpszipper, {$ENDIF} *) fpsStreams, fpsUtils; {------------------------------------------------------------------------------} { 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; begin Result := ''; if (ANode = nil) or (ANode.Attributes = nil) then exit; Found := false; i := 0; while not Found and (i < ANode.Attributes.Length) do begin if ANode.Attributes.Item[i].NodeName = AAttrName then begin Found := true; Result := ANode.Attributes.Item[i].NodeValue; end; inc(i); end; 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 GetNodeValue(ANode: TDOMNode): String; var child: TDOMNode; begin Result := ''; child := ANode.FirstChild; if Assigned(child) and (child.NodeName = '#text') then Result := child.NodeValue; end; {@@ ---------------------------------------------------------------------------- Replaces LineEnding character(s) by '
'; -------------------------------------------------------------------------------} function LineEndingToBR(const AText: String): String; var i: Integer; begin Result := ''; i := 1; while (i <= Length(AText)) do begin case AText[i] of #13: begin Result := Result + '
'; if (i < Length(AText)) and (AText[i+1] = #10) then inc(i); end; #10: Result := Result + '
'; else Result := Result + AText[i]; end; inc(i); end; end; {@@ ---------------------------------------------------------------------------- Converts a string encoded in UTF8 to a string usable in XML. For this purpose, some characters must be translated. @param AText Input string encoded as UTF8 @param ProcessLineEndings If TRUE line ending characters are replaced by their HTML entities (e.g., #10 --> ' ' @return String usable in XML with some characters replaced by the HTML codes. -------------------------------------------------------------------------------} function UTF8TextToXMLText(AText: string; ProcessLineEndings: Boolean = false): string; var Idx: Integer; AppoSt: string; begin Result := ''; idx := 1; while idx <= Length(AText) do begin case AText[Idx] of '&': begin AppoSt := Copy(AText, Idx, 6); if (Pos('&', AppoSt) = 1) or (Pos('<', AppoSt) = 1) or (Pos('>', AppoSt) = 1) or (Pos('"', AppoSt) = 1) or (Pos(''', AppoSt) = 1) or (Pos('%', AppoSt) = 1) // % then begin //'&' is the first char of a special chat, it must not be converted Result := Result + AText[Idx]; end else begin Result := Result + '&'; end; end; '<': Result := Result + '<'; '>': Result := Result + '>'; '"': Result := Result + '"'; '''':Result := Result + '''; '%': Result := Result + '%'; #10: if ProcessLineEndings then Result := Result + ' ' else Result := Result + #10; #13: if ProcessLineEndings then Result := Result + ' ' else Result := Result + #13; { this breaks multi-line labels in xlsx #10: begin Result := Result + '
'; if (idx < Length(AText)) and (AText[idx+1] = #13) then inc(idx); end; #13: begin Result := Result + '
'; if (idx < Length(AText)) and (AText[idx+1] = #10) then inc(idx); end; } else Result := Result + AText[Idx]; end; inc(idx); end; end; {@@ ---------------------------------------------------------------------------- Checks a string for characters that are not permitted in XML strings. The function returns FALSE if a character <#32 is contained (except for #9, #10, #13), TRUE otherwise. Invalid characters are replaced by a box symbol. If ReplaceSpecialChars is TRUE, some other characters are converted to valid HTML codes by calling UTF8TextToXMLText @param AText String to be checked. Is replaced by valid string. @param ReplaceSpecialChars Special characters are replaced by their HTML codes (e.g. '>' --> '>') @param ProcessLineEndings If TRUE line ending characters are replaced by their HTML entities. @return FALSE if characters < #32 were replaced, TRUE otherwise. -------------------------------------------------------------------------------} function ValidXMLText(var AText: string; ReplaceSpecialChars: Boolean = true; ProcessLineEndings: Boolean = false): Boolean; const BOX = #$E2#$8E#$95; var i: Integer; begin Result := true; for i := Length(AText) downto 1 do if (AText[i] < #32) and not (AText[i] in [#9, #10, #13]) then begin // Replace invalid character by box symbol Delete(AText, i, 1); Insert(BOX, AText, i); Result := false; end; if ReplaceSpecialChars then AText := UTF8TextToXMLText(AText, ProcessLineEndings); 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 Unused(AItem); FSuccess := True; AStream := FOutputStream; end; procedure TStreamUnzipper.DoneStream(Sender: TObject; var AStream: TStream; AItem: TFullZipFileEntry); begin Unused(AItem); 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 stream: TStream; begin if (boBufStream in Workbook.Options) then stream := TBufStream.Create(AFilename, fmOpenRead + fmShareDenyWrite) else stream := TFileStream.Create(AFileName, fmOpenRead + fmShareDenyWrite); try 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; unzip: TUnzipper; begin list := TStringList.Create; try list.Add(AZippedFile); unzip := TUnzipper.Create; try Unzip.OutputPath := ADestFolder; Unzip.UnzipFiles(AZipFileName, list); finally unzip.Free; end; finally list.Free; end; 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; {@@ ---------------------------------------------------------------------------- Creates a basic stream for storing of the individual files. Depending on the set workbook options the stream is created as a memory stream (default), buffered stream or file stream. In the latter two cases a filename mask is provided to create a temporary filename around this mask. -------------------------------------------------------------------------------} function CreateTempStream(AWorkbook: TsWorkbook; AFilenameBase: String): TStream; begin if boFileStream in AWorkbook.Options then Result := TFileStream.Create(GetTempFileName('', AFilenameBase), fmCreate) else if boBufStream in AWorkbook.Options then Result := TBufStream.Create(GetTempFileName('', AFilenameBase)) else Result := TMemoryStream.Create; end; procedure DestroyTempStream(AStream: TStream); var fn: String; begin // TMemoryStream and TBufStream need not be considered separately, // they destroy everything themselves. Only the TFileStream must delete its // temporary file. if AStream is TFileStream then begin fn := TFileStream(AStream).Filename; AStream.Free; // Destroy stream before deleting temp file! DeleteFile(fn); // Otherwise the temp file will not be deleted. end else AStream.Free; end; end.