1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
10 #include "htmlreqifreader.hxx"
12 #include <comphelper/scopeguard.hxx>
13 #include <filter/msfilter/rtfutil.hxx>
14 #include <rtl/strbuf.hxx>
15 #include <sot/storage.hxx>
16 #include <svtools/parrtf.hxx>
17 #include <svtools/rtfkeywd.hxx>
18 #include <svtools/rtftoken.h>
19 #include <tools/stream.hxx>
20 #include <filter/msfilter/msdffimp.hxx>
21 #include <vcl/cvtgrf.hxx>
23 #include <sal/log.hxx>
24 #include <vcl/FilterConfigItem.hxx>
25 #include <vcl/wmf.hxx>
26 #include <comphelper/propertyvalue.hxx>
27 #include <fmtfsize.hxx>
30 using namespace com::sun::star
;
34 /// RTF parser that just extracts a single OLE2 object from a file.
35 class ReqIfRtfReader
: public SvRTFParser
38 ReqIfRtfReader(SvStream
& rStream
);
39 void NextToken(int nToken
) override
;
40 bool WriteObjectData(SvStream
& rOLE
);
43 bool m_bInObjData
= false;
47 ReqIfRtfReader::ReqIfRtfReader(SvStream
& rStream
)
48 : SvRTFParser(rStream
)
52 void ReqIfRtfReader::NextToken(int nToken
)
61 m_aHex
.append(OUStringToOString(aToken
, RTL_TEXTENCODING_ASCII_US
));
69 bool ReqIfRtfReader::WriteObjectData(SvStream
& rOLE
)
71 return msfilter::rtfutil::ExtractOLE2FromObjdata(m_aHex
.makeStringAndClear(), rOLE
);
74 /// Looks up what OLE1 calls the ClassName, see [MS-OLEDS] 2.3.8 CompObjStream.
75 OString
ExtractOLEClassName(const tools::SvRef
<SotStorage
>& xStorage
)
79 tools::SvRef
<SotStorageStream
> pCompObj
= xStorage
->OpenSotStream("\1CompObj");
84 pCompObj
->SeekRel(28); // Header
85 if (!pCompObj
->good())
89 pCompObj
->ReadUInt32(nData
); // AnsiUserType
90 pCompObj
->SeekRel(nData
);
91 if (!pCompObj
->good())
94 pCompObj
->ReadUInt32(nData
); // AnsiClipboardFormat
95 pCompObj
->SeekRel(nData
);
96 if (!pCompObj
->good())
99 pCompObj
->ReadUInt32(nData
); // Reserved1
100 return read_uInt8s_ToOString(*pCompObj
, nData
- 1); // -1 because it is null-terminated
103 /// Parses the presentation stream of an OLE2 storage.
104 bool ParseOLE2Presentation(SvStream
& rOle2
, sal_uInt32
& nWidth
, sal_uInt32
& nHeight
,
105 SvStream
& rPresentationData
)
107 // See [MS-OLEDS] 2.3.4, OLEPresentationStream
109 tools::SvRef
<SotStorage
> pStorage
= new SotStorage(rOle2
);
110 tools::SvRef
<SotStorageStream
> xOle2Presentation
111 = pStorage
->OpenSotStream("\002OlePres000", StreamMode::STD_READ
);
113 // Read AnsiClipboardFormat.
114 sal_uInt32 nMarkerOrLength
= 0;
115 xOle2Presentation
->ReadUInt32(nMarkerOrLength
);
116 if (nMarkerOrLength
!= 0xffffffff)
117 // FormatOrAnsiString is not present
119 sal_uInt32 nFormatOrAnsiLength
= 0;
120 xOle2Presentation
->ReadUInt32(nFormatOrAnsiLength
);
121 if (nFormatOrAnsiLength
!= 0x00000003) // CF_METAFILEPICT
124 // Read TargetDeviceSize.
125 sal_uInt32 nTargetDeviceSize
= 0;
126 xOle2Presentation
->ReadUInt32(nTargetDeviceSize
);
127 if (nTargetDeviceSize
!= 0x00000004)
128 // TargetDevice is present
131 sal_uInt32 nAspect
= 0;
132 xOle2Presentation
->ReadUInt32(nAspect
);
133 sal_uInt32 nLindex
= 0;
134 xOle2Presentation
->ReadUInt32(nLindex
);
135 sal_uInt32 nAdvf
= 0;
136 xOle2Presentation
->ReadUInt32(nAdvf
);
137 sal_uInt32 nReserved1
= 0;
138 xOle2Presentation
->ReadUInt32(nReserved1
);
139 xOle2Presentation
->ReadUInt32(nWidth
);
140 xOle2Presentation
->ReadUInt32(nHeight
);
141 sal_uInt32 nSize
= 0;
142 xOle2Presentation
->ReadUInt32(nSize
);
145 if (nSize
> xOle2Presentation
->remainingSize())
151 "ParseOLE2Presentation: ignoring potentially broken small preview: size is "
156 std::vector
<char> aBuffer(nSize
);
157 xOle2Presentation
->ReadBytes(aBuffer
.data(), aBuffer
.size());
158 rPresentationData
.WriteBytes(aBuffer
.data(), aBuffer
.size());
164 * Inserts an OLE1 header before an OLE2 storage, assuming that the storage has an Ole10Native
167 OString
InsertOLE1HeaderFromOle10NativeStream(const tools::SvRef
<SotStorage
>& xStorage
,
168 SwOLENode
& rOLENode
, SvStream
& rOle1
)
170 tools::SvRef
<SotStorageStream
> xOle1Stream
171 = xStorage
->OpenSotStream("\1Ole10Native", StreamMode::STD_READ
);
172 sal_uInt32 nOle1Size
= 0;
173 xOle1Stream
->ReadUInt32(nOle1Size
);
176 if (xStorage
->GetClassName() == SvGlobalName(0x0003000A, 0, 0, 0xc0, 0, 0, 0, 0, 0, 0, 0x46))
178 aClassName
= "PBrush";
182 if (xStorage
->GetClassName()
183 != SvGlobalName(0x0003000C, 0, 0, 0xc0, 0, 0, 0, 0, 0, 0, 0x46))
185 SAL_WARN("sw.html", "InsertOLE1HeaderFromOle10NativeStream: unexpected class id: "
186 << xStorage
->GetClassName().GetHexName());
188 aClassName
= "Package";
191 // Write ObjectHeader, see [MS-OLEDS] 2.2.4.
194 rOle1
.WriteUInt32(0x00000501);
196 // FormatID is EmbeddedObject.
197 rOle1
.WriteUInt32(0x00000002);
200 rOle1
.WriteUInt32(aClassName
.isEmpty() ? 0 : aClassName
.getLength() + 1);
201 if (!aClassName
.isEmpty())
203 rOle1
.WriteOString(aClassName
);
204 // Null terminated pascal string.
209 rOle1
.WriteUInt32(0);
212 rOle1
.WriteUInt32(0);
215 rOle1
.WriteUInt32(nOle1Size
);
217 // Write the actual native data.
218 rOle1
.WriteStream(*xOle1Stream
, nOle1Size
);
220 // Write Presentation.
221 if (!rOLENode
.GetGraphic())
226 const Graphic
& rGraphic
= *rOLENode
.GetGraphic();
227 Size aSize
= rOLENode
.GetTwipSize();
228 SvMemoryStream aGraphicStream
;
229 if (GraphicConverter::Export(aGraphicStream
, rGraphic
, ConvertDataFormat::WMF
) != ERRCODE_NONE
)
234 auto pGraphicAry
= static_cast<const sal_uInt8
*>(aGraphicStream
.GetData());
235 sal_uInt64 nPresentationData
= aGraphicStream
.TellEnd();
236 msfilter::rtfutil::StripMetafileHeader(pGraphicAry
, nPresentationData
);
239 rOle1
.WriteUInt32(0x00000501);
240 // FormatID: constant means the ClassName field is present.
241 rOle1
.WriteUInt32(0x00000005);
242 // ClassName: null terminated pascal string.
243 OString
aPresentationClassName("METAFILEPICT");
244 rOle1
.WriteUInt32(aPresentationClassName
.getLength() + 1);
245 rOle1
.WriteOString(aPresentationClassName
);
248 rOle1
.WriteUInt32(aSize
.getWidth());
250 rOle1
.WriteUInt32(aSize
.getHeight() * -1);
251 // PresentationDataSize
252 rOle1
.WriteUInt32(8 + nPresentationData
);
254 rOle1
.WriteUInt16(0x0008);
255 rOle1
.WriteUInt16(0x31b1);
256 rOle1
.WriteUInt16(0x1dd9);
257 rOle1
.WriteUInt16(0x0000);
258 rOle1
.WriteBytes(pGraphicAry
, nPresentationData
);
264 * Writes an OLE1 header and data from rOle2 to rOle1.
266 * In case rOle2 has presentation data, then its size is written to nWidth/nHeight. Otherwise
267 * nWidth/nHeight/pPresentationData/nPresentationData is used for the presentation data.
269 OString
InsertOLE1Header(SvStream
& rOle2
, SvStream
& rOle1
, sal_uInt32
& nWidth
, sal_uInt32
& nHeight
,
270 SwOLENode
& rOLENode
, const sal_uInt8
* pPresentationData
,
271 sal_uInt64 nPresentationData
)
274 tools::SvRef
<SotStorage
> xStorage(new SotStorage(rOle2
));
275 if (xStorage
->GetError() != ERRCODE_NONE
)
278 if (xStorage
->IsStream("\1Ole10Native"))
280 return InsertOLE1HeaderFromOle10NativeStream(xStorage
, rOLENode
, rOle1
);
283 OString aClassName
= ExtractOLEClassName(xStorage
);
285 // Write ObjectHeader, see [MS-OLEDS] 2.2.4.
288 rOle1
.WriteUInt32(0x00000501);
290 // FormatID is EmbeddedObject.
291 rOle1
.WriteUInt32(0x00000002);
294 rOle1
.WriteUInt32(aClassName
.isEmpty() ? 0 : aClassName
.getLength() + 1);
295 if (!aClassName
.isEmpty())
297 rOle1
.WriteOString(aClassName
);
298 // Null terminated pascal string.
303 rOle1
.WriteUInt32(0);
306 rOle1
.WriteUInt32(0);
309 rOle1
.WriteUInt32(rOle2
.TellEnd());
311 // Write the actual native data.
313 rOle1
.WriteStream(rOle2
);
315 // Write Presentation.
316 SvMemoryStream aPresentationData
;
318 rOle1
.WriteUInt32(0x00000501);
319 // FormatID: constant means the ClassName field is present.
320 rOle1
.WriteUInt32(0x00000005);
321 // ClassName: null terminated pascal string.
322 OString
aPresentationClassName("METAFILEPICT");
323 rOle1
.WriteUInt32(aPresentationClassName
.getLength() + 1);
324 rOle1
.WriteOString(aPresentationClassName
);
326 const sal_uInt8
* pBytes
= nullptr;
327 sal_uInt64 nBytes
= 0;
328 if (ParseOLE2Presentation(rOle2
, nWidth
, nHeight
, aPresentationData
))
330 // Take presentation data for OLE1 from OLE2.
331 pBytes
= static_cast<const sal_uInt8
*>(aPresentationData
.GetData());
332 nBytes
= aPresentationData
.Tell();
336 // Take presentation data for OLE1 from RTF.
337 pBytes
= pPresentationData
;
338 nBytes
= nPresentationData
;
341 rOle1
.WriteUInt32(nWidth
);
343 rOle1
.WriteUInt32(nHeight
* -1);
344 // PresentationDataSize: size of (reserved fields + pBytes).
345 rOle1
.WriteUInt32(8 + nBytes
);
347 rOle1
.WriteUInt16(0x0008);
348 rOle1
.WriteUInt16(0x31b1);
349 rOle1
.WriteUInt16(0x1dd9);
350 rOle1
.WriteUInt16(0x0000);
351 rOle1
.WriteBytes(pBytes
, nBytes
);
356 /// Writes presentation data with the specified size to rRtf as an RTF hexdump.
357 void WrapOleGraphicInRtf(SvStream
& rRtf
, sal_uInt32 nWidth
, sal_uInt32 nHeight
,
358 const sal_uInt8
* pPresentationData
, sal_uInt64 nPresentationData
)
361 rRtf
.WriteOString("{" OOO_STRING_SVTOOLS_RTF_RESULT
);
364 rRtf
.WriteOString("{" OOO_STRING_SVTOOLS_RTF_PICT
);
366 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_WMETAFILE
"8");
367 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_PICW
);
368 rRtf
.WriteOString(OString::number(nWidth
));
369 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_PICH
);
370 rRtf
.WriteOString(OString::number(nHeight
));
371 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_PICWGOAL
);
372 rRtf
.WriteOString(OString::number(nWidth
));
373 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_PICHGOAL
);
374 rRtf
.WriteOString(OString::number(nHeight
));
375 if (pPresentationData
)
377 rRtf
.WriteOString(SAL_NEWLINE_STRING
);
378 msfilter::rtfutil::WriteHex(pPresentationData
, nPresentationData
, &rRtf
);
382 rRtf
.WriteOString("}");
385 rRtf
.WriteOString("}");
389 namespace SwReqIfReader
391 bool ExtractOleFromRtf(SvStream
& rRtf
, SvStream
& rOle
, bool& bOwnFormat
)
393 // Add missing header/footer.
395 aRtf
.WriteOString("{\\rtf1");
396 aRtf
.WriteStream(rRtf
);
397 aRtf
.WriteOString("}");
400 // Read the RTF markup.
401 tools::SvRef
<ReqIfRtfReader
> xReader(new ReqIfRtfReader(aRtf
));
402 SvParserState eState
= xReader
->CallParser();
403 if (eState
== SvParserState::Error
)
406 // Write the OLE2 data.
407 if (!xReader
->WriteObjectData(rOle
))
410 tools::SvRef
<SotStorage
> pStorage
= new SotStorage(rOle
);
411 OUString aFilterName
= SvxMSDffManager::GetFilterNameFromClassID(pStorage
->GetClassName());
412 bOwnFormat
= !aFilterName
.isEmpty();
415 // Real OLE2 data, we're done.
420 // ODF-in-OLE2 case, extract actual data.
421 SvMemoryStream aMemory
;
422 SvxMSDffManager::ExtractOwnStream(*pStorage
, aMemory
);
425 rOle
.WriteStream(aMemory
);
426 // Stream length is current position + 1.
427 rOle
.SetStreamSize(aMemory
.GetSize() + 1);
432 bool WrapOleInRtf(SvStream
& rOle2
, SvStream
& rRtf
, SwOLENode
& rOLENode
,
433 const SwFrameFormat
& rFormat
)
435 sal_uInt64 nPos
= rOle2
.Tell();
436 comphelper::ScopeGuard
g([&rOle2
, nPos
] { rOle2
.Seek(nPos
); });
438 // Write OLE1 header, then the RTF wrapper.
439 SvMemoryStream aOLE1
;
441 // Prepare presentation data early, so it's available to both OLE1 and RTF.
442 Size aSize
= rFormat
.GetFrameSize().GetSize();
443 sal_uInt32 nWidth
= aSize
.getWidth();
444 sal_uInt32 nHeight
= aSize
.getHeight();
445 const Graphic
* pGraphic
= rOLENode
.GetGraphic();
446 const sal_uInt8
* pPresentationData
= nullptr;
447 sal_uInt64 nPresentationData
= 0;
448 SvMemoryStream aGraphicStream
;
451 uno::Sequence
<beans::PropertyValue
> aFilterData
452 = { comphelper::makePropertyValue("EmbedEMF", false) };
453 FilterConfigItem
aConfigItem(&aFilterData
);
454 if (ConvertGraphicToWMF(*pGraphic
, aGraphicStream
, &aConfigItem
))
456 pPresentationData
= static_cast<const sal_uInt8
*>(aGraphicStream
.GetData());
457 nPresentationData
= aGraphicStream
.TellEnd();
458 msfilter::rtfutil::StripMetafileHeader(pPresentationData
, nPresentationData
);
461 OString aClassName
= InsertOLE1Header(rOle2
, aOLE1
, nWidth
, nHeight
, rOLENode
,
462 pPresentationData
, nPresentationData
);
465 rRtf
.WriteOString("{" OOO_STRING_SVTOOLS_RTF_OBJECT
);
466 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_OBJEMB
);
469 rRtf
.WriteOString("{" OOO_STRING_SVTOOLS_RTF_IGNORE OOO_STRING_SVTOOLS_RTF_OBJCLASS
" ");
470 rRtf
.WriteOString(aClassName
);
472 rRtf
.WriteOString("}");
475 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_OBJW
);
476 rRtf
.WriteOString(OString::number(nWidth
));
477 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_OBJH
);
478 rRtf
.WriteOString(OString::number(nHeight
));
482 "{" OOO_STRING_SVTOOLS_RTF_IGNORE OOO_STRING_SVTOOLS_RTF_OBJDATA SAL_NEWLINE_STRING
);
483 msfilter::rtfutil::WriteHex(static_cast<const sal_uInt8
*>(aOLE1
.GetData()), aOLE1
.GetSize(),
486 rRtf
.WriteOString("}");
488 if (pPresentationData
)
490 WrapOleGraphicInRtf(rRtf
, nWidth
, nHeight
, pPresentationData
, nPresentationData
);
494 rRtf
.WriteOString("}");
499 bool WrapGraphicInRtf(const Graphic
& rGraphic
, const SwFrameFormat
& rFormat
, SvStream
& rRtf
)
502 rRtf
.WriteOString("{" OOO_STRING_SVTOOLS_RTF_OBJECT
);
503 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_OBJEMB
);
505 // Object size: as used in the document model (not pixel size)
506 Size aSize
= rFormat
.GetFrameSize().GetSize();
507 sal_uInt32 nWidth
= aSize
.getWidth();
508 sal_uInt32 nHeight
= aSize
.getHeight();
509 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_OBJW
);
510 rRtf
.WriteOString(OString::number(nWidth
));
511 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_OBJH
);
512 rRtf
.WriteOString(OString::number(nHeight
));
513 rRtf
.WriteOString(SAL_NEWLINE_STRING
);
516 rRtf
.WriteOString("{" OOO_STRING_SVTOOLS_RTF_IGNORE OOO_STRING_SVTOOLS_RTF_OBJCLASS
" ");
517 OString
aClassName("PBrush");
518 rRtf
.WriteOString(aClassName
);
520 rRtf
.WriteOString("}");
521 rRtf
.WriteOString(SAL_NEWLINE_STRING
);
524 rRtf
.WriteOString("{" OOO_STRING_SVTOOLS_RTF_IGNORE OOO_STRING_SVTOOLS_RTF_OBJDATA
" ");
526 SvMemoryStream aOle1
;
527 // Write ObjectHeader, see [MS-OLEDS] 2.2.4.
529 aOle1
.WriteUInt32(0x00000501);
531 // FormatID is EmbeddedObject.
532 aOle1
.WriteUInt32(0x00000002);
535 aOle1
.WriteUInt32(aClassName
.getLength() + 1);
536 aOle1
.WriteOString(aClassName
);
537 // Null terminated pascal string.
541 aOle1
.WriteUInt32(0);
544 aOle1
.WriteUInt32(0);
547 SvMemoryStream aNativeData
;
549 // Set white background for the semi-transparent pixels.
550 BitmapEx aBitmapEx
= rGraphic
.GetBitmapEx();
551 Bitmap aBitmap
= aBitmapEx
.GetBitmap(/*aTransparentReplaceColor=*/COL_WHITE
);
553 if (aBitmap
.getPixelFormat() != vcl::PixelFormat::N24_BPP
)
555 // More exotic pixel formats cause trouble for ms paint.
556 aBitmap
.Convert(BmpConversion::N24Bit
);
559 if (GraphicConverter::Export(aNativeData
, BitmapEx(aBitmap
), ConvertDataFormat::BMP
)
562 SAL_WARN("sw.html", "WrapGraphicInRtf: bmp conversion failed");
564 aOle1
.WriteUInt32(aNativeData
.TellEnd());
566 // Write the actual native data.
568 aOle1
.WriteStream(aNativeData
);
570 // Prepare presentation data.
571 const sal_uInt8
* pPresentationData
= nullptr;
572 sal_uInt64 nPresentationData
= 0;
573 SvMemoryStream aGraphicStream
;
574 uno::Sequence
<beans::PropertyValue
> aFilterData
575 = { comphelper::makePropertyValue("EmbedEMF", false) };
576 FilterConfigItem
aConfigItem(&aFilterData
);
577 if (ConvertGraphicToWMF(rGraphic
, aGraphicStream
, &aConfigItem
))
579 pPresentationData
= static_cast<const sal_uInt8
*>(aGraphicStream
.GetData());
580 nPresentationData
= aGraphicStream
.TellEnd();
581 msfilter::rtfutil::StripMetafileHeader(pPresentationData
, nPresentationData
);
584 // Write Presentation.
586 aOle1
.WriteUInt32(0x00000501);
587 // FormatID: constant means the ClassName field is present.
588 aOle1
.WriteUInt32(0x00000005);
589 // ClassName: null terminated pascal string.
590 OString
aPresentationClassName("METAFILEPICT");
591 aOle1
.WriteUInt32(aPresentationClassName
.getLength() + 1);
592 aOle1
.WriteOString(aPresentationClassName
);
594 const sal_uInt8
* pBytes
= nullptr;
595 sal_uInt64 nBytes
= 0;
596 // Take presentation data for OLE1 from RTF.
597 pBytes
= pPresentationData
;
598 nBytes
= nPresentationData
;
600 aOle1
.WriteUInt32(nWidth
);
602 aOle1
.WriteUInt32(nHeight
* -1);
603 // PresentationDataSize: size of (reserved fields + pBytes).
604 aOle1
.WriteUInt32(8 + nBytes
);
606 aOle1
.WriteUInt16(0x0008);
607 aOle1
.WriteUInt16(0x31b1);
608 aOle1
.WriteUInt16(0x1dd9);
609 aOle1
.WriteUInt16(0x0000);
610 aOle1
.WriteBytes(pBytes
, nBytes
);
613 msfilter::rtfutil::WriteHex(static_cast<const sal_uInt8
*>(aOle1
.GetData()), aOle1
.GetSize(),
615 rRtf
.WriteOString("}");
616 rRtf
.WriteOString(SAL_NEWLINE_STRING
);
618 rRtf
.WriteOString("{" OOO_STRING_SVTOOLS_RTF_RESULT
);
619 rRtf
.WriteOString("{" OOO_STRING_SVTOOLS_RTF_PICT
);
621 Size
aMapped(rGraphic
.GetPrefSize());
622 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_PICW
);
623 rRtf
.WriteOString(OString::number(aMapped
.Width()));
624 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_PICH
);
625 rRtf
.WriteOString(OString::number(aMapped
.Height()));
627 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_PICWGOAL
);
628 rRtf
.WriteOString(OString::number(nWidth
));
629 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_PICHGOAL
);
630 rRtf
.WriteOString(OString::number(nHeight
));
631 rRtf
.WriteOString(OOO_STRING_SVTOOLS_RTF_WMETAFILE
"8");
632 rRtf
.WriteOString(SAL_NEWLINE_STRING
);
634 if (pPresentationData
)
636 msfilter::rtfutil::WriteHex(pPresentationData
, nPresentationData
, &rRtf
);
637 rRtf
.WriteOString(SAL_NEWLINE_STRING
);
641 rRtf
.WriteOString("}");
644 rRtf
.WriteOString("}");
647 rRtf
.WriteOString("}");
652 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */