File indexing completed on 2025-07-13 10:51:46
0001 /* This file is part of the KDE project 0002 SPDX-FileCopyrightText: 2002 Laurent Montel <lmontel@mandrakesoft.com> 0003 SPDX-FileCopyrightText: 2003 Lukas Tinkl <lukas@kde.org> 0004 SPDX-FileCopyrightText: 2003 David Faure <faure@kde.org> 0005 0006 SPDX-License-Identifier: LGPL-2.0-or-later 0007 */ 0008 0009 #include "ooutils.h" 0010 #include <KoStyleStack.h> 0011 #include <KoStore.h> 0012 #include <KoXmlReader.h> 0013 #include <QDomDocument> 0014 #include <QColor> 0015 #include <QImage> 0016 #include <KoUnit.h> 0017 #include <QRegExp> 0018 #include <QDebug> 0019 #include <kzip.h> 0020 0021 const char ooNS::office[] = "http://openoffice.org/2000/office"; 0022 const char ooNS::style[] = "http://openoffice.org/2000/style"; 0023 const char ooNS::text[] = "http://openoffice.org/2000/text"; 0024 const char ooNS::table[] = "http://openoffice.org/2000/table"; 0025 const char ooNS::draw[] = "http://openoffice.org/2000/drawing"; 0026 const char ooNS::presentation[] = "http://openoffice.org/2000/presentation"; 0027 const char ooNS::fo[] = "http://www.w3.org/1999/XSL/Format"; 0028 const char ooNS::xlink[] = "http://www.w3.org/1999/xlink"; 0029 const char ooNS::number[] = "http://openoffice.org/2000/datastyle"; 0030 const char ooNS::svg[] = "http://www.w3.org/2000/svg"; 0031 const char ooNS::dc[] = "http://purl.org/dc/elements/1.1/"; 0032 const char ooNS::meta[] = "http://openoffice.org/2000/meta"; 0033 const char ooNS::config[] = "http://openoffice.org/2001/config"; 0034 0035 QString OoUtils::expandWhitespace(const KoXmlElement& tag) 0036 { 0037 //tags like <text:s text:c="4"> 0038 0039 int howmany = 1; 0040 if (tag.hasAttributeNS(ooNS::text, "c")) 0041 howmany = tag.attributeNS(ooNS::text, "c", QString()).toInt(); 0042 0043 QString result; 0044 return result.fill(32, howmany); 0045 } 0046 0047 bool OoUtils::parseBorder(const QString & tag, double * width, int * style, QColor * color) 0048 { 0049 //string like "0.088cm solid #800000" 0050 0051 if (tag.isEmpty() || tag == "none" || tag == "hidden") // in fact no border 0052 return false; 0053 0054 QString _width = tag.section(' ', 0, 0); 0055 QString _style = tag.section(' ', 1, 1); 0056 QString _color = tag.section(' ', 2, 2); 0057 0058 *width = KoUnit::parseValue(_width, 1.0); 0059 0060 if (_style == "dashed") 0061 *style = 1; 0062 else if (_style == "dotted") 0063 *style = 2; 0064 else if (_style == "dot-dash") // not in xsl/fo, but in OASIS (in other places) 0065 *style = 3; 0066 else if (_style == "dot-dot-dash") // not in xsl/fo, but in OASIS (in other places) 0067 *style = 4; 0068 else if (_style == "double") 0069 *style = 5; 0070 else 0071 *style = 0; 0072 0073 if (_color.isEmpty()) 0074 *color = QColor(); 0075 else 0076 color->setNamedColor(_color); 0077 0078 return true; 0079 } 0080 0081 void OoUtils::importIndents(QDomElement& parentElement, const KoStyleStack& styleStack) 0082 { 0083 if (styleStack.hasProperty(ooNS::fo, "margin-left") || // 3.11.19 0084 styleStack.hasProperty(ooNS::fo, "margin-right")) 0085 // *text-indent must always be bound to either margin-left or margin-right 0086 { 0087 double marginLeft = KoUnit::parseValue(styleStack.property(ooNS::fo, "margin-left")); 0088 double marginRight = KoUnit::parseValue(styleStack.property(ooNS::fo, "margin-right")); 0089 double first = 0; 0090 if (styleStack.property(ooNS::style, "auto-text-indent") == "true") // style:auto-text-indent takes precedence 0091 // ### "indented by a value that is based on the current font size" 0092 // ### and "requires margin-left and margin-right 0093 // ### but how much is the indent? 0094 first = 10; 0095 else if (styleStack.hasProperty(ooNS::fo, "text-indent")) 0096 first = KoUnit::parseValue(styleStack.property(ooNS::fo, "text-indent")); 0097 0098 if (marginLeft != 0 || marginRight != 0 || first != 0) { 0099 QDomElement indent = parentElement.ownerDocument().createElement("INDENTS"); 0100 if (marginLeft != 0) 0101 indent.setAttribute("left", QString::number(marginLeft)); 0102 if (marginRight != 0) 0103 indent.setAttribute("right", QString::number(marginRight)); 0104 if (first != 0) 0105 indent.setAttribute("first", QString::number(first)); 0106 parentElement.appendChild(indent); 0107 } 0108 } 0109 } 0110 0111 void OoUtils::importLineSpacing(QDomElement& parentElement, const KoStyleStack& styleStack) 0112 { 0113 if (styleStack.hasProperty(ooNS::fo, "line-height")) { 0114 // Fixed line height 0115 QString value = styleStack.property(ooNS::fo, "line-height"); // 3.11.1 0116 if (value != "normal") { 0117 QDomElement lineSpacing = parentElement.ownerDocument().createElement("LINESPACING"); 0118 if (value == "100%") 0119 lineSpacing.setAttribute("type", "single"); 0120 else if (value == "150%") 0121 lineSpacing.setAttribute("type", "oneandhalf"); 0122 else if (value == "200%") 0123 lineSpacing.setAttribute("type", "double"); 0124 else if (value.contains('%')) { 0125 double percent = value.toDouble(); 0126 lineSpacing.setAttribute("type", "multiple"); 0127 lineSpacing.setAttribute("spacingvalue", QString::number(percent / 100)); 0128 } else { // fixed value (use KoUnit::parseValue to get it in pt) 0129 qWarning() << "Unhandled value for fo:line-height: " << value; 0130 } 0131 parentElement.appendChild(lineSpacing); 0132 } 0133 } 0134 // Line-height-at-least is mutually exclusive with line-height 0135 else if (styleStack.hasProperty(ooNS::style, "line-height-at-least")) { // 3.11.2 0136 QString value = styleStack.property(ooNS::style, "line-height-at-least"); 0137 // kotext has "at least" but that's for the linespacing, not for the entire line height! 0138 // Strange. kotext also has "at least" for the whole line height.... 0139 // Did we make the wrong choice in kotext? 0140 //qWarning() << "Unimplemented support for style:line-height-at-least: " << value; 0141 // Well let's see if this makes a big difference. 0142 QDomElement lineSpacing = parentElement.ownerDocument().createElement("LINESPACING"); 0143 lineSpacing.setAttribute("type", "atleast"); 0144 lineSpacing.setAttribute("spacingvalue", QString::number(KoUnit::parseValue(value))); 0145 parentElement.appendChild(lineSpacing); 0146 } 0147 // Line-spacing is mutually exclusive with line-height and line-height-at-least 0148 else if (styleStack.hasProperty(ooNS::style, "line-spacing")) { // 3.11.3 0149 double value = KoUnit::parseValue(styleStack.property(ooNS::style, "line-spacing")); 0150 if (value != 0.0) { 0151 QDomElement lineSpacing = parentElement.ownerDocument().createElement("LINESPACING"); 0152 lineSpacing.setAttribute("type", "custom"); 0153 lineSpacing.setAttribute("spacingvalue", QString::number(value)); 0154 parentElement.appendChild(lineSpacing); 0155 } 0156 } 0157 0158 } 0159 0160 void OoUtils::importTopBottomMargin(QDomElement& parentElement, const KoStyleStack& styleStack) 0161 { 0162 if (styleStack.hasProperty(ooNS::fo, "margin-top") || // 3.11.22 0163 styleStack.hasProperty(ooNS::fo, "margin-bottom")) { 0164 double mtop = KoUnit::parseValue(styleStack.property(ooNS::fo, "margin-top")); 0165 double mbottom = KoUnit::parseValue(styleStack.property(ooNS::fo, "margin-bottom")); 0166 if (mtop != 0 || mbottom != 0) { 0167 QDomElement offset = parentElement.ownerDocument().createElement("OFFSETS"); 0168 if (mtop != 0) 0169 offset.setAttribute("before", QString::number(mtop)); 0170 if (mbottom != 0) 0171 offset.setAttribute("after", QString::number(mbottom)); 0172 parentElement.appendChild(offset); 0173 } 0174 } 0175 } 0176 0177 void OoUtils::importTabulators(QDomElement& parentElement, const KoStyleStack& styleStack) 0178 { 0179 if (!styleStack.hasChildNode(ooNS::style, "tab-stops")) // 3.11.10 0180 return; 0181 KoXmlElement tabStops = styleStack.childNode(ooNS::style, "tab-stops"); 0182 //qDebug() << tabStops.childNodes().count() <<" tab stops in layout."; 0183 for (KoXmlNode it = tabStops.firstChild(); !it.isNull(); it = it.nextSibling()) { 0184 KoXmlElement tabStop = it.toElement(); 0185 Q_ASSERT(tabStop.prefix() == "style"); 0186 Q_ASSERT(tabStop.tagName() == "tab-stop"); 0187 QString type = tabStop.attributeNS(ooNS::style, "type", QString()); // left, right, center or char 0188 0189 QDomElement elem = parentElement.ownerDocument().createElement("TABULATOR"); 0190 int calligraType = 0; 0191 if (type == "left") 0192 calligraType = 0; 0193 else if (type == "center") 0194 calligraType = 1; 0195 else if (type == "right") 0196 calligraType = 2; 0197 else if (type == "char") { 0198 QString delimiterChar = tabStop.attributeNS(ooNS::style, "char", QString()); // single character 0199 elem.setAttribute("alignchar", delimiterChar); 0200 calligraType = 3; // "alignment on decimal point" 0201 } 0202 0203 elem.setAttribute("type", calligraType); 0204 0205 double pos = KoUnit::parseValue(tabStop.attributeNS(ooNS::style, "position", QString())); 0206 elem.setAttribute("ptpos", QString::number(pos)); 0207 0208 // TODO Convert leaderChar's unicode value to the Calligra enum 0209 // (blank/dots/line/dash/dash-dot/dash-dot-dot, 0 to 5) 0210 QString leaderChar = tabStop.attributeNS(ooNS::style, "leader-char", QString()); // single character 0211 if (!leaderChar.isEmpty()) { 0212 int filling = 0; 0213 QChar ch = leaderChar[0]; 0214 switch (ch.toLatin1()) { 0215 case '.': 0216 filling = 1; break; 0217 case '-': 0218 case '_': // TODO in Words: differentiate --- and ___ 0219 filling = 2; break; 0220 default: 0221 // Words doesn't have support for "any char" as filling. 0222 // Instead it has dash-dot and dash-dot-dot - but who uses that in a tabstop? 0223 break; 0224 } 0225 elem.setAttribute("filling", filling); 0226 } 0227 parentElement.appendChild(elem); 0228 } 0229 0230 } 0231 0232 void OoUtils::importBorders(QDomElement& parentElement, const KoStyleStack& styleStack) 0233 { 0234 if (styleStack.hasProperty(ooNS::fo, "border", "left")) { 0235 double width; 0236 int style; 0237 QColor color; 0238 if (OoUtils::parseBorder(styleStack.property(ooNS::fo, "border", "left"), &width, &style, &color)) { 0239 QDomElement lbElem = parentElement.ownerDocument().createElement("LEFTBORDER"); 0240 lbElem.setAttribute("width", QString::number(width)); 0241 lbElem.setAttribute("style", QString::number(style)); 0242 if (color.isValid()) { 0243 lbElem.setAttribute("red", color.red()); 0244 lbElem.setAttribute("green", color.green()); 0245 lbElem.setAttribute("blue", color.blue()); 0246 } 0247 parentElement.appendChild(lbElem); 0248 } 0249 } 0250 0251 if (styleStack.hasProperty(ooNS::fo, "border", "right")) { 0252 double width; 0253 int style; 0254 QColor color; 0255 if (OoUtils::parseBorder(styleStack.property(ooNS::fo, "border", "right"), &width, &style, &color)) { 0256 QDomElement lbElem = parentElement.ownerDocument().createElement("RIGHTBORDER"); 0257 lbElem.setAttribute("width", QString::number(width)); 0258 lbElem.setAttribute("style", QString::number(style)); 0259 if (color.isValid()) { 0260 lbElem.setAttribute("red", color.red()); 0261 lbElem.setAttribute("green", color.green()); 0262 lbElem.setAttribute("blue", color.blue()); 0263 } 0264 parentElement.appendChild(lbElem); 0265 } 0266 } 0267 0268 if (styleStack.hasProperty(ooNS::fo, "border", "top")) { 0269 double width; 0270 int style; 0271 QColor color; 0272 if (OoUtils::parseBorder(styleStack.property(ooNS::fo, "border", "top"), &width, &style, &color)) { 0273 QDomElement lbElem = parentElement.ownerDocument().createElement("TOPBORDER"); 0274 lbElem.setAttribute("width", QString::number(width)); 0275 lbElem.setAttribute("style", QString::number(style)); 0276 if (color.isValid()) { 0277 lbElem.setAttribute("red", color.red()); 0278 lbElem.setAttribute("green", color.green()); 0279 lbElem.setAttribute("blue", color.blue()); 0280 } 0281 parentElement.appendChild(lbElem); 0282 } 0283 } 0284 0285 if (styleStack.hasProperty(ooNS::fo, "border", "bottom")) { 0286 double width; 0287 int style; 0288 QColor color; 0289 if (OoUtils::parseBorder(styleStack.property(ooNS::fo, "border", "bottom"), &width, &style, &color)) { 0290 QDomElement lbElem = parentElement.ownerDocument().createElement("BOTTOMBORDER"); 0291 lbElem.setAttribute("width", QString::number(width)); 0292 lbElem.setAttribute("style", QString::number(style)); 0293 if (color.isValid()) { 0294 lbElem.setAttribute("red", color.red()); 0295 lbElem.setAttribute("green", color.green()); 0296 lbElem.setAttribute("blue", color.blue()); 0297 } 0298 parentElement.appendChild(lbElem); 0299 } 0300 } 0301 } 0302 0303 void OoUtils::importUnderline(const QString& in, QString& underline, QString& styleline) 0304 { 0305 underline = "single"; 0306 if (in == "none") 0307 underline = "0"; 0308 else if (in == "single") 0309 styleline = "solid"; 0310 else if (in == "double") { 0311 underline = in; 0312 styleline = "solid"; 0313 } else if (in == "dotted" || in == "bold-dotted") // bold-dotted not in libkotext 0314 styleline = "dot"; 0315 else if (in == "dash" 0316 // those are not in libkotext: 0317 || in == "long-dash" 0318 || in == "bold-dash" 0319 || in == "bold-long-dash" 0320 ) 0321 styleline = "dash"; 0322 else if (in == "dot-dash" 0323 || in == "bold-dot-dash") // not in libkotext 0324 styleline = "dashdot"; // tricky ;) 0325 else if (in == "dot-dot-dash" 0326 || in == "bold-dot-dot-dash") // not in libkotext 0327 styleline = "dashdotdot"; // this is getting fun... 0328 else if (in == "wave" 0329 || in == "bold-wave" // not in libkotext 0330 || in == "double-wave" // not in libkotext 0331 || in == "small-wave") { // not in libkotext 0332 underline = in; 0333 styleline = "solid"; 0334 } else if (in == "bold") { 0335 underline = "single-bold"; 0336 styleline = "solid"; 0337 } else 0338 qWarning() << " unsupported text-underline value: " << in; 0339 } 0340 0341 void OoUtils::importTextPosition(const QString& text_position, QString& value, QString& relativetextsize) 0342 { 0343 //OO: <vertical position (% or sub or super)> [<size as %>] 0344 //Examples: "super" or "super 58%" or "82% 58%" (where 82% is the vertical position) 0345 // TODO in words: vertical positions other than sub/super 0346 QStringList lst = text_position.split(' '); 0347 if (!lst.isEmpty()) { 0348 QString textPos = lst.front().trimmed(); 0349 QString textSize; 0350 lst.pop_front(); 0351 if (!lst.isEmpty()) 0352 textSize = lst.front().trimmed(); 0353 if (!lst.isEmpty()) 0354 qWarning() << "Strange text position: " << text_position; 0355 bool super = textPos == "super"; 0356 bool sub = textPos == "sub"; 0357 if (textPos.endsWith(QLatin1Char('%'))) { 0358 textPos.chop(1); 0359 // This is where we interpret the text position into kotext's simpler 0360 // "super" or "sub". 0361 double val = textPos.toDouble(); 0362 if (val > 0) 0363 super = true; 0364 else if (val < 0) 0365 sub = true; 0366 } 0367 if (super) 0368 value = "2"; 0369 else if (sub) 0370 value = "1"; 0371 else 0372 value = "0"; 0373 if (!textSize.isEmpty() && textSize.endsWith(QLatin1Char('%'))) { 0374 textSize.chop(1); 0375 double textSizeValue = textSize.toDouble() / 100; // e.g. 0.58 0376 relativetextsize = QString::number(textSizeValue); 0377 } 0378 } else 0379 value = "0"; 0380 } 0381 0382 void OoUtils::createDocumentInfo(KoXmlDocument &_meta, QDomDocument & docinfo) 0383 { 0384 KoXmlNode meta = KoXml::namedItemNS(_meta, ooNS::office, "document-meta"); 0385 KoXmlNode office = KoXml::namedItemNS(meta, ooNS::office, "meta"); 0386 0387 if (office.isNull()) 0388 return; 0389 QDomElement elementDocInfo = docinfo.documentElement(); 0390 0391 KoXmlElement e = KoXml::namedItemNS(office, ooNS::dc, "creator"); 0392 if (!e.isNull() && !e.text().isEmpty()) { 0393 QDomElement author = docinfo.createElement("author"); 0394 QDomElement t = docinfo.createElement("full-name"); 0395 author.appendChild(t); 0396 t.appendChild(docinfo.createTextNode(e.text())); 0397 elementDocInfo.appendChild(author); 0398 } 0399 0400 e = KoXml::namedItemNS(office, ooNS::dc, "title"); 0401 if (!e.isNull() && !e.text().isEmpty()) { 0402 QDomElement about = docinfo.createElement("about"); 0403 QDomElement title = docinfo.createElement("title"); 0404 about.appendChild(title); 0405 title.appendChild(docinfo.createTextNode(e.text())); 0406 elementDocInfo.appendChild(about); 0407 } 0408 0409 e = KoXml::namedItemNS(office, ooNS::dc, "description"); 0410 if (!e.isNull() && !e.text().isEmpty()) { 0411 QDomElement about = elementDocInfo.namedItem("about").toElement(); 0412 if (about.isNull()) { 0413 about = docinfo.createElement("about"); 0414 elementDocInfo.appendChild(about); 0415 } 0416 QDomElement title = docinfo.createElement("abstract"); 0417 about.appendChild(title); 0418 title.appendChild(docinfo.createTextNode(e.text())); 0419 } 0420 e = KoXml::namedItemNS(office, ooNS::dc, "subject"); 0421 if (!e.isNull() && !e.text().isEmpty()) { 0422 QDomElement about = elementDocInfo.namedItem("about").toElement(); 0423 if (about.isNull()) { 0424 about = docinfo.createElement("about"); 0425 elementDocInfo.appendChild(about); 0426 } 0427 QDomElement subject = docinfo.createElement("subject"); 0428 about.appendChild(subject); 0429 subject.appendChild(docinfo.createTextNode(e.text())); 0430 } 0431 e = KoXml::namedItemNS(office, ooNS::meta, "keywords"); 0432 if (!e.isNull()) { 0433 QDomElement about = elementDocInfo.namedItem("about").toElement(); 0434 if (about.isNull()) { 0435 about = docinfo.createElement("about"); 0436 elementDocInfo.appendChild(about); 0437 } 0438 KoXmlElement tmp = KoXml::namedItemNS(e, ooNS::meta, "keyword"); 0439 if (!tmp.isNull() && !tmp.text().isEmpty()) { 0440 QDomElement keyword = docinfo.createElement("keyword"); 0441 about.appendChild(keyword); 0442 keyword.appendChild(docinfo.createTextNode(tmp.text())); 0443 } 0444 } 0445 } 0446 0447 KoFilter::ConversionStatus OoUtils::loadAndParse(const QString& fileName, KoXmlDocument& doc, KoStore* store) 0448 { 0449 qDebug() << "loadAndParse: Trying to open" << fileName; 0450 0451 if (!store->open(fileName)) { 0452 qWarning() << "Entry " << fileName << " not found!"; 0453 return KoFilter::FileNotFound; 0454 } 0455 KoFilter::ConversionStatus convertStatus = loadAndParse(store->device(), doc, fileName); 0456 store->close(); 0457 return convertStatus; 0458 0459 } 0460 0461 KoFilter::ConversionStatus OoUtils::loadAndParse(QIODevice* io, KoXmlDocument& doc, const QString & fileName) 0462 { 0463 // Error variables for QDomDocument::setContent 0464 QString errorMsg; 0465 int errorLine, errorColumn; 0466 if (!doc.setContent(io, &errorMsg, &errorLine, &errorColumn)) { 0467 qWarning() << "Parsing error in " << fileName << "! Aborting!" << endl 0468 << " In line: " << errorLine << ", column: " << errorColumn << endl 0469 << " Error message: " << errorMsg << endl; 0470 return KoFilter::ParsingError; 0471 } 0472 0473 qDebug() << "File" << fileName << " loaded and parsed!"; 0474 0475 return KoFilter::OK; 0476 } 0477 0478 KoFilter::ConversionStatus OoUtils::loadAndParse(const QString& filename, KoXmlDocument& doc, KZip* zip) 0479 { 0480 qDebug() << "Trying to open" << filename; 0481 0482 if (!zip) { 0483 qWarning() << "No ZIP file!" << endl; 0484 return KoFilter::CreationError; // Should not happen 0485 } 0486 0487 const KArchiveEntry* entry = zip->directory()->entry(filename); 0488 if (!entry) { 0489 qWarning() << "Entry " << filename << " not found!"; 0490 return KoFilter::FileNotFound; 0491 } 0492 if (entry->isDirectory()) { 0493 qWarning() << "Entry " << filename << " is a directory!"; 0494 return KoFilter::WrongFormat; 0495 } 0496 const KZipFileEntry* f = static_cast<const KZipFileEntry *>(entry); 0497 qDebug() << "Entry" << filename << " has size" << f->size(); 0498 QIODevice* io = f->createDevice(); 0499 KoFilter::ConversionStatus convertStatus = loadAndParse(io, doc, filename); 0500 delete io; 0501 return convertStatus; 0502 } 0503 0504 KoFilter::ConversionStatus OoUtils::loadThumbnail(QImage& thumbnail, KZip* zip) 0505 { 0506 const QString filename("Thumbnails/thumbnail.png"); 0507 qDebug() << "Trying to open thumbnail" << filename; 0508 0509 if (!zip) { 0510 qWarning() << "No ZIP file!" << endl; 0511 return KoFilter::CreationError; // Should not happen 0512 } 0513 0514 const KArchiveEntry* entry = zip->directory()->entry(filename); 0515 if (!entry) { 0516 qWarning() << "Entry " << filename << " not found!"; 0517 return KoFilter::FileNotFound; 0518 } 0519 if (entry->isDirectory()) { 0520 qWarning() << "Entry " << filename << " is a directory!"; 0521 return KoFilter::WrongFormat; 0522 } 0523 const KZipFileEntry* f = static_cast<const KZipFileEntry *>(entry); 0524 QIODevice* io = f->createDevice(); 0525 qDebug() << "Entry" << filename << " has size" << f->size(); 0526 0527 if (! io->open(QIODevice::ReadOnly)) { 0528 qWarning() << "Thumbnail could not be opened!"; 0529 delete io; 0530 return KoFilter::StupidError; 0531 } 0532 0533 if (! thumbnail.load(io, "PNG")) { 0534 qWarning() << "Thumbnail could not be read!"; 0535 delete io; 0536 return KoFilter::StupidError; 0537 } 0538 0539 io->close(); 0540 0541 if (thumbnail.isNull()) { 0542 qWarning() << "Read thumbnail is null!"; 0543 delete io; 0544 return KoFilter::StupidError; 0545 } 0546 0547 delete io; 0548 0549 qDebug() << "File" << filename << " loaded!"; 0550 0551 return KoFilter::OK; 0552 }