1 /* This file is part of the KDE project
2 
3    Copyright 2018 Dag Andersen <danders@get2net.dk>
4 
5    This library is free software; you can redistribute it and/or
6    modify it under the terms of the GNU Library General Public
7    License as published by the Free Software Foundation; either
8    version 2 of the License, or (at your option) any later version.
9 
10    This library is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13    Library General Public License for more details.
14 
15    You should have received a copy of the GNU Library General Public License
16    along with this library; see the file COPYING.LIB.  If not, write to
17    the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18    Boston, MA 02110-1301, USA.
19 */
20 
21 
22 #include "OdfHelper.h"
23 
24 #include "ChartDebug.h"
25 
26 // Posix
27 #include <float.h> // For basic data types characteristics.
28 
29 // Qt
30 #include <QPointF>
31 #include <QPainter>
32 #include <QSizeF>
33 #include <QTextDocument>
34 #include <QStandardItemModel>
35 #include <QUrl>
36 
37 // Calligra
38 #include <KoShapeLoadingContext.h>
39 #include <KoOdfLoadingContext.h>
40 #include <KoEmbeddedDocumentSaver.h>
41 #include <KoStore.h>
42 #include <KoDocument.h>
43 #include <KoShapeSavingContext.h>
44 #include <KoViewConverter.h>
45 #include <KoXmlReader.h>
46 #include <KoXmlWriter.h>
47 #include <KoXmlNS.h>
48 #include <KoGenStyles.h>
49 #include <KoStyleStack.h>
50 #include <KoShapeRegistry.h>
51 #include <KoTextShapeData.h>
52 #include <KoTextDocumentLayout.h>
53 #include <KoCanvasBase.h>
54 #include <KoShapeManager.h>
55 #include <KoSelection.h>
56 #include <KoShapeBackground.h>
57 #include <KoInsets.h>
58 #include <KoShapeStrokeModel.h>
59 #include <KoColorBackground.h>
60 #include <KoShapeStroke.h>
61 #include <KoOdfWorkaround.h>
62 #include <KoTextDocument.h>
63 #include <KoUnit.h>
64 #include <KoShapePaintingContext.h>
65 #include <KoTextShapeDataBase.h>
66 #include <KoShapeShadow.h>
67 #include <KoBorder.h>
68 #include <KoHatchBackground.h>
69 #include <KoOdfGradientBackground.h>
70 #include <KoPatternBackground.h>
71 #include <KoGradientBackground.h>
72 #include <KoOdfGraphicStyles.h>
73 #include "kochart_global.h"
74 
75 namespace KoChart {
76 namespace OdfHelper {
77 
78 
79 // HACK: To get correct position also for rotated titles
itemPosition(const KoShape * shape)80 QPointF itemPosition(const KoShape *shape)
81 {
82     return QPointF(shape->transformation().dx(), shape->transformation().dy());
83 }
84 
85 //fo:font-weight attribute are normal, bold, 100, 200, 300, 400, 500, 600, 700, 800 or 900.
fromOdfFontWeight(const QString & odfweight)86 int fromOdfFontWeight(const QString &odfweight) {
87     if (odfweight.isEmpty() || odfweight == "normal") {
88         return QFont::Normal;
89     }
90     if (odfweight == "bold") {
91         return QFont::Bold;
92     }
93     bool ok;
94     int weight = odfweight.toInt(&ok);
95     if (!ok) {
96         return 50;
97     }
98     switch (weight) {
99         case 100: weight = 1; break;
100         case 200: weight = 17; break;
101         case 300: weight = 33; break;
102         case 400: weight = 50; break;
103         case 500: weight = 58; break;
104         case 600: weight = 66; break;
105         case 700: weight = 75; break;
106         case 800: weight = 87; break;
107         case 900: weight = 99; break;
108         default: weight = 50; break;
109     }
110     return weight;
111 }
112 
toOdfFontWeight(int weight)113 QString toOdfFontWeight(int weight) {
114     QString w;
115     if (weight < 8) {
116         w = "100";
117     } else if (weight < 25) {
118         w = "200";
119     } else if (weight < 41) {
120         w = "300";
121     } else if (weight < 54) {
122         w = "normal";
123     } else if (weight < 62) {
124         w = "500";
125     } else if (weight < 70) {
126         w = "600";
127     } else if (weight < 81) {
128         w = "bold";
129     } else if (weight < 92) {
130         w = "800";
131     } else {
132         w = "900";
133     }
134     return w;
135 }
136 
saveOdfFont(KoGenStyle & style,const QFont & font,const QColor & color)137 void saveOdfFont(KoGenStyle &style, const QFont& font, const QColor& color)
138 {
139     style.addProperty("fo:font-family", font.family(), KoGenStyle::TextType);
140     style.addPropertyPt("fo:font-size", font.pointSize(), KoGenStyle::TextType);
141     style.addProperty("fo:color", color.isValid() ? color.name() : "#000000", KoGenStyle::TextType);
142     style.addProperty("fo:font-weight", toOdfFontWeight(font.weight()), KoGenStyle::TextType);
143     style.addProperty("fo:font-style", font.italic() ? "italic" : "normal", KoGenStyle::TextType);
144 }
145 
saveOdfFont(KoGenStyles & mainStyles,const QFont & font,const QColor & color)146 QString saveOdfFont(KoGenStyles& mainStyles,
147                     const QFont& font,
148                     const QColor& color)
149 {
150     KoGenStyle autoStyle(KoGenStyle::ParagraphAutoStyle, "chart", 0);
151     saveOdfFont(autoStyle, font, color);
152     return mainStyles.insert(autoStyle, "ch");
153 }
154 
saveOdfTitleStyle(KoShape * title,KoGenStyle & style,KoShapeSavingContext & context)155 void saveOdfTitleStyle(KoShape *title, KoGenStyle &style, KoShapeSavingContext &context)
156 {
157     TextLabelData *titleData = qobject_cast<TextLabelData*>(title->userData());
158     Q_ASSERT(titleData);
159     QTextCursor cursor(titleData->document());
160     QFont titleFont = cursor.charFormat().font();
161     QColor color = cursor.charFormat().foreground().color();
162 
163     saveOdfFont(style, titleFont, color);
164 
165     // title->saveStyle(style, context) is protected,
166     // so we duplicate some code:
167     KoShapeStrokeModel *sm = title->stroke();
168     if (sm) {
169         sm->fillStyle(style, context);
170     } else {
171         style.addProperty("draw:stroke", "none", KoGenStyle::GraphicType);
172     }
173     KoShapeShadow *s = title->shadow();
174     if (s) {
175         s->fillStyle(style, context);
176     }
177     QSharedPointer<KoShapeBackground> bg = title->background();
178     if (bg) {
179         bg->fillStyle(style, context);
180     } else {
181         style.addProperty("draw:fill", "none", KoGenStyle::GraphicType);
182     }
183 
184     KoBorder *b = title->border();
185     if (b) {
186         b->saveOdf(style);
187     }
188 
189     QMap<QByteArray, QString>::const_iterator it(title->additionalStyleAttributes().constBegin());
190     for (; it != title->additionalStyleAttributes().constEnd(); ++it) {
191         style.addProperty(it.key(), it.value(), KoGenStyle::ChartType);
192     }
193     style.addProperty("chart:auto-size", (titleData->resizeMethod() == KoTextShapeDataBase::AutoResize), KoGenStyle::ChartType);
194 }
195 
saveOdfTitle(KoShape * title,KoXmlWriter & bodyWriter,const char * titleType,KoShapeSavingContext & context)196 void saveOdfTitle(KoShape *title, KoXmlWriter &bodyWriter, const char *titleType, KoShapeSavingContext &context)
197 {
198     // Don't save hidden titles, as that's the way of removing them
199     // from a chart.
200     if (!title->isVisible())
201         return;
202 
203     TextLabelData *titleData = qobject_cast<TextLabelData*>(title->userData());
204     if (!titleData)
205         return;
206 
207     bodyWriter.startElement(titleType);
208 
209     KoGenStyle autoStyle(KoGenStyle::ChartAutoStyle, "chart", 0);
210     autoStyle.addPropertyPt("style:rotation-angle", 360 - title->rotation());
211     saveOdfTitleStyle(title, autoStyle, context);
212 
213     // always save position and size and let consumer decide if they are used
214     QPointF position = itemPosition(title);
215     bodyWriter.addAttributePt("svg:x", position.x());
216     bodyWriter.addAttributePt("svg:y", position.y());
217 
218     const QSizeF size = title->size();
219     bodyWriter.addAttributePt("svg:width", size.width());
220     bodyWriter.addAttributePt("svg:height", size.height());
221 
222     bodyWriter.addAttribute("chart:style-name", context.mainStyles().insert(autoStyle, "ch"));
223 
224     // lo (and odf?) does not support formatted text :(
225     bodyWriter.startElement("text:p");
226     bodyWriter.addTextNode(titleData->document()->toPlainText());
227     bodyWriter.endElement(); // text:p
228 
229     // save calligra specific formatted text
230     bodyWriter.startElement("calligra:text");
231     titleData->saveOdf(context);
232     bodyWriter.endElement(); // calligra:text
233 
234     bodyWriter.endElement(); // chart:title/subtitle/footer
235 }
236 
getStyleProperty(const char * property,KoShapeLoadingContext & context)237 QString getStyleProperty(const char *property, KoShapeLoadingContext &context)
238 {
239     KoStyleStack &styleStack = context.odfLoadingContext().styleStack();
240     QString value;
241 
242     if (styleStack.hasProperty(KoXmlNS::draw, property)) {
243         value = styleStack.property(KoXmlNS::draw, property);
244     }
245 
246     return value;
247 }
248 
loadOdfFill(KoShape * title,KoShapeLoadingContext & context)249 QSharedPointer<KoShapeBackground> loadOdfFill(KoShape *title, KoShapeLoadingContext &context)
250 {
251     QString fill = getStyleProperty("fill", context);
252     QSharedPointer<KoShapeBackground> bg;
253     if (fill == "solid") {
254         bg = QSharedPointer<KoShapeBackground>(new KoColorBackground());
255     }
256     else if (fill == "hatch") {
257         bg = QSharedPointer<KoShapeBackground>(new KoHatchBackground());
258     } else if (fill == "gradient") {
259         QString styleName = getStyleProperty("fill-gradient-name", context);
260         KoXmlElement *e = context.odfLoadingContext().stylesReader().drawStyles("gradient").value(styleName);
261         QString style;
262         if (e) {
263             style = e->attributeNS(KoXmlNS::draw, "style", QString());
264         }
265         if ((style == "rectangular") || (style == "square")) {
266             bg = QSharedPointer<KoShapeBackground>(new KoOdfGradientBackground());
267         } else {
268             QGradient *gradient = new QLinearGradient();
269             gradient->setCoordinateMode(QGradient::ObjectBoundingMode);
270             bg = QSharedPointer<KoShapeBackground>(new KoGradientBackground(gradient));
271         }
272     } else if (fill == "bitmap") {
273         bg = QSharedPointer<KoShapeBackground>(new KoPatternBackground(context.imageCollection()));
274 #ifndef NWORKAROUND_ODF_BUGS
275     } else if (fill.isEmpty()) {
276         bg = QSharedPointer<KoShapeBackground>(KoOdfWorkaround::fixBackgroundColor(title, context));
277         return bg;
278 #endif
279     } else {
280         return QSharedPointer<KoShapeBackground>(0);
281     }
282 
283     if (!bg->loadStyle(context.odfLoadingContext(), title->size())) {
284         return QSharedPointer<KoShapeBackground>(0);
285     }
286 
287     return bg;
288 }
289 
loadOdfStroke(KoShape * title,const KoXmlElement & element,KoShapeLoadingContext & context)290 KoShapeStrokeModel *loadOdfStroke(KoShape *title, const KoXmlElement &element, KoShapeLoadingContext &context)
291 {
292     KoStyleStack &styleStack = context.odfLoadingContext().styleStack();
293     KoOdfStylesReader &stylesReader = context.odfLoadingContext().stylesReader();
294 
295     QString stroke = getStyleProperty("stroke", context);
296     if (stroke == "solid" || stroke == "dash") {
297         QPen pen = KoOdfGraphicStyles::loadOdfStrokeStyle(styleStack, stroke, stylesReader);
298 
299         KoShapeStroke *stroke = new KoShapeStroke();
300 
301         if (styleStack.hasProperty(KoXmlNS::calligra, "stroke-gradient")) {
302             QString gradientName = styleStack.property(KoXmlNS::calligra, "stroke-gradient");
303             QBrush brush = KoOdfGraphicStyles::loadOdfGradientStyleByName(stylesReader, gradientName, title->size());
304             stroke->setLineBrush(brush);
305         } else {
306             stroke->setColor(pen.color());
307         }
308 
309 #ifndef NWORKAROUND_ODF_BUGS
310         KoOdfWorkaround::fixPenWidth(pen, context);
311 #endif
312         stroke->setLineWidth(pen.widthF());
313         stroke->setJoinStyle(pen.joinStyle());
314         stroke->setLineStyle(pen.style(), pen.dashPattern());
315         stroke->setCapStyle(pen.capStyle());
316 
317         return stroke;
318 #ifndef NWORKAROUND_ODF_BUGS
319     } else if (stroke.isEmpty()) {
320         QPen pen = KoOdfGraphicStyles::loadOdfStrokeStyle(styleStack, "solid", stylesReader);
321         if (KoOdfWorkaround::fixMissingStroke(pen, element, context, title)) {
322             KoShapeStroke *stroke = new KoShapeStroke();
323 
324 #ifndef NWORKAROUND_ODF_BUGS
325             KoOdfWorkaround::fixPenWidth(pen, context);
326 #endif
327             stroke->setLineWidth(pen.widthF());
328             stroke->setJoinStyle(pen.joinStyle());
329             stroke->setLineStyle(pen.style(), pen.dashPattern());
330             stroke->setCapStyle(pen.capStyle());
331             stroke->setColor(pen.color());
332 
333             return stroke;
334         }
335 #endif
336     }
337 
338     return 0;
339 }
340 
loadOdfShadow(KoShape * title,KoShapeLoadingContext & context)341 KoShapeShadow *loadOdfShadow(KoShape *title, KoShapeLoadingContext &context)
342 {
343     Q_UNUSED(title);
344     KoStyleStack &styleStack = context.odfLoadingContext().styleStack();
345     QString shadowStyle = getStyleProperty("shadow", context);
346     if (shadowStyle == "visible" || shadowStyle == "hidden") {
347         KoShapeShadow *shadow = new KoShapeShadow();
348         QColor shadowColor(styleStack.property(KoXmlNS::draw, "shadow-color"));
349         qreal offsetX = KoUnit::parseValue(styleStack.property(KoXmlNS::draw, "shadow-offset-x"));
350         qreal offsetY = KoUnit::parseValue(styleStack.property(KoXmlNS::draw, "shadow-offset-y"));
351         shadow->setOffset(QPointF(offsetX, offsetY));
352         qreal blur = KoUnit::parseValue(styleStack.property(KoXmlNS::calligra, "shadow-blur-radius"));
353         shadow->setBlur(blur);
354 
355         QString opacity = styleStack.property(KoXmlNS::draw, "shadow-opacity");
356         if (! opacity.isEmpty() && opacity.right(1) == "%")
357             shadowColor.setAlphaF(opacity.leftRef(opacity.length() - 1).toFloat() / 100.0);
358         shadow->setColor(shadowColor);
359         shadow->setVisible(shadowStyle == "visible");
360 
361         return shadow;
362     }
363     return 0;
364 }
365 
loadOdfBorder(KoShape * title,KoShapeLoadingContext & context)366 KoBorder *loadOdfBorder(KoShape *title, KoShapeLoadingContext &context)
367 {
368     Q_UNUSED(title);
369     KoStyleStack &styleStack = context.odfLoadingContext().styleStack();
370 
371     KoBorder *border = new KoBorder();
372     if (border->loadOdf(styleStack)) {
373         return border;
374     }
375     delete border;
376     return 0;
377 }
378 
loadOdfTitle(KoShape * title,KoXmlElement & titleElement,KoShapeLoadingContext & context)379 bool loadOdfTitle(KoShape *title, KoXmlElement &titleElement, KoShapeLoadingContext &context)
380 {
381     TextLabelData *titleData = qobject_cast<TextLabelData*>(title->userData());
382     if (!titleData)
383         return false;
384 
385     // Following will always return false cause KoTextShapeData::loadOdf will try to load
386     // a frame while our text:p is not within a frame. So, let's just not call loadOdf then...
387     //title->loadOdf(titleElement, context);
388 
389     QTextDocument* doc = titleData->document();
390     doc->setPlainText(QString()); // remove default text
391     QTextCursor cursor(doc);
392     QTextCharFormat charFormat = cursor.charFormat();
393 
394     title->setSize(QSize(0,0));
395     title->setPosition(QPointF(0,0));
396 
397     bool autoPosition = !(titleElement.hasAttributeNS(KoXmlNS::svg, "x") && titleElement.hasAttributeNS(KoXmlNS::svg, "y"));
398     bool autoSize = !(titleElement.hasAttributeNS(KoXmlNS::svg, "width") && titleElement.hasAttributeNS(KoXmlNS::svg, "height"));
399     qreal rotationAngle = title->rotation();
400     // Set the styles
401     if (titleElement.hasAttributeNS(KoXmlNS::chart, "style-name")) {
402         KoStyleStack &styleStack = context.odfLoadingContext().styleStack();
403         styleStack.clear();
404         context.odfLoadingContext().fillStyleStack(titleElement, KoXmlNS::chart, "style-name", "chart");
405 
406         styleStack.setTypeProperties("chart");
407         if (styleStack.hasProperty(KoXmlNS::style, "rotation-angle")) {
408             rotationAngle = 360 - KoUnit::parseValue(styleStack.property(KoXmlNS::style, "rotation-angle"));
409             if (rotationAngle != title->rotation()) {
410                 title->rotate(rotationAngle);
411             }
412         }
413         if (styleStack.hasProperty(KoXmlNS::style, "auto-position")) {
414             autoPosition |= styleStack.property(KoXmlNS::style, "auto-position") == "true";
415         }
416         if (styleStack.hasProperty(KoXmlNS::style, "auto-size")) {
417             autoSize |= styleStack.property(KoXmlNS::style, "auto-size") == "true" ;
418         }
419         // title->loadStyle(titleElement, context) is protected
420         // so we duplicate some code:
421         styleStack.setTypeProperties("graphic");
422 
423         title->setBackground(loadOdfFill(title, context));
424         title->setStroke(loadOdfStroke(title, titleElement, context));
425         title->setShadow(loadOdfShadow(title, context));
426         title->setBorder(loadOdfBorder(title, context));
427 
428         styleStack.setTypeProperties("text");
429 
430         QFont font = doc->defaultFont();
431         if (styleStack.hasProperty(KoXmlNS::fo, "font-size")) {
432             const qreal fontSize = KoUnit::parseValue(styleStack.property(KoXmlNS::fo, "font-size"));
433             font.setPointSizeF(fontSize);
434         }
435         if (styleStack.hasProperty(KoXmlNS::fo, "font-family")) {
436             const QString fontFamily = styleStack.property(KoXmlNS::fo, "font-family");
437             font.setFamily(fontFamily);
438         }
439         if (styleStack.hasProperty(KoXmlNS::fo, "font-style")) {
440             QString fontStyle = styleStack.property(KoXmlNS::fo, "font-style");
441             if (fontStyle == "italic") {
442                 font.setItalic(true);
443             } else if (fontStyle == "oblique") {
444                 font.setStyle(QFont::StyleOblique);
445             }
446         }
447         if (styleStack.hasProperty(KoXmlNS::fo, "font-weight")) {
448             QString fontWeight = styleStack.property(KoXmlNS::fo, "font-weight");
449             font.setWeight(fromOdfFontWeight(fontWeight));
450         }
451         doc->setDefaultFont(font);
452         charFormat.setFont(doc->defaultFont());
453 
454         if (styleStack.hasProperty(KoXmlNS::fo, "color")) {
455             const QColor color(styleStack.property(KoXmlNS::fo, "color"));
456             charFormat.setForeground(color);
457         }
458         cursor.setCharFormat(charFormat);
459     }
460     title->setAdditionalStyleAttribute("chart:auto-position", autoPosition ? "true" : "false");
461     if (!autoPosition) {
462         QPointF pos;
463         pos.setX(KoUnit::parseValue(titleElement.attributeNS(KoXmlNS::svg, "x", QString())));
464         if (pos.x() < 0) {
465             pos.setX(0);
466         }
467         pos.setY(KoUnit::parseValue(titleElement.attributeNS(KoXmlNS::svg, "y", QString())));
468         if (pos.y() < 0) {
469             pos.setY(0);
470         }
471         title->setPosition(pos);
472         debugChartOdf<<"position:"<<"odf:"<<pos<<"title"<<title->position();
473     }
474     if (autoSize) {
475         titleData->setResizeMethod(KoTextShapeDataBase::AutoResize);
476     } else {
477         titleData->setResizeMethod(KoTextShapeDataBase::NoResize);
478         QSizeF size;
479         size.setWidth(KoUnit::parseValue(titleElement.attributeNS(KoXmlNS::svg, "width")));
480         size.setHeight(KoUnit::parseValue(titleElement.attributeNS(KoXmlNS::svg, "height")));
481         title->setSize(size);
482         debugChartOdf<<"size:"<<"odf:"<<size<<"title"<<title->size();
483     }
484 
485     // load text
486     bool loaded = false;
487     if (context.documentResourceManager()) {
488         const KoXmlElement textElement = KoXml::namedItemNS(titleElement, KoXmlNS::calligra, "text");
489         if (!textElement.isNull()) {
490             loaded = titleData->loadOdf(textElement, context, 0, title);
491             title->setVisible(true);
492         }
493     }
494     if (!loaded) {
495         const KoXmlElement textElement = KoXml::namedItemNS(titleElement, KoXmlNS::text, "p");
496         if (!textElement.isNull()) {
497             cursor.insertText(textElement.text(), charFormat);
498             title->setVisible(true);
499         }
500     }
501     debugChartOdf<<title->position()<<title->size()<<titleData->document()->toPlainText();
502     return true;
503 }
504 
505 } // Namespace OdfHelper
506 } // Namespace KoChart
507