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 }