1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include <qglobal.h> |
5 | |
6 | #ifndef QT_NO_TEXTODFWRITER |
7 | |
8 | #include "qtextodfwriter_p.h" |
9 | |
10 | #include <QImageReader> |
11 | #include <QImageWriter> |
12 | #include <QTextListFormat> |
13 | #include <QTextList> |
14 | #include <QBuffer> |
15 | #include <QUrl> |
16 | |
17 | #include "qtextdocument_p.h" |
18 | #include "qtexttable.h" |
19 | #include "qtextcursor.h" |
20 | #include "qtextimagehandler_p.h" |
21 | |
22 | #include <QDebug> |
23 | #include <QtCore/private/qzipwriter_p.h> |
24 | |
25 | |
26 | QT_BEGIN_NAMESPACE |
27 | |
28 | using namespace Qt::StringLiterals; |
29 | |
30 | /// Convert pixels to postscript point units |
31 | static QString pixelToPoint(qreal pixels) |
32 | { |
33 | // we hardcode 96 DPI, we do the same in the ODF importer to have a perfect roundtrip. |
34 | return QString::number(pixels * 72 / 96) + "pt"_L1 ; |
35 | } |
36 | |
37 | // strategies |
38 | class QOutputStrategy { |
39 | public: |
40 | QOutputStrategy() : contentStream(nullptr), counter(1) { } |
41 | virtual ~QOutputStrategy() {} |
42 | virtual void addFile(const QString &fileName, const QString &mimeType, const QByteArray &bytes) = 0; |
43 | |
44 | QString createUniqueImageName() |
45 | { |
46 | return QString::fromLatin1(ba: "Pictures/Picture%1" ).arg(a: counter++); |
47 | } |
48 | |
49 | QIODevice *contentStream; |
50 | int counter; |
51 | }; |
52 | |
53 | class QXmlStreamStrategy : public QOutputStrategy { |
54 | public: |
55 | QXmlStreamStrategy(QIODevice *device) |
56 | { |
57 | contentStream = device; |
58 | } |
59 | |
60 | ~QXmlStreamStrategy() |
61 | { |
62 | if (contentStream) |
63 | contentStream->close(); |
64 | } |
65 | virtual void addFile(const QString &, const QString &, const QByteArray &) override |
66 | { |
67 | // we ignore this... |
68 | } |
69 | }; |
70 | |
71 | class QZipStreamStrategy : public QOutputStrategy { |
72 | public: |
73 | QZipStreamStrategy(QIODevice *device) |
74 | : zip(device), |
75 | manifestWriter(&manifest) |
76 | { |
77 | QByteArray mime("application/vnd.oasis.opendocument.text" ); |
78 | zip.setCompressionPolicy(QZipWriter::NeverCompress); |
79 | zip.addFile(fileName: QString::fromLatin1(ba: "mimetype" ), data: mime); // for mime-magick |
80 | zip.setCompressionPolicy(QZipWriter::AutoCompress); |
81 | contentStream = &content; |
82 | content.open(openMode: QIODevice::WriteOnly); |
83 | manifest.open(openMode: QIODevice::WriteOnly); |
84 | |
85 | manifestNS = QString::fromLatin1(ba: "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" ); |
86 | // prettyfy |
87 | manifestWriter.setAutoFormatting(true); |
88 | manifestWriter.setAutoFormattingIndent(1); |
89 | |
90 | manifestWriter.writeNamespace(namespaceUri: manifestNS, prefix: QString::fromLatin1(ba: "manifest" )); |
91 | manifestWriter.writeStartDocument(); |
92 | manifestWriter.writeStartElement(namespaceUri: manifestNS, name: QString::fromLatin1(ba: "manifest" )); |
93 | manifestWriter.writeAttribute(namespaceUri: manifestNS, name: QString::fromLatin1(ba: "version" ), value: QString::fromLatin1(ba: "1.2" )); |
94 | addFile(fileName: QString::fromLatin1(ba: "/" ), mimeType: QString::fromLatin1(ba: "application/vnd.oasis.opendocument.text" )); |
95 | addFile(fileName: QString::fromLatin1(ba: "content.xml" ), mimeType: QString::fromLatin1(ba: "text/xml" )); |
96 | } |
97 | |
98 | ~QZipStreamStrategy() |
99 | { |
100 | manifestWriter.writeEndDocument(); |
101 | manifest.close(); |
102 | zip.addFile(fileName: QString::fromLatin1(ba: "META-INF/manifest.xml" ), device: &manifest); |
103 | content.close(); |
104 | zip.addFile(fileName: QString::fromLatin1(ba: "content.xml" ), device: &content); |
105 | zip.close(); |
106 | } |
107 | |
108 | virtual void addFile(const QString &fileName, const QString &mimeType, const QByteArray &bytes) override |
109 | { |
110 | zip.addFile(fileName, data: bytes); |
111 | addFile(fileName, mimeType); |
112 | } |
113 | |
114 | private: |
115 | void addFile(const QString &fileName, const QString &mimeType) |
116 | { |
117 | manifestWriter.writeEmptyElement(namespaceUri: manifestNS, name: QString::fromLatin1(ba: "file-entry" )); |
118 | manifestWriter.writeAttribute(namespaceUri: manifestNS, name: QString::fromLatin1(ba: "media-type" ), value: mimeType); |
119 | manifestWriter.writeAttribute(namespaceUri: manifestNS, name: QString::fromLatin1(ba: "full-path" ), value: fileName); |
120 | } |
121 | |
122 | QBuffer content; |
123 | QBuffer manifest; |
124 | QZipWriter zip; |
125 | QXmlStreamWriter manifestWriter; |
126 | QString manifestNS; |
127 | }; |
128 | |
129 | static QStringView bullet_char(QTextListFormat::Style style) |
130 | { |
131 | static_assert(int(QTextListFormat::ListDisc) == -1); |
132 | static_assert(int(QTextListFormat::ListUpperRoman) == -8); |
133 | static const char16_t chars[] = { |
134 | u'\x25cf', // bullet character |
135 | u'\x25cb', // white circle |
136 | u'\x25a1', // white square |
137 | u'1', |
138 | u'a', |
139 | u'A', |
140 | u'i', |
141 | u'I', |
142 | }; |
143 | const auto map = [](QTextListFormat::Style s) { return -int(s) - 1; }; |
144 | static_assert(uint(map(QTextListFormat::ListUpperRoman)) == std::size(chars) - 1); |
145 | const auto idx = map(style); |
146 | if (idx < 0) |
147 | return nullptr; |
148 | else |
149 | return {chars + idx, 1}; |
150 | } |
151 | |
152 | static QString bulletChar(QTextListFormat::Style style) |
153 | { |
154 | return bullet_char(style).toString(); |
155 | } |
156 | |
157 | static QString borderStyleName(QTextFrameFormat::BorderStyle style) |
158 | { |
159 | switch (style) { |
160 | case QTextFrameFormat::BorderStyle_None: |
161 | return QString::fromLatin1(ba: "none" ); |
162 | case QTextFrameFormat::BorderStyle_Dotted: |
163 | return QString::fromLatin1(ba: "dotted" ); |
164 | case QTextFrameFormat::BorderStyle_Dashed: |
165 | return QString::fromLatin1(ba: "dashed" ); |
166 | case QTextFrameFormat::BorderStyle_Solid: |
167 | return QString::fromLatin1(ba: "solid" ); |
168 | case QTextFrameFormat::BorderStyle_Double: |
169 | return QString::fromLatin1(ba: "double" ); |
170 | case QTextFrameFormat::BorderStyle_DotDash: |
171 | return QString::fromLatin1(ba: "dashed" ); |
172 | case QTextFrameFormat::BorderStyle_DotDotDash: |
173 | return QString::fromLatin1(ba: "dotted" ); |
174 | case QTextFrameFormat::BorderStyle_Groove: |
175 | return QString::fromLatin1(ba: "groove" ); |
176 | case QTextFrameFormat::BorderStyle_Ridge: |
177 | return QString::fromLatin1(ba: "ridge" ); |
178 | case QTextFrameFormat::BorderStyle_Inset: |
179 | return QString::fromLatin1(ba: "inset" ); |
180 | case QTextFrameFormat::BorderStyle_Outset: |
181 | return QString::fromLatin1(ba: "outset" ); |
182 | } |
183 | return QString::fromLatin1(ba: "" ); |
184 | } |
185 | |
186 | void QTextOdfWriter::writeFrame(QXmlStreamWriter &writer, const QTextFrame *frame) |
187 | { |
188 | Q_ASSERT(frame); |
189 | const QTextTable *table = qobject_cast<const QTextTable*> (object: frame); |
190 | |
191 | if (table) { // Start a table. |
192 | writer.writeStartElement(namespaceUri: tableNS, name: QString::fromLatin1(ba: "table" )); |
193 | writer.writeAttribute(namespaceUri: tableNS, name: QString::fromLatin1(ba: "style-name" ), |
194 | value: QString::fromLatin1(ba: "Table%1" ).arg(a: table->formatIndex())); |
195 | // check if column widths are set, if so add TableNS line above for all columns and link to style |
196 | if (m_tableFormatsWithColWidthConstraints.contains(value: table->formatIndex())) { |
197 | for (int colit = 0; colit < table->columns(); ++colit) { |
198 | writer.writeStartElement(namespaceUri: tableNS, name: QString::fromLatin1(ba: "table-column" )); |
199 | writer.writeAttribute(namespaceUri: tableNS, name: QString::fromLatin1(ba: "style-name" ), |
200 | value: QString::fromLatin1(ba: "Table%1.%2" ).arg(a: table->formatIndex()).arg(a: colit)); |
201 | writer.writeEndElement(); |
202 | } |
203 | } else { |
204 | writer.writeEmptyElement(namespaceUri: tableNS, name: QString::fromLatin1(ba: "table-column" )); |
205 | writer.writeAttribute(namespaceUri: tableNS, name: QString::fromLatin1(ba: "number-columns-repeated" ), |
206 | value: QString::number(table->columns())); |
207 | } |
208 | } else if (frame->document() && frame->document()->rootFrame() != frame) { // start a section |
209 | writer.writeStartElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "section" )); |
210 | } |
211 | |
212 | QTextFrame::iterator iterator = frame->begin(); |
213 | QTextFrame *child = nullptr; |
214 | |
215 | int tableRow = -1; |
216 | while (! iterator.atEnd()) { |
217 | if (iterator.currentFrame() && child != iterator.currentFrame()) |
218 | writeFrame(writer, frame: iterator.currentFrame()); |
219 | else { // no frame, its a block |
220 | QTextBlock block = iterator.currentBlock(); |
221 | if (table) { |
222 | QTextTableCell cell = table->cellAt(position: block.position()); |
223 | if (tableRow < cell.row()) { |
224 | if (tableRow >= 0) |
225 | writer.writeEndElement(); // close table row |
226 | tableRow = cell.row(); |
227 | writer.writeStartElement(namespaceUri: tableNS, name: QString::fromLatin1(ba: "table-row" )); |
228 | } |
229 | writer.writeStartElement(namespaceUri: tableNS, name: QString::fromLatin1(ba: "table-cell" )); |
230 | if (cell.columnSpan() > 1) |
231 | writer.writeAttribute(namespaceUri: tableNS, name: QString::fromLatin1(ba: "number-columns-spanned" ), value: QString::number(cell.columnSpan())); |
232 | if (cell.rowSpan() > 1) |
233 | writer.writeAttribute(namespaceUri: tableNS, name: QString::fromLatin1(ba: "number-rows-spanned" ), value: QString::number(cell.rowSpan())); |
234 | if (cell.format().isTableCellFormat()) { |
235 | if (m_cellFormatsInTablesWithBorders.contains(key: cell.tableCellFormatIndex()) ) { |
236 | // writing table:style-name tag in <table:table-cell> element |
237 | writer.writeAttribute(namespaceUri: tableNS, name: QString::fromLatin1(ba: "style-name" ), |
238 | value: QString::fromLatin1(ba: "TB%1.%2" ).arg(a: table->formatIndex()) |
239 | .arg(a: cell.tableCellFormatIndex())); |
240 | } else { |
241 | writer.writeAttribute(namespaceUri: tableNS, name: QString::fromLatin1(ba: "style-name" ), |
242 | value: QString::fromLatin1(ba: "T%1" ).arg(a: cell.tableCellFormatIndex())); |
243 | } |
244 | } |
245 | } |
246 | writeBlock(writer, block); |
247 | if (table) |
248 | writer.writeEndElement(); // table-cell |
249 | } |
250 | child = iterator.currentFrame(); |
251 | ++iterator; |
252 | } |
253 | if (tableRow >= 0) |
254 | writer.writeEndElement(); // close table-row |
255 | |
256 | if (table || (frame->document() && frame->document()->rootFrame() != frame)) |
257 | writer.writeEndElement(); // close table or section element |
258 | } |
259 | |
260 | void QTextOdfWriter::writeBlock(QXmlStreamWriter &writer, const QTextBlock &block) |
261 | { |
262 | if (block.textList()) { // its a list-item |
263 | const int listLevel = block.textList()->format().indent(); |
264 | if (m_listStack.isEmpty() || m_listStack.top() != block.textList()) { |
265 | // not the same list we were in. |
266 | while (m_listStack.size() >= listLevel && !m_listStack.isEmpty() && m_listStack.top() != block.textList() ) { // we need to close tags |
267 | m_listStack.pop(); |
268 | writer.writeEndElement(); // list |
269 | if (m_listStack.size()) |
270 | writer.writeEndElement(); // list-item |
271 | } |
272 | while (m_listStack.size() < listLevel) { |
273 | if (m_listStack.size()) |
274 | writer.writeStartElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "list-item" )); |
275 | writer.writeStartElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "list" )); |
276 | if (m_listStack.size() == listLevel - 1) { |
277 | m_listStack.push(t: block.textList()); |
278 | writer.writeAttribute(namespaceUri: textNS, name: QString::fromLatin1(ba: "style-name" ), value: QString::fromLatin1(ba: "L%1" ) |
279 | .arg(a: block.textList()->formatIndex())); |
280 | } |
281 | else { |
282 | m_listStack.push(t: nullptr); |
283 | } |
284 | } |
285 | } |
286 | writer.writeStartElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "list-item" )); |
287 | } |
288 | else { |
289 | while (! m_listStack.isEmpty()) { |
290 | m_listStack.pop(); |
291 | writer.writeEndElement(); // list |
292 | if (m_listStack.size()) |
293 | writer.writeEndElement(); // list-item |
294 | } |
295 | } |
296 | |
297 | if (block.length() == 1) { // only a linefeed |
298 | writer.writeEmptyElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "p" )); |
299 | writer.writeAttribute(namespaceUri: textNS, name: QString::fromLatin1(ba: "style-name" ), value: QString::fromLatin1(ba: "p%1" ) |
300 | .arg(a: block.blockFormatIndex())); |
301 | if (block.textList()) |
302 | writer.writeEndElement(); // numbered-paragraph |
303 | return; |
304 | } |
305 | writer.writeStartElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "p" )); |
306 | writer.writeAttribute(namespaceUri: textNS, name: QString::fromLatin1(ba: "style-name" ), value: QString::fromLatin1(ba: "p%1" ) |
307 | .arg(a: block.blockFormatIndex())); |
308 | for (QTextBlock::Iterator frag = block.begin(); !frag.atEnd(); ++frag) { |
309 | bool isHyperlink = frag.fragment().charFormat().hasProperty(propertyId: QTextFormat::AnchorHref); |
310 | if (isHyperlink) { |
311 | QString value = frag.fragment().charFormat().property(propertyId: QTextFormat::AnchorHref).toString(); |
312 | writer.writeStartElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "a" )); |
313 | writer.writeAttribute(namespaceUri: xlinkNS, name: QString::fromLatin1(ba: "href" ), value); |
314 | } |
315 | writer.writeCharacters(text: QString()); // Trick to make sure that the span gets no linefeed in front of it. |
316 | writer.writeStartElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "span" )); |
317 | |
318 | QString fragmentText = frag.fragment().text(); |
319 | if (fragmentText.size() == 1 && fragmentText[0] == u'\xFFFC') { // its an inline character. |
320 | writeInlineCharacter(writer, fragment: frag.fragment()); |
321 | writer.writeEndElement(); // span |
322 | continue; |
323 | } |
324 | |
325 | writer.writeAttribute(namespaceUri: textNS, name: QString::fromLatin1(ba: "style-name" ), value: QString::fromLatin1(ba: "c%1" ) |
326 | .arg(a: frag.fragment().charFormatIndex())); |
327 | bool escapeNextSpace = true; |
328 | int precedingSpaces = 0; |
329 | int exportedIndex = 0; |
330 | for (int i=0; i <= fragmentText.size(); ++i) { |
331 | QChar character = (i == fragmentText.size() ? QChar() : fragmentText.at(i)); |
332 | bool isSpace = character.unicode() == ' '; |
333 | |
334 | // find more than one space. -> <text:s text:c="2" /> |
335 | if (!isSpace && escapeNextSpace && precedingSpaces > 1) { |
336 | const bool startParag = exportedIndex == 0 && i == precedingSpaces; |
337 | if (!startParag) |
338 | writer.writeCharacters(text: fragmentText.mid(position: exportedIndex, n: i - precedingSpaces + 1 - exportedIndex)); |
339 | writer.writeEmptyElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "s" )); |
340 | const int count = precedingSpaces - (startParag?0:1); |
341 | if (count > 1) |
342 | writer.writeAttribute(namespaceUri: textNS, name: QString::fromLatin1(ba: "c" ), value: QString::number(count)); |
343 | precedingSpaces = 0; |
344 | exportedIndex = i; |
345 | } |
346 | |
347 | if (i < fragmentText.size()) { |
348 | if (character.unicode() == 0x2028) { // soft-return |
349 | //if (exportedIndex < i) |
350 | writer.writeCharacters(text: fragmentText.mid(position: exportedIndex, n: i - exportedIndex)); |
351 | // adding tab before line-break, so last line in justified paragraph |
352 | // will not stretch to the end |
353 | writer.writeEmptyElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "tab" )); |
354 | writer.writeEmptyElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "line-break" )); |
355 | exportedIndex = i+1; |
356 | continue; |
357 | } else if (character.unicode() == '\t') { // Tab |
358 | //if (exportedIndex < i) |
359 | writer.writeCharacters(text: fragmentText.mid(position: exportedIndex, n: i - exportedIndex)); |
360 | writer.writeEmptyElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "tab" )); |
361 | exportedIndex = i+1; |
362 | precedingSpaces = 0; |
363 | } else if (isSpace) { |
364 | ++precedingSpaces; |
365 | escapeNextSpace = true; |
366 | } else if (!isSpace) { |
367 | precedingSpaces = 0; |
368 | } |
369 | } |
370 | } |
371 | |
372 | writer.writeCharacters(text: fragmentText.mid(position: exportedIndex)); |
373 | writer.writeEndElement(); // span |
374 | writer.writeCharacters(text: QString()); // Trick to make sure that the span gets no linefeed behind it. |
375 | if (isHyperlink) |
376 | writer.writeEndElement(); // a |
377 | } |
378 | writer.writeCharacters(text: QString()); // Trick to make sure that the span gets no linefeed behind it. |
379 | writer.writeEndElement(); // p |
380 | if (block.textList()) |
381 | writer.writeEndElement(); // list-item |
382 | } |
383 | |
384 | static bool probeImageData(QIODevice *device, QImage *image, QString *mimeType, qreal *width, qreal *height) |
385 | { |
386 | QImageReader reader(device); |
387 | const QByteArray format = reader.format().toLower(); |
388 | if (format == "png" ) { |
389 | *mimeType = QStringLiteral("image/png" ); |
390 | } else if (format == "jpg" ) { |
391 | *mimeType = QStringLiteral("image/jpg" ); |
392 | } else if (format == "svg" ) { |
393 | *mimeType = QStringLiteral("image/svg+xml" ); |
394 | } else { |
395 | *image = reader.read(); |
396 | return false; |
397 | } |
398 | |
399 | const QSize size = reader.size(); |
400 | |
401 | *width = size.width(); |
402 | *height = size.height(); |
403 | |
404 | return true; |
405 | } |
406 | |
407 | void QTextOdfWriter::writeInlineCharacter(QXmlStreamWriter &writer, const QTextFragment &fragment) const |
408 | { |
409 | writer.writeStartElement(namespaceUri: drawNS, name: QString::fromLatin1(ba: "frame" )); |
410 | if (m_strategy == nullptr) { |
411 | // don't do anything. |
412 | } |
413 | else if (fragment.charFormat().isImageFormat()) { |
414 | QTextImageFormat imageFormat = fragment.charFormat().toImageFormat(); |
415 | writer.writeAttribute(namespaceUri: drawNS, name: QString::fromLatin1(ba: "name" ), value: imageFormat.name()); |
416 | |
417 | QByteArray data; |
418 | QString mimeType; |
419 | qreal width = 0; |
420 | qreal height = 0; |
421 | |
422 | QImage image; |
423 | QString name = imageFormat.name(); |
424 | if (name.startsWith(s: ":/"_L1 )) // auto-detect resources |
425 | name.prepend(s: "qrc"_L1 ); |
426 | QUrl url = QUrl(name); |
427 | const QVariant variant = m_document->resource(type: QTextDocument::ImageResource, name: url); |
428 | if (variant.userType() == QMetaType::QPixmap || variant.userType() == QMetaType::QImage) { |
429 | image = qvariant_cast<QImage>(v: variant); |
430 | } else if (variant.userType() == QMetaType::QByteArray) { |
431 | data = variant.toByteArray(); |
432 | |
433 | QBuffer buffer(&data); |
434 | buffer.open(openMode: QIODevice::ReadOnly); |
435 | probeImageData(device: &buffer, image: &image, mimeType: &mimeType, width: &width, height: &height); |
436 | } else { |
437 | // try direct loading |
438 | QFile file(imageFormat.name()); |
439 | if (file.open(flags: QIODevice::ReadOnly) && !probeImageData(device: &file, image: &image, mimeType: &mimeType, width: &width, height: &height)) { |
440 | file.seek(offset: 0); |
441 | data = file.readAll(); |
442 | } |
443 | } |
444 | |
445 | if (! image.isNull()) { |
446 | QBuffer imageBytes; |
447 | |
448 | int imgQuality = imageFormat.quality(); |
449 | if (imgQuality >= 100 || imgQuality <= 0 || image.hasAlphaChannel()) { |
450 | QImageWriter imageWriter(&imageBytes, "png" ); |
451 | imageWriter.write(image); |
452 | |
453 | data = imageBytes.data(); |
454 | mimeType = QStringLiteral("image/png" ); |
455 | } else { |
456 | // Write images without alpha channel as jpg with quality set by QTextImageFormat |
457 | QImageWriter imageWriter(&imageBytes, "jpg" ); |
458 | imageWriter.setQuality(imgQuality); |
459 | imageWriter.write(image); |
460 | |
461 | data = imageBytes.data(); |
462 | mimeType = QStringLiteral("image/jpg" ); |
463 | } |
464 | |
465 | width = image.width(); |
466 | height = image.height(); |
467 | } |
468 | |
469 | if (!data.isEmpty()) { |
470 | if (imageFormat.hasProperty(propertyId: QTextFormat::ImageWidth)) { |
471 | width = imageFormat.width(); |
472 | } |
473 | if (imageFormat.hasProperty(propertyId: QTextFormat::ImageHeight)) { |
474 | height = imageFormat.height(); |
475 | } |
476 | |
477 | QString filename = m_strategy->createUniqueImageName(); |
478 | |
479 | m_strategy->addFile(fileName: filename, mimeType, bytes: data); |
480 | |
481 | writer.writeAttribute(namespaceUri: svgNS, name: QString::fromLatin1(ba: "width" ), value: pixelToPoint(pixels: width)); |
482 | writer.writeAttribute(namespaceUri: svgNS, name: QString::fromLatin1(ba: "height" ), value: pixelToPoint(pixels: height)); |
483 | writer.writeAttribute(namespaceUri: textNS, QStringLiteral("anchor-type" ), QStringLiteral("as-char" )); |
484 | writer.writeStartElement(namespaceUri: drawNS, name: QString::fromLatin1(ba: "image" )); |
485 | writer.writeAttribute(namespaceUri: xlinkNS, name: QString::fromLatin1(ba: "href" ), value: filename); |
486 | writer.writeEndElement(); // image |
487 | } |
488 | } |
489 | writer.writeEndElement(); // frame |
490 | } |
491 | |
492 | void QTextOdfWriter::writeFormats(QXmlStreamWriter &writer, const QSet<int> &formats) const |
493 | { |
494 | writer.writeStartElement(namespaceUri: officeNS, name: QString::fromLatin1(ba: "automatic-styles" )); |
495 | QList<QTextFormat> allStyles = m_document->allFormats(); |
496 | for (int formatIndex : formats) { |
497 | QTextFormat textFormat = allStyles.at(i: formatIndex); |
498 | switch (textFormat.type()) { |
499 | case QTextFormat::CharFormat: |
500 | if (textFormat.isTableCellFormat()) |
501 | writeTableCellFormat(writer, format: textFormat.toTableCellFormat(), formatIndex, styles&: allStyles); |
502 | else |
503 | writeCharacterFormat(writer, format: textFormat.toCharFormat(), formatIndex); |
504 | break; |
505 | case QTextFormat::BlockFormat: |
506 | writeBlockFormat(writer, format: textFormat.toBlockFormat(), formatIndex); |
507 | break; |
508 | case QTextFormat::ListFormat: |
509 | writeListFormat(writer, format: textFormat.toListFormat(), formatIndex); |
510 | break; |
511 | case QTextFormat::FrameFormat: |
512 | if (textFormat.isTableFormat()) |
513 | writeTableFormat(writer, format: textFormat.toTableFormat(), formatIndex); |
514 | else |
515 | writeFrameFormat(writer, format: textFormat.toFrameFormat(), formatIndex); |
516 | break; |
517 | } |
518 | } |
519 | |
520 | writer.writeEndElement(); // automatic-styles |
521 | } |
522 | |
523 | void QTextOdfWriter::writeBlockFormat(QXmlStreamWriter &writer, QTextBlockFormat format, int formatIndex) const |
524 | { |
525 | writer.writeStartElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "style" )); |
526 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "name" ), value: QString::fromLatin1(ba: "p%1" ).arg(a: formatIndex)); |
527 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "family" ), value: QString::fromLatin1(ba: "paragraph" )); |
528 | writer.writeStartElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "paragraph-properties" )); |
529 | |
530 | if (format.hasProperty(propertyId: QTextBlockFormat::LineHeightType)) { |
531 | const int blockLineHeightType = format.lineHeightType(); |
532 | const qreal blockLineHeight = format.lineHeight(); |
533 | QString type, value; |
534 | switch (blockLineHeightType) { |
535 | case QTextBlockFormat::SingleHeight: |
536 | type = QString::fromLatin1(ba: "line-height" ); |
537 | value = QString::fromLatin1(ba: "100%" ); |
538 | break; |
539 | case QTextBlockFormat::ProportionalHeight: |
540 | type = QString::fromLatin1(ba: "line-height" ); |
541 | value = QString::number(blockLineHeight) + QString::fromLatin1(ba: "%" ); |
542 | break; |
543 | case QTextBlockFormat::FixedHeight: |
544 | type = QString::fromLatin1(ba: "line-height" ); |
545 | value = pixelToPoint(pixels: qMax(a: qreal(0.), b: blockLineHeight)); |
546 | break; |
547 | case QTextBlockFormat::MinimumHeight: |
548 | type = QString::fromLatin1(ba: "line-height-at-least" ); |
549 | value = pixelToPoint(pixels: qMax(a: qreal(0.), b: blockLineHeight)); |
550 | break; |
551 | case QTextBlockFormat::LineDistanceHeight: |
552 | type = QString::fromLatin1(ba: "line-spacing" ); |
553 | value = pixelToPoint(pixels: qMax(a: qreal(0.), b: blockLineHeight)); |
554 | } |
555 | |
556 | if (!type.isNull()) |
557 | writer.writeAttribute(namespaceUri: styleNS, name: type, value); |
558 | } |
559 | |
560 | if (format.hasProperty(propertyId: QTextFormat::BlockAlignment)) { |
561 | const Qt::Alignment alignment = format.alignment() & Qt::AlignHorizontal_Mask; |
562 | QString value; |
563 | if (alignment == Qt::AlignLeading) |
564 | value = QString::fromLatin1(ba: "start" ); |
565 | else if (alignment == Qt::AlignTrailing) |
566 | value = QString::fromLatin1(ba: "end" ); |
567 | else if (alignment == (Qt::AlignLeft | Qt::AlignAbsolute)) |
568 | value = QString::fromLatin1(ba: "left" ); |
569 | else if (alignment == (Qt::AlignRight | Qt::AlignAbsolute)) |
570 | value = QString::fromLatin1(ba: "right" ); |
571 | else if (alignment == Qt::AlignHCenter) |
572 | value = QString::fromLatin1(ba: "center" ); |
573 | else if (alignment == Qt::AlignJustify) |
574 | value = QString::fromLatin1(ba: "justify" ); |
575 | else |
576 | qWarning() << "QTextOdfWriter: unsupported paragraph alignment; " << format.alignment(); |
577 | if (! value.isNull()) |
578 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "text-align" ), value); |
579 | } |
580 | |
581 | if (format.hasProperty(propertyId: QTextFormat::BlockTopMargin)) |
582 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "margin-top" ), value: pixelToPoint(pixels: qMax(a: qreal(0.), b: format.topMargin())) ); |
583 | if (format.hasProperty(propertyId: QTextFormat::BlockBottomMargin)) |
584 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "margin-bottom" ), value: pixelToPoint(pixels: qMax(a: qreal(0.), b: format.bottomMargin())) ); |
585 | if (format.hasProperty(propertyId: QTextFormat::BlockLeftMargin) || format.hasProperty(propertyId: QTextFormat::BlockIndent)) |
586 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "margin-left" ), value: pixelToPoint(pixels: qMax(a: qreal(0.), |
587 | b: format.leftMargin() + format.indent()))); |
588 | if (format.hasProperty(propertyId: QTextFormat::BlockRightMargin)) |
589 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "margin-right" ), value: pixelToPoint(pixels: qMax(a: qreal(0.), b: format.rightMargin())) ); |
590 | if (format.hasProperty(propertyId: QTextFormat::TextIndent)) |
591 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "text-indent" ), value: pixelToPoint(pixels: format.textIndent())); |
592 | if (format.hasProperty(propertyId: QTextFormat::PageBreakPolicy)) { |
593 | if (format.pageBreakPolicy() & QTextFormat::PageBreak_AlwaysBefore) |
594 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "break-before" ), value: QString::fromLatin1(ba: "page" )); |
595 | if (format.pageBreakPolicy() & QTextFormat::PageBreak_AlwaysAfter) |
596 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "break-after" ), value: QString::fromLatin1(ba: "page" )); |
597 | } |
598 | if (format.hasProperty(propertyId: QTextFormat::BackgroundBrush)) { |
599 | QBrush brush = format.background(); |
600 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "background-color" ), value: brush.color().name()); |
601 | } |
602 | if (format.hasProperty(propertyId: QTextFormat::BlockNonBreakableLines)) |
603 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "keep-together" ), |
604 | value: format.nonBreakableLines() ? QString::fromLatin1(ba: "true" ) : QString::fromLatin1(ba: "false" )); |
605 | if (format.hasProperty(propertyId: QTextFormat::TabPositions)) { |
606 | QList<QTextOption::Tab> tabs = format.tabPositions(); |
607 | writer.writeStartElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "tab-stops" )); |
608 | QList<QTextOption::Tab>::Iterator iterator = tabs.begin(); |
609 | while(iterator != tabs.end()) { |
610 | writer.writeEmptyElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "tab-stop" )); |
611 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "position" ), value: pixelToPoint(pixels: iterator->position) ); |
612 | QString type; |
613 | switch(iterator->type) { |
614 | case QTextOption::DelimiterTab: type = QString::fromLatin1(ba: "char" ); break; |
615 | case QTextOption::LeftTab: type = QString::fromLatin1(ba: "left" ); break; |
616 | case QTextOption::RightTab: type = QString::fromLatin1(ba: "right" ); break; |
617 | case QTextOption::CenterTab: type = QString::fromLatin1(ba: "center" ); break; |
618 | } |
619 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "type" ), value: type); |
620 | if (!iterator->delimiter.isNull()) |
621 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "char" ), value: iterator->delimiter); |
622 | ++iterator; |
623 | } |
624 | |
625 | writer.writeEndElement(); // tab-stops |
626 | } |
627 | |
628 | writer.writeEndElement(); // paragraph-properties |
629 | writer.writeEndElement(); // style |
630 | } |
631 | |
632 | void QTextOdfWriter::writeCharacterFormat(QXmlStreamWriter &writer, QTextCharFormat format, int formatIndex) const |
633 | { |
634 | writer.writeStartElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "style" )); |
635 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "name" ), value: QString::fromLatin1(ba: "c%1" ).arg(a: formatIndex)); |
636 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "family" ), value: QString::fromLatin1(ba: "text" )); |
637 | writer.writeEmptyElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "text-properties" )); |
638 | if (format.fontItalic()) |
639 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "font-style" ), value: QString::fromLatin1(ba: "italic" )); |
640 | if (format.hasProperty(propertyId: QTextFormat::FontWeight) && format.fontWeight() != QFont::Normal) { |
641 | QString value; |
642 | if (format.fontWeight() == QFont::Bold) |
643 | value = QString::fromLatin1(ba: "bold" ); |
644 | else |
645 | value = QString::number(format.fontWeight()); |
646 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "font-weight" ), value); |
647 | } |
648 | if (format.hasProperty(propertyId: QTextFormat::OldFontFamily)) |
649 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "font-family" ), value: format.fontFamilies().toStringList().value(i: 0, defaultValue: QString())); |
650 | else |
651 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "font-family" ), value: QString::fromLatin1(ba: "Sans" )); // Qt default |
652 | if (format.hasProperty(propertyId: QTextFormat::FontPointSize)) |
653 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "font-size" ), value: QString::fromLatin1(ba: "%1pt" ).arg(a: format.fontPointSize())); |
654 | if (format.hasProperty(propertyId: QTextFormat::FontCapitalization)) { |
655 | switch(format.fontCapitalization()) { |
656 | case QFont::MixedCase: |
657 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "text-transform" ), value: QString::fromLatin1(ba: "none" )); break; |
658 | case QFont::AllUppercase: |
659 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "text-transform" ), value: QString::fromLatin1(ba: "uppercase" )); break; |
660 | case QFont::AllLowercase: |
661 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "text-transform" ), value: QString::fromLatin1(ba: "lowercase" )); break; |
662 | case QFont::Capitalize: |
663 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "text-transform" ), value: QString::fromLatin1(ba: "capitalize" )); break; |
664 | case QFont::SmallCaps: |
665 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "font-variant" ), value: QString::fromLatin1(ba: "small-caps" )); break; |
666 | } |
667 | } |
668 | if (format.hasProperty(propertyId: QTextFormat::FontLetterSpacing)) |
669 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "letter-spacing" ), value: pixelToPoint(pixels: format.fontLetterSpacing())); |
670 | if (format.hasProperty(propertyId: QTextFormat::FontWordSpacing) && format.fontWordSpacing() != 0) |
671 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "word-spacing" ), value: pixelToPoint(pixels: format.fontWordSpacing())); |
672 | if (format.hasProperty(propertyId: QTextFormat::FontUnderline)) |
673 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "text-underline-type" ), |
674 | value: format.fontUnderline() ? QString::fromLatin1(ba: "single" ) : QString::fromLatin1(ba: "none" )); |
675 | if (format.hasProperty(propertyId: QTextFormat::FontOverline)) { |
676 | // bool fontOverline () const TODO |
677 | } |
678 | if (format.hasProperty(propertyId: QTextFormat::FontStrikeOut)) |
679 | writer.writeAttribute(namespaceUri: styleNS,name: QString::fromLatin1( ba: "text-line-through-type" ), |
680 | value: format.fontStrikeOut() ? QString::fromLatin1(ba: "single" ) : QString::fromLatin1(ba: "none" )); |
681 | if (format.hasProperty(propertyId: QTextFormat::TextUnderlineColor)) |
682 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "text-underline-color" ), value: format.underlineColor().name()); |
683 | if (format.hasProperty(propertyId: QTextFormat::FontFixedPitch)) { |
684 | // bool fontFixedPitch () const TODO |
685 | } |
686 | if (format.hasProperty(propertyId: QTextFormat::TextUnderlineStyle)) { |
687 | QString value; |
688 | switch (format.underlineStyle()) { |
689 | case QTextCharFormat::NoUnderline: value = QString::fromLatin1(ba: "none" ); break; |
690 | case QTextCharFormat::SingleUnderline: value = QString::fromLatin1(ba: "solid" ); break; |
691 | case QTextCharFormat::DashUnderline: value = QString::fromLatin1(ba: "dash" ); break; |
692 | case QTextCharFormat::DotLine: value = QString::fromLatin1(ba: "dotted" ); break; |
693 | case QTextCharFormat::DashDotLine: value = QString::fromLatin1(ba: "dash-dot" ); break; |
694 | case QTextCharFormat::DashDotDotLine: value = QString::fromLatin1(ba: "dot-dot-dash" ); break; |
695 | case QTextCharFormat::WaveUnderline: value = QString::fromLatin1(ba: "wave" ); break; |
696 | case QTextCharFormat::SpellCheckUnderline: value = QString::fromLatin1(ba: "none" ); break; |
697 | } |
698 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "text-underline-style" ), value); |
699 | } |
700 | if (format.hasProperty(propertyId: QTextFormat::TextVerticalAlignment)) { |
701 | QString value; |
702 | switch (format.verticalAlignment()) { |
703 | case QTextCharFormat::AlignMiddle: |
704 | case QTextCharFormat::AlignNormal: value = QString::fromLatin1(ba: "0%" ); break; |
705 | case QTextCharFormat::AlignSuperScript: value = QString::fromLatin1(ba: "super" ); break; |
706 | case QTextCharFormat::AlignSubScript: value = QString::fromLatin1(ba: "sub" ); break; |
707 | case QTextCharFormat::AlignTop: value = QString::fromLatin1(ba: "100%" ); break; |
708 | case QTextCharFormat::AlignBottom : value = QString::fromLatin1(ba: "-100%" ); break; |
709 | case QTextCharFormat::AlignBaseline: break; |
710 | } |
711 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "text-position" ), value); |
712 | } |
713 | if (format.hasProperty(propertyId: QTextFormat::TextOutline)) |
714 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "text-outline" ), value: QString::fromLatin1(ba: "true" )); |
715 | if (format.hasProperty(propertyId: QTextFormat::TextToolTip)) { |
716 | // QString toolTip () const TODO |
717 | } |
718 | if (format.hasProperty(propertyId: QTextFormat::IsAnchor)) { |
719 | // bool isAnchor () const TODO |
720 | } |
721 | if (format.hasProperty(propertyId: QTextFormat::AnchorHref)) { |
722 | // QString anchorHref () const TODO |
723 | } |
724 | if (format.hasProperty(propertyId: QTextFormat::AnchorName)) { |
725 | // QString anchorName () const TODO |
726 | } |
727 | if (format.hasProperty(propertyId: QTextFormat::ForegroundBrush)) { |
728 | QBrush brush = format.foreground(); |
729 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "color" ), value: brush.color().name()); |
730 | } |
731 | if (format.hasProperty(propertyId: QTextFormat::BackgroundBrush)) { |
732 | QBrush brush = format.background(); |
733 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "background-color" ), value: brush.color().name()); |
734 | } |
735 | |
736 | writer.writeEndElement(); // style |
737 | } |
738 | |
739 | void QTextOdfWriter::writeListFormat(QXmlStreamWriter &writer, QTextListFormat format, int formatIndex) const |
740 | { |
741 | writer.writeStartElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "list-style" )); |
742 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "name" ), value: QString::fromLatin1(ba: "L%1" ).arg(a: formatIndex)); |
743 | |
744 | QTextListFormat::Style style = format.style(); |
745 | if (style == QTextListFormat::ListDecimal || style == QTextListFormat::ListLowerAlpha |
746 | || style == QTextListFormat::ListUpperAlpha |
747 | || style == QTextListFormat::ListLowerRoman |
748 | || style == QTextListFormat::ListUpperRoman) { |
749 | writer.writeStartElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "list-level-style-number" )); |
750 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "num-format" ), value: bulletChar(style)); |
751 | |
752 | if (format.hasProperty(propertyId: QTextFormat::ListNumberSuffix)) |
753 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "num-suffix" ), value: format.numberSuffix()); |
754 | else |
755 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "num-suffix" ), value: QString::fromLatin1(ba: "." )); |
756 | |
757 | if (format.hasProperty(propertyId: QTextFormat::ListNumberPrefix)) |
758 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "num-prefix" ), value: format.numberPrefix()); |
759 | |
760 | } else { |
761 | writer.writeStartElement(namespaceUri: textNS, name: QString::fromLatin1(ba: "list-level-style-bullet" )); |
762 | writer.writeAttribute(namespaceUri: textNS, name: QString::fromLatin1(ba: "bullet-char" ), value: bulletChar(style)); |
763 | } |
764 | |
765 | writer.writeAttribute(namespaceUri: textNS, name: QString::fromLatin1(ba: "level" ), value: QString::number(format.indent())); |
766 | writer.writeEmptyElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "list-level-properties" )); |
767 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "text-align" ), value: QString::fromLatin1(ba: "start" )); |
768 | QString spacing = QString::fromLatin1(ba: "%1mm" ).arg(a: format.indent() * 8); |
769 | writer.writeAttribute(namespaceUri: textNS, name: QString::fromLatin1(ba: "space-before" ), value: spacing); |
770 | //writer.writeAttribute(textNS, QString::fromLatin1("min-label-width"), spacing); |
771 | |
772 | writer.writeEndElement(); // list-level-style-* |
773 | writer.writeEndElement(); // list-style |
774 | } |
775 | |
776 | void QTextOdfWriter::writeFrameFormat(QXmlStreamWriter &writer, QTextFrameFormat format, int formatIndex) const |
777 | { |
778 | writer.writeStartElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "style" )); |
779 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "name" ), value: QString::fromLatin1(ba: "s%1" ).arg(a: formatIndex)); |
780 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "family" ), value: QString::fromLatin1(ba: "section" )); |
781 | writer.writeEmptyElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "section-properties" )); |
782 | if (format.hasProperty(propertyId: QTextFormat::FrameTopMargin)) |
783 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "margin-top" ), value: pixelToPoint(pixels: qMax(a: qreal(0.), b: format.topMargin())) ); |
784 | if (format.hasProperty(propertyId: QTextFormat::FrameBottomMargin)) |
785 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "margin-bottom" ), value: pixelToPoint(pixels: qMax(a: qreal(0.), b: format.bottomMargin())) ); |
786 | if (format.hasProperty(propertyId: QTextFormat::FrameLeftMargin)) |
787 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "margin-left" ), value: pixelToPoint(pixels: qMax(a: qreal(0.), b: format.leftMargin())) ); |
788 | if (format.hasProperty(propertyId: QTextFormat::FrameRightMargin)) |
789 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "margin-right" ), value: pixelToPoint(pixels: qMax(a: qreal(0.), b: format.rightMargin())) ); |
790 | |
791 | writer.writeEndElement(); // style |
792 | |
793 | // TODO consider putting the following properties in a qt-namespace. |
794 | // Position position () const |
795 | // qreal border () const |
796 | // QBrush borderBrush () const |
797 | // BorderStyle borderStyle () const |
798 | // qreal padding () const |
799 | // QTextLength width () const |
800 | // QTextLength height () const |
801 | // PageBreakFlags pageBreakPolicy () const |
802 | } |
803 | |
804 | void QTextOdfWriter::writeTableFormat(QXmlStreamWriter &writer, QTextTableFormat format, int formatIndex) const |
805 | { |
806 | // start writing table style element |
807 | writer.writeStartElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "style" )); |
808 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "name" ), |
809 | value: QString::fromLatin1(ba: "Table%1" ).arg(a: formatIndex)); |
810 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "family" ), value: QString::fromLatin1(ba: "table" )); |
811 | writer.writeEmptyElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "table-properties" )); |
812 | |
813 | if (m_tableFormatsWithBorders.contains(value: formatIndex)) { |
814 | // write border format collapsing to table style |
815 | writer.writeAttribute(namespaceUri: tableNS, name: QString::fromLatin1(ba: "border-model" ), |
816 | value: QString::fromLatin1(ba: "collapsing" )); |
817 | } |
818 | const char* align = nullptr; |
819 | switch (format.alignment()) { |
820 | case Qt::AlignLeft: |
821 | align = "left" ; |
822 | break; |
823 | case Qt::AlignRight: |
824 | align = "right" ; |
825 | break; |
826 | case Qt::AlignHCenter: |
827 | align = "center" ; |
828 | break; |
829 | case Qt::AlignJustify: |
830 | align = "margins" ; |
831 | break; |
832 | } |
833 | if (align) |
834 | writer.writeAttribute(namespaceUri: tableNS, name: QString::fromLatin1(ba: "align" ), value: QString::fromLatin1(ba: align)); |
835 | if (format.width().rawValue()) { |
836 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "width" ), |
837 | value: QString::number(format.width().rawValue()) + "pt"_L1 ); |
838 | } |
839 | writer.writeEndElement(); |
840 | // start writing table-column style element |
841 | if (format.columnWidthConstraints().size()) { |
842 | // write table-column-properties for columns with constraints |
843 | m_tableFormatsWithColWidthConstraints.insert(value: formatIndex); // needed for linking of columns to styles |
844 | for (int colit = 0; colit < format.columnWidthConstraints().size(); ++colit) { |
845 | writer.writeStartElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "style" )); |
846 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "name" ), |
847 | value: QString::fromLatin1(ba: "Table%1.%2" ).arg(a: formatIndex).arg(a: colit)); |
848 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "family" ), value: QString::fromLatin1(ba: "table-column" )); |
849 | writer.writeEmptyElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "table-column-properties" )); |
850 | QString columnWidth; |
851 | if (format.columnWidthConstraints().at(i: colit).type() == QTextLength::PercentageLength) { |
852 | columnWidth = QString::number(format.columnWidthConstraints().at(i: colit).rawValue()) |
853 | + "%"_L1 ; |
854 | } else if (format.columnWidthConstraints().at(i: colit).type() == QTextLength::FixedLength) { |
855 | columnWidth = QString::number(format.columnWidthConstraints().at(i: colit).rawValue()) |
856 | + "pt"_L1 ; |
857 | } else { |
858 | //!! HARD-CODING variableWidth Constraints to 100% / nr constraints |
859 | columnWidth = QString::number(100 / format.columnWidthConstraints().size()) |
860 | + "%"_L1 ; |
861 | } |
862 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "column-width" ), value: columnWidth); |
863 | writer.writeEndElement(); |
864 | } |
865 | } |
866 | } |
867 | |
868 | void QTextOdfWriter::writeTableCellFormat(QXmlStreamWriter &writer, QTextTableCellFormat format, |
869 | int formatIndex, QList<QTextFormat> &styles) const |
870 | { |
871 | // check for all table cells here if they are in a table with border |
872 | if (m_cellFormatsInTablesWithBorders.contains(key: formatIndex)) { |
873 | const QList<int> tableIdVector = m_cellFormatsInTablesWithBorders.value(key: formatIndex); |
874 | for (const auto &tableId : tableIdVector) { |
875 | const auto &tmpStyle = styles.at(i: tableId); |
876 | if (tmpStyle.isTableFormat()) { |
877 | QTextTableFormat tableFormatTmp = tmpStyle.toTableFormat(); |
878 | tableCellStyleElement(writer, formatIndex, format, hasBorder: true, tableId, tableFormatTmp); |
879 | } else { |
880 | qDebug(msg: "QTextOdfWriter::writeTableCellFormat: ERROR writing table border format" ); |
881 | } |
882 | } |
883 | } |
884 | tableCellStyleElement(writer, formatIndex, format, hasBorder: false); |
885 | } |
886 | |
887 | void QTextOdfWriter::tableCellStyleElement(QXmlStreamWriter &writer, const int &formatIndex, |
888 | const QTextTableCellFormat &format, |
889 | bool hasBorder, int tableId, |
890 | const QTextTableFormat tableFormatTmp) const { |
891 | writer.writeStartElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "style" )); |
892 | if (hasBorder) { |
893 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "name" ), |
894 | value: QString::fromLatin1(ba: "TB%1.%2" ).arg(a: tableId).arg(a: formatIndex)); |
895 | } else { |
896 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "name" ), value: QString::fromLatin1(ba: "T%1" ).arg(a: formatIndex)); |
897 | } |
898 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "family" ), value: QString::fromLatin1(ba: "table-cell" )); |
899 | writer.writeEmptyElement(namespaceUri: styleNS, name: QString::fromLatin1(ba: "table-cell-properties" )); |
900 | if (hasBorder) { |
901 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "border" ), |
902 | value: pixelToPoint(pixels: tableFormatTmp.border()) + " "_L1 |
903 | + borderStyleName(style: tableFormatTmp.borderStyle()) + " "_L1 |
904 | + tableFormatTmp.borderBrush().color().name(format: QColor::HexRgb)); |
905 | } |
906 | qreal topPadding = format.topPadding(); |
907 | qreal padding = topPadding + tableFormatTmp.cellPadding(); |
908 | if (padding > 0 && topPadding == format.bottomPadding() |
909 | && topPadding == format.leftPadding() && topPadding == format.rightPadding()) { |
910 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "padding" ), value: pixelToPoint(pixels: padding)); |
911 | } |
912 | else { |
913 | if (padding > 0) |
914 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "padding-top" ), value: pixelToPoint(pixels: padding)); |
915 | padding = format.bottomPadding() + tableFormatTmp.cellPadding(); |
916 | if (padding > 0) |
917 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "padding-bottom" ), |
918 | value: pixelToPoint(pixels: padding)); |
919 | padding = format.leftPadding() + tableFormatTmp.cellPadding(); |
920 | if (padding > 0) |
921 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "padding-left" ), |
922 | value: pixelToPoint(pixels: padding)); |
923 | padding = format.rightPadding() + tableFormatTmp.cellPadding(); |
924 | if (padding > 0) |
925 | writer.writeAttribute(namespaceUri: foNS, name: QString::fromLatin1(ba: "padding-right" ), |
926 | value: pixelToPoint(pixels: padding)); |
927 | } |
928 | |
929 | if (format.hasProperty(propertyId: QTextFormat::TextVerticalAlignment)) { |
930 | QString pos; |
931 | switch (format.verticalAlignment()) { // TODO - review: doesn't handle all cases |
932 | case QTextCharFormat::AlignMiddle: |
933 | pos = QString::fromLatin1(ba: "middle" ); break; |
934 | case QTextCharFormat::AlignTop: |
935 | pos = QString::fromLatin1(ba: "top" ); break; |
936 | case QTextCharFormat::AlignBottom: |
937 | pos = QString::fromLatin1(ba: "bottom" ); break; |
938 | default: |
939 | pos = QString::fromLatin1(ba: "automatic" ); break; |
940 | } |
941 | writer.writeAttribute(namespaceUri: styleNS, name: QString::fromLatin1(ba: "vertical-align" ), value: pos); |
942 | } |
943 | |
944 | // TODO |
945 | // ODF just search for style-table-cell-properties-attlist) |
946 | // QTextFormat::BackgroundImageUrl |
947 | // format.background |
948 | writer.writeEndElement(); // style |
949 | } |
950 | |
951 | /////////////////////// |
952 | |
953 | QTextOdfWriter::QTextOdfWriter(const QTextDocument &document, QIODevice *device) |
954 | : officeNS ("urn:oasis:names:tc:opendocument:xmlns:office:1.0"_L1 ), |
955 | textNS ("urn:oasis:names:tc:opendocument:xmlns:text:1.0"_L1 ), |
956 | styleNS ("urn:oasis:names:tc:opendocument:xmlns:style:1.0"_L1 ), |
957 | foNS ("urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"_L1 ), |
958 | tableNS ("urn:oasis:names:tc:opendocument:xmlns:table:1.0"_L1 ), |
959 | drawNS ("urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"_L1 ), |
960 | xlinkNS ("http://www.w3.org/1999/xlink"_L1 ), |
961 | svgNS ("urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"_L1 ), |
962 | m_document(&document), |
963 | m_device(device), |
964 | m_strategy(nullptr), |
965 | m_createArchive(true) |
966 | { |
967 | } |
968 | |
969 | bool QTextOdfWriter::writeAll() |
970 | { |
971 | if (m_createArchive) |
972 | m_strategy = new QZipStreamStrategy(m_device); |
973 | else |
974 | m_strategy = new QXmlStreamStrategy(m_device); |
975 | |
976 | if (!m_device->isWritable() && ! m_device->open(mode: QIODevice::WriteOnly)) { |
977 | qWarning(msg: "QTextOdfWriter::writeAll: the device cannot be opened for writing" ); |
978 | return false; |
979 | } |
980 | QXmlStreamWriter writer(m_strategy->contentStream); |
981 | // prettyfy |
982 | writer.setAutoFormatting(true); |
983 | writer.setAutoFormattingIndent(2); |
984 | |
985 | writer.writeNamespace(namespaceUri: officeNS, prefix: QString::fromLatin1(ba: "office" )); |
986 | writer.writeNamespace(namespaceUri: textNS, prefix: QString::fromLatin1(ba: "text" )); |
987 | writer.writeNamespace(namespaceUri: styleNS, prefix: QString::fromLatin1(ba: "style" )); |
988 | writer.writeNamespace(namespaceUri: foNS, prefix: QString::fromLatin1(ba: "fo" )); |
989 | writer.writeNamespace(namespaceUri: tableNS, prefix: QString::fromLatin1(ba: "table" )); |
990 | writer.writeNamespace(namespaceUri: drawNS, prefix: QString::fromLatin1(ba: "draw" )); |
991 | writer.writeNamespace(namespaceUri: xlinkNS, prefix: QString::fromLatin1(ba: "xlink" )); |
992 | writer.writeNamespace(namespaceUri: svgNS, prefix: QString::fromLatin1(ba: "svg" )); |
993 | writer.writeStartDocument(); |
994 | writer.writeStartElement(namespaceUri: officeNS, name: QString::fromLatin1(ba: "document-content" )); |
995 | writer.writeAttribute(namespaceUri: officeNS, name: QString::fromLatin1(ba: "version" ), value: QString::fromLatin1(ba: "1.2" )); |
996 | |
997 | // add fragments. (for character formats) |
998 | QTextDocumentPrivate::FragmentIterator fragIt = QTextDocumentPrivate::get(document: m_document)->begin(); |
999 | QSet<int> formats; |
1000 | while (fragIt != QTextDocumentPrivate::get(document: m_document)->end()) { |
1001 | const QTextFragmentData * const frag = fragIt.value(); |
1002 | formats << frag->format; |
1003 | ++fragIt; |
1004 | } |
1005 | |
1006 | // add blocks (for blockFormats) |
1007 | QTextDocumentPrivate::BlockMap &blocks = const_cast<QTextDocumentPrivate *>(QTextDocumentPrivate::get(document: m_document))->blockMap(); |
1008 | QTextDocumentPrivate::BlockMap::Iterator blockIt = blocks.begin(); |
1009 | while (blockIt != blocks.end()) { |
1010 | const QTextBlockData * const block = blockIt.value(); |
1011 | formats << block->format; |
1012 | ++blockIt; |
1013 | } |
1014 | |
1015 | // add objects for lists, frames and tables |
1016 | const QList<QTextFormat> allFormats = m_document->allFormats(); |
1017 | const QList<int> copy = formats.values(); |
1018 | for (auto index : copy) { |
1019 | QTextObject *object = m_document->objectForFormat(allFormats[index]); |
1020 | if (object) { |
1021 | formats << object->formatIndex(); |
1022 | if (auto *tableobject = qobject_cast<QTextTable *>(object)) { |
1023 | if (tableobject->format().borderStyle()) { |
1024 | int tableID = tableobject->formatIndex(); |
1025 | m_tableFormatsWithBorders.insert(value: tableID); |
1026 | // loop through all rows and cols of table and store cell IDs, |
1027 | // create Hash with cell ID as Key and table IDs as Vector |
1028 | for (int rowindex = 0; rowindex < tableobject->rows(); ++rowindex) { |
1029 | for (int colindex = 0; colindex < tableobject->columns(); ++colindex) { |
1030 | const int cellFormatID = tableobject->cellAt(row: rowindex, col: colindex).tableCellFormatIndex(); |
1031 | QList<int> tableIdsTmp; |
1032 | if (m_cellFormatsInTablesWithBorders.contains(key: cellFormatID)) |
1033 | tableIdsTmp = m_cellFormatsInTablesWithBorders.value(key: cellFormatID); |
1034 | if (!tableIdsTmp.contains(t: tableID)) |
1035 | tableIdsTmp.append(t: tableID); |
1036 | m_cellFormatsInTablesWithBorders.insert(key: cellFormatID, value: tableIdsTmp); |
1037 | } |
1038 | } |
1039 | } |
1040 | } |
1041 | } |
1042 | } |
1043 | |
1044 | writeFormats(writer, formats); |
1045 | |
1046 | writer.writeStartElement(namespaceUri: officeNS, name: QString::fromLatin1(ba: "body" )); |
1047 | writer.writeStartElement(namespaceUri: officeNS, name: QString::fromLatin1(ba: "text" )); |
1048 | QTextFrame *rootFrame = m_document->rootFrame(); |
1049 | writeFrame(writer, frame: rootFrame); |
1050 | writer.writeEndElement(); // text |
1051 | writer.writeEndElement(); // body |
1052 | writer.writeEndElement(); // document-content |
1053 | writer.writeEndDocument(); |
1054 | delete m_strategy; |
1055 | m_strategy = nullptr; |
1056 | |
1057 | return true; |
1058 | } |
1059 | |
1060 | QT_END_NAMESPACE |
1061 | |
1062 | #endif // QT_NO_TEXTODFWRITER |
1063 | |