Add more formatting to QTextDocumentWriter when writing ODF files

QTextDocumentWriter only supports a small subset of table formatting
when exporting to ODF-format. This patch adds more formatting
capabilities to the ODF exporter:
- table border support
- table alignment
- table width
- respect column constraints (column widths)
- add a tab before soft line breaks. This will avoid causing the last
line to stretch all over the cell in justified paragraphs.

With this patch, line height settings are now exported, too.

[ChangeLog][QtGui][QDocumentWriter] QDocumentWriter now supports table
borders, table alignment, table width, column widths, line height and
image resolution when exporting QTextDocuments to ODF files.

Task-number: QTBUG-63581
Change-Id: I2d269ef0f842e73af64d48bfef531d5fa3078088
Reviewed-by: Lars Knoll <lars.knoll@qt.io>
This commit is contained in:
Daniel Savi 2017-10-04 18:10:48 +02:00
parent f8e551cf08
commit 6a4b6971f4
3 changed files with 242 additions and 24 deletions

View File

@ -183,6 +183,35 @@ static QString bulletChar(QTextListFormat::Style style)
}
}
static QString borderStyleName(QTextFrameFormat::BorderStyle style)
{
switch (style) {
case QTextFrameFormat::BorderStyle_None:
return QString::fromLatin1("none");
case QTextFrameFormat::BorderStyle_Dotted:
return QString::fromLatin1("dotted");
case QTextFrameFormat::BorderStyle_Dashed:
return QString::fromLatin1("dashed");
case QTextFrameFormat::BorderStyle_Solid:
return QString::fromLatin1("solid");
case QTextFrameFormat::BorderStyle_Double:
return QString::fromLatin1("double");
case QTextFrameFormat::BorderStyle_DotDash:
return QString::fromLatin1("dashed");
case QTextFrameFormat::BorderStyle_DotDotDash:
return QString::fromLatin1("dotted");
case QTextFrameFormat::BorderStyle_Groove:
return QString::fromLatin1("groove");
case QTextFrameFormat::BorderStyle_Ridge:
return QString::fromLatin1("ridge");
case QTextFrameFormat::BorderStyle_Inset:
return QString::fromLatin1("inset");
case QTextFrameFormat::BorderStyle_Outset:
return QString::fromLatin1("outset");
}
return QString::fromLatin1("");
}
void QTextOdfWriter::writeFrame(QXmlStreamWriter &writer, const QTextFrame *frame)
{
Q_ASSERT(frame);
@ -190,8 +219,21 @@ void QTextOdfWriter::writeFrame(QXmlStreamWriter &writer, const QTextFrame *fram
if (table) { // Start a table.
writer.writeStartElement(tableNS, QString::fromLatin1("table"));
writer.writeEmptyElement(tableNS, QString::fromLatin1("table-column"));
writer.writeAttribute(tableNS, QString::fromLatin1("number-columns-repeated"), QString::number(table->columns()));
writer.writeAttribute(tableNS, QString::fromLatin1("style-name"),
QString::fromLatin1("Table%1").arg(table->formatIndex()));
// check if column widths are set, if so add TableNS line above for all columns and link to style
if (m_tableFormatsWithColWidthConstraints.contains(table->formatIndex())) {
for (int colit = 0; colit < table->columns(); ++colit) {
writer.writeStartElement(tableNS, QString::fromLatin1("table-column"));
writer.writeAttribute(tableNS, QString::fromLatin1("style-name"),
QString::fromLatin1("Table%1.%2").arg(table->formatIndex()).arg(colit));
writer.writeEndElement();
}
} else {
writer.writeEmptyElement(tableNS, QString::fromLatin1("table-column"));
writer.writeAttribute(tableNS, QString::fromLatin1("number-columns-repeated"),
QString::number(table->columns()));
}
} else if (frame->document() && frame->document()->rootFrame() != frame) { // start a section
writer.writeStartElement(textNS, QString::fromLatin1("section"));
}
@ -219,7 +261,15 @@ void QTextOdfWriter::writeFrame(QXmlStreamWriter &writer, const QTextFrame *fram
if (cell.rowSpan() > 1)
writer.writeAttribute(tableNS, QString::fromLatin1("number-rows-spanned"), QString::number(cell.rowSpan()));
if (cell.format().isTableCellFormat()) {
writer.writeAttribute(tableNS, QString::fromLatin1("style-name"), QString::fromLatin1("T%1").arg(cell.tableCellFormatIndex()));
if (m_cellFormatsInTablesWithBorders.contains(cell.tableCellFormatIndex()) ) {
// writing table:style-name tag in <table:table-cell> element
writer.writeAttribute(tableNS, QString::fromLatin1("style-name"),
QString::fromLatin1("TB%1.%2").arg(table->formatIndex())
.arg(cell.tableCellFormatIndex()));
} else {
writer.writeAttribute(tableNS, QString::fromLatin1("style-name"),
QString::fromLatin1("T%1").arg(cell.tableCellFormatIndex()));
}
}
}
writeBlock(writer, block);
@ -326,7 +376,10 @@ void QTextOdfWriter::writeBlock(QXmlStreamWriter &writer, const QTextBlock &bloc
if (i < fragmentText.count()) {
if (character.unicode() == 0x2028) { // soft-return
//if (exportedIndex < i)
writer.writeCharacters(fragmentText.mid(exportedIndex, i - exportedIndex));
writer.writeCharacters(fragmentText.mid(exportedIndex, i - exportedIndex));
// adding tab before line-break, so last line in justified paragraph
// will not stretch to the end
writer.writeEmptyElement(textNS, QString::fromLatin1("tab"));
writer.writeEmptyElement(textNS, QString::fromLatin1("line-break"));
exportedIndex = i+1;
continue;
@ -387,7 +440,6 @@ void QTextOdfWriter::writeInlineCharacter(QXmlStreamWriter &writer, const QTextF
}
}
// ^^^ Copy pasted mostly from Qt =================
if (! image.isNull()) {
QBuffer imageBytes;
QImageWriter imageWriter(&imageBytes, "png");
@ -396,17 +448,17 @@ void QTextOdfWriter::writeInlineCharacter(QXmlStreamWriter &writer, const QTextF
m_strategy->addFile(filename, QString::fromLatin1("image/png"), imageBytes.data());
// get the width/height from the format.
qreal width = (imageFormat.hasProperty(QTextFormat::ImageWidth)) ? imageFormat.width() : image.width();
qreal width = imageFormat.hasProperty(QTextFormat::ImageWidth)
? imageFormat.width() : image.width();
writer.writeAttribute(svgNS, QString::fromLatin1("width"), pixelToPoint(width));
qreal height = (imageFormat.hasProperty(QTextFormat::ImageHeight)) ? imageFormat.height() : image.height();
qreal height = imageFormat.hasProperty(QTextFormat::ImageHeight)
? imageFormat.height() : image.height();
writer.writeAttribute(svgNS, QString::fromLatin1("height"), pixelToPoint(height));
writer.writeStartElement(drawNS, QString::fromLatin1("image"));
writer.writeAttribute(xlinkNS, QString::fromLatin1("href"), filename);
writer.writeEndElement(); // image
}
}
writer.writeEndElement(); // frame
}
@ -421,7 +473,7 @@ void QTextOdfWriter::writeFormats(QXmlStreamWriter &writer, const QSet<int> &for
switch (textFormat.type()) {
case QTextFormat::CharFormat:
if (textFormat.isTableCellFormat())
writeTableCellFormat(writer, textFormat.toTableCellFormat(), formatIndex);
writeTableCellFormat(writer, textFormat.toTableCellFormat(), formatIndex, allStyles);
else
writeCharacterFormat(writer, textFormat.toCharFormat(), formatIndex);
break;
@ -432,10 +484,15 @@ void QTextOdfWriter::writeFormats(QXmlStreamWriter &writer, const QSet<int> &for
writeListFormat(writer, textFormat.toListFormat(), formatIndex);
break;
case QTextFormat::FrameFormat:
writeFrameFormat(writer, textFormat.toFrameFormat(), formatIndex);
if (textFormat.isTableFormat())
writeTableFormat(writer, textFormat.toTableFormat(), formatIndex);
else
writeFrameFormat(writer, textFormat.toFrameFormat(), formatIndex);
break;
case QTextFormat::TableFormat:
;break;
// this case never happens, because TableFormat is a FrameFormat
Q_UNREACHABLE();
break;
}
}
@ -449,6 +506,36 @@ void QTextOdfWriter::writeBlockFormat(QXmlStreamWriter &writer, QTextBlockFormat
writer.writeAttribute(styleNS, QString::fromLatin1("family"), QString::fromLatin1("paragraph"));
writer.writeStartElement(styleNS, QString::fromLatin1("paragraph-properties"));
if (format.hasProperty(QTextBlockFormat::LineHeightType)) {
const int blockLineHeightType = format.lineHeightType();
const qreal blockLineHeight = format.lineHeight();
QString type, value;
switch (blockLineHeightType) {
case QTextBlockFormat::SingleHeight:
type = QString::fromLatin1("line-height");
value = QString::fromLatin1("100%");
break;
case QTextBlockFormat::ProportionalHeight:
type = QString::fromLatin1("line-height");
value = QString::number(blockLineHeight) + QString::fromLatin1("%");
break;
case QTextBlockFormat::FixedHeight:
type = QString::fromLatin1("line-height");
value = pixelToPoint(qMax(qreal(0.), blockLineHeight));
break;
case QTextBlockFormat::MinimumHeight:
type = QString::fromLatin1("line-height-at-least");
value = pixelToPoint(qMax(qreal(0.), blockLineHeight));
break;
case QTextBlockFormat::LineDistanceHeight:
type = QString::fromLatin1("line-spacing");
value = pixelToPoint(qMax(qreal(0.), blockLineHeight));
}
if (!type.isNull())
writer.writeAttribute(styleNS, type, value);
}
if (format.hasProperty(QTextFormat::BlockAlignment)) {
const Qt::Alignment alignment = format.alignment() & Qt::AlignHorizontal_Mask;
QString value;
@ -693,14 +780,108 @@ void QTextOdfWriter::writeFrameFormat(QXmlStreamWriter &writer, QTextFrameFormat
// PageBreakFlags pageBreakPolicy () const
}
void QTextOdfWriter::writeTableCellFormat(QXmlStreamWriter &writer, QTextTableCellFormat format, int formatIndex) const
void QTextOdfWriter::writeTableFormat(QXmlStreamWriter &writer, QTextTableFormat format, int formatIndex) const
{
// start writing table style element
writer.writeStartElement(styleNS, QString::fromLatin1("style"));
writer.writeAttribute(styleNS, QString::fromLatin1("name"), QString::fromLatin1("T%1").arg(formatIndex));
writer.writeAttribute(styleNS, QString::fromLatin1("name"),
QString::fromLatin1("Table%1").arg(formatIndex));
writer.writeAttribute(styleNS, QString::fromLatin1("family"), QString::fromLatin1("table"));
writer.writeEmptyElement(styleNS, QString::fromLatin1("table-properties"));
if (m_tableFormatsWithBorders.contains(formatIndex)) {
// write border format collapsing to table style
writer.writeAttribute(tableNS, QString::fromLatin1("border-model"),
QString::fromLatin1("collapsing"));
}
const char* align = nullptr;
switch (format.alignment()) {
case Qt::AlignLeft:
align = "left";
break;
case Qt::AlignRight:
align = "right";
break;
case Qt::AlignHCenter:
align = "center";
break;
case Qt::AlignJustify:
align = "margins";
break;
}
if (align)
writer.writeAttribute(tableNS, QString::fromLatin1("align"), QString::fromLatin1(align));
if (format.width().rawValue()) {
writer.writeAttribute(styleNS, QString::fromLatin1("width"),
QString::number(format.width().rawValue()) + QLatin1String("pt"));
}
writer.writeEndElement();
// start writing table-column style element
if (format.columnWidthConstraints().size()) {
// write table-column-properties for columns with constraints
m_tableFormatsWithColWidthConstraints.insert(formatIndex); // needed for linking of columns to styles
for (int colit = 0; colit < format.columnWidthConstraints().size(); ++colit) {
writer.writeStartElement(styleNS, QString::fromLatin1("style"));
writer.writeAttribute(styleNS, QString::fromLatin1("name"),
QString::fromLatin1("Table%1.%2").arg(formatIndex).arg(colit));
writer.writeAttribute(styleNS, QString::fromLatin1("family"), QString::fromLatin1("table-column"));
writer.writeEmptyElement(styleNS, QString::fromLatin1("table-column-properties"));
QString columnWidth;
if (format.columnWidthConstraints().at(colit).type() == QTextLength::PercentageLength) {
columnWidth = QString::number(format.columnWidthConstraints().at(colit).rawValue())
+ QLatin1String("%");
} else if (format.columnWidthConstraints().at(colit).type() == QTextLength::FixedLength) {
columnWidth = QString::number(format.columnWidthConstraints().at(colit).rawValue())
+ QLatin1String("pt");
} else {
//!! HARD-CODING variableWidth Constraints to 100% / nr constraints
columnWidth = QString::number(100 / format.columnWidthConstraints().size())
+ QLatin1String("%");
}
writer.writeAttribute(styleNS, QString::fromLatin1("column-width"), columnWidth);
writer.writeEndElement();
}
}
}
void QTextOdfWriter::writeTableCellFormat(QXmlStreamWriter &writer, QTextTableCellFormat format,
int formatIndex, QVector<QTextFormat> &styles) const
{
// check for all table cells here if they are in a table with border
if (m_cellFormatsInTablesWithBorders.contains(formatIndex)) {
const QVector<int> tableIdVector = m_cellFormatsInTablesWithBorders.value(formatIndex);
for (const auto &tableId : tableIdVector) {
const auto &tmpStyle = styles.at(tableId);
if (tmpStyle.isTableFormat()) {
QTextTableFormat tableFormatTmp = tmpStyle.toTableFormat();
tableCellStyleElement(writer, formatIndex, format, true, tableId, tableFormatTmp);
} else {
qDebug("QTextOdfWriter::writeTableCellFormat: ERROR writing table border format");
}
}
}
tableCellStyleElement(writer, formatIndex, format, false);
}
void QTextOdfWriter::tableCellStyleElement(QXmlStreamWriter &writer, const int &formatIndex,
const QTextTableCellFormat &format,
bool hasBorder, int tableId,
const QTextTableFormat tableFormatTmp) const {
writer.writeStartElement(styleNS, QString::fromLatin1("style"));
if (hasBorder) {
writer.writeAttribute(styleNS, QString::fromLatin1("name"),
QString::fromLatin1("TB%1.%2").arg(tableId).arg(formatIndex));
} else {
writer.writeAttribute(styleNS, QString::fromLatin1("name"), QString::fromLatin1("T%1").arg(formatIndex));
}
writer.writeAttribute(styleNS, QString::fromLatin1("family"), QString::fromLatin1("table-cell"));
writer.writeEmptyElement(styleNS, QString::fromLatin1("table-cell-properties"));
if (hasBorder) {
writer.writeAttribute(foNS, QString::fromLatin1("border"),
pixelToPoint(tableFormatTmp.border()) + QLatin1String(" ")
+ borderStyleName(tableFormatTmp.borderStyle())
+ QLatin1String(" #000000")); //!! HARD-CODING color black
}
qreal padding = format.topPadding();
if (padding > 0 && padding == format.bottomPadding()
&& padding == format.leftPadding() && padding == format.rightPadding()) {
@ -710,16 +891,19 @@ void QTextOdfWriter::writeTableCellFormat(QXmlStreamWriter &writer, QTextTableCe
if (padding > 0)
writer.writeAttribute(foNS, QString::fromLatin1("padding-top"), pixelToPoint(padding));
if (format.bottomPadding() > 0)
writer.writeAttribute(foNS, QString::fromLatin1("padding-bottom"), pixelToPoint(format.bottomPadding()));
writer.writeAttribute(foNS, QString::fromLatin1("padding-bottom"),
pixelToPoint(format.bottomPadding()));
if (format.leftPadding() > 0)
writer.writeAttribute(foNS, QString::fromLatin1("padding-left"), pixelToPoint(format.leftPadding()));
writer.writeAttribute(foNS, QString::fromLatin1("padding-left"),
pixelToPoint(format.leftPadding()));
if (format.rightPadding() > 0)
writer.writeAttribute(foNS, QString::fromLatin1("padding-right"), pixelToPoint(format.rightPadding()));
writer.writeAttribute(foNS, QString::fromLatin1("padding-right"),
pixelToPoint(format.rightPadding()));
}
if (format.hasProperty(QTextFormat::TextVerticalAlignment)) {
QString pos;
switch (format.verticalAlignment()) {
switch (format.verticalAlignment()) { // TODO - review: doesn't handle all cases
case QTextCharFormat::AlignMiddle:
pos = QString::fromLatin1("middle"); break;
case QTextCharFormat::AlignTop:
@ -736,8 +920,6 @@ void QTextOdfWriter::writeTableCellFormat(QXmlStreamWriter &writer, QTextTableCe
// ODF just search for style-table-cell-properties-attlist)
// QTextFormat::BackgroundImageUrl
// format.background
// QTextFormat::FrameBorder
writer.writeEndElement(); // style
}
@ -815,8 +997,28 @@ bool QTextOdfWriter::writeAll()
const QList<int> copy = formats.toList();
for (auto index : copy) {
QTextObject *object = m_document->objectForFormat(allFormats[index]);
if (object)
if (object) {
formats << object->formatIndex();
if (auto *tableobject = qobject_cast<QTextTable *>(object)) {
if (tableobject->format().borderStyle()) {
int tableID = tableobject->formatIndex();
m_tableFormatsWithBorders.insert(tableID);
// loop through all rows and cols of table and store cell IDs,
// create Hash with cell ID as Key and table IDs as Vector
for (int rowindex = 0; rowindex < tableobject->rows(); ++rowindex) {
for (int colindex = 0; colindex < tableobject->columns(); ++colindex) {
const int cellFormatID = tableobject->cellAt(rowindex, colindex).tableCellFormatIndex();
QVector<int> tableIdsTmp;
if (m_cellFormatsInTablesWithBorders.contains(cellFormatID))
tableIdsTmp = m_cellFormatsInTablesWithBorders.value(cellFormatID);
if (!tableIdsTmp.contains(tableID))
tableIdsTmp.append(tableID);
m_cellFormatsInTablesWithBorders.insert(cellFormatID, tableIdsTmp);
}
}
}
}
}
}
writeFormats(writer, formats);

View File

@ -56,8 +56,10 @@
//
#include <QtCore/QXmlStreamWriter>
#include <QtCore/qhash.h>
#include <QtCore/qset.h>
#include <QtCore/qstack.h>
#include <QtCore/qvector.h>
#include "qtextdocument_p.h"
#include "qtextdocumentwriter.h"
@ -94,11 +96,21 @@ public:
void writeCharacterFormat(QXmlStreamWriter &writer, QTextCharFormat format, int formatIndex) const;
void writeListFormat(QXmlStreamWriter &writer, QTextListFormat format, int formatIndex) const;
void writeFrameFormat(QXmlStreamWriter &writer, QTextFrameFormat format, int formatIndex) const;
void writeTableCellFormat(QXmlStreamWriter &writer, QTextTableCellFormat format, int formatIndex) const;
void writeTableFormat(QXmlStreamWriter &writer, QTextTableFormat format, int formatIndex) const;
void writeTableCellFormat(QXmlStreamWriter &writer, QTextTableCellFormat format, int formatIndex,
QVector<QTextFormat> &styles) const;
void writeFrame(QXmlStreamWriter &writer, const QTextFrame *frame);
void writeInlineCharacter(QXmlStreamWriter &writer, const QTextFragment &fragment) const;
const QString officeNS, textNS, styleNS, foNS, tableNS, drawNS, xlinkNS, svgNS;
const int defaultImageResolution = 11811; // 11811 dots per meter = (about) 300 dpi
protected:
void tableCellStyleElement(QXmlStreamWriter &writer, const int &formatIndex,
const QTextTableCellFormat &format,
bool hasBorder, int tableId = 0,
const QTextTableFormat tableFormatTmp = QTextTableFormat()) const;
private:
const QTextDocument *m_document;
QIODevice *m_device;
@ -108,6 +120,10 @@ private:
bool m_createArchive;
QStack<QTextList *> m_listStack;
QHash<int, QVector<int>> m_cellFormatsInTablesWithBorders;
QSet<int> m_tableFormatsWithBorders;
mutable QSet<int> m_tableFormatsWithColWidthConstraints;
};
QT_END_NAMESPACE

View File

@ -131,7 +131,7 @@ void tst_QTextOdfWriter::testWriteParagraph_data()
QTest::newRow("misc2") << "\t \tFoo" <<
"<text:p text:style-name=\"p1\"><text:span text:style-name=\"c0\"><text:tab/> <text:s text:c=\"4\"/><text:tab/>Foo</text:span></text:p>";
QTest::newRow("linefeed") << (QStringLiteral("line1") + QChar(0x2028) + QStringLiteral("line2")) <<
"<text:p text:style-name=\"p1\"><text:span text:style-name=\"c0\">line1<text:line-break/>line2</text:span></text:p>";
"<text:p text:style-name=\"p1\"><text:span text:style-name=\"c0\">line1<text:tab/><text:line-break/>line2</text:span></text:p>";
QTest::newRow("spaces") << "The quick brown fox jumped over the lazy dog" <<
"<text:p text:style-name=\"p1\"><text:span text:style-name=\"c0\">The quick brown fox jumped over the lazy dog</text:span></text:p>";
}
@ -378,7 +378,7 @@ void tst_QTextOdfWriter::testWriteTable()
odfWriter->writeFrame(*xmlWriter, document->rootFrame());
QString xml = QString::fromLatin1(
"<text:p text:style-name=\"p1\"/>"
"<table:table>"
"<table:table table:style-name=\"Table2\">"
"<table:table-column table:number-columns-repeated=\"3\"/>"
"<table:table-row>"
"<table:table-cell table:style-name=\"T3\">"