/* * Copyright (C) 2025-2026 ООО «КАРУМА» * * This file is part of SaiCore. * * SaiCore is proprietary software: you can NOT redistribute it and/or modify * it without the express written permission of ООО «КАРУМА». * * SaiCore is distributed under a proprietary EULA license. * See the LICENSE file for details, or contact support@saicore.ru */ #include "UBSvgSubsetAdaptor.h" #include #include #include #include #include #include #include #include "domain/UBGraphicsSvgItem.h" #include "domain/UBGraphicsPixmapItem.h" #include "domain/UBGraphicsPolygonItem.h" #include "domain/UBGraphicsMediaItem.h" #include "domain/UBGraphicsWidgetItem.h" #include "domain/UBGraphicsPDFItem.h" #include "domain/UBGraphicsTextItem.h" #include "domain/UBGraphicsTextItemDelegate.h" #include "domain/UBGraphicsStroke.h" #include "domain/UBGraphicsStrokesGroup.h" #include "domain/UBGraphicsGroupContainerItem.h" #include "domain/UBGraphicsGroupContainerItemDelegate.h" #include "domain/UBItem.h" #include "tools/UBGraphicsRuler.h" #include "tools/UBGraphicsAxes.h" #include "tools/UBGraphicsCompass.h" #include "tools/UBGraphicsProtractor.h" #include "tools/UBGraphicsCurtainItem.h" #include "tools/UBGraphicsTriangle.h" #include "tools/UBGraphicsCache.h" #include "document/UBDocumentProxy.h" #include "board/UBBoardView.h" #include "board/UBBoardController.h" #include "board/UBDrawingController.h" #include "board/UBBoardPaletteManager.h" #include "frameworks/UBFileSystemUtils.h" #include "frameworks/UBStringUtils.h" #include "frameworks/UBFileSystemUtils.h" #include "core/UBSettings.h" #include "core/UBSetting.h" #include "core/UBPersistenceManager.h" #include "core/UBApplication.h" #include "core/UBDisplayManager.h" #include "core/UBTextTools.h" #include "pdf/PDFRenderer.h" #include "core/memcheck.h" const QString UBSvgSubsetAdaptor::nsSvg = "http://www.w3.org/2000/svg"; const QString UBSvgSubsetAdaptor::nsXHtml = "http://www.w3.org/1999/xhtml"; const QString UBSvgSubsetAdaptor::nsXLink = "http://www.w3.org/1999/xlink"; const QString UBSvgSubsetAdaptor::xmlTrue = "true"; const QString UBSvgSubsetAdaptor::xmlFalse = "false"; const QString UBSvgSubsetAdaptor::sFontSizePrefix = "font-size:"; const QString UBSvgSubsetAdaptor::sPixelUnit = "px"; const QString UBSvgSubsetAdaptor::sFontWeightPrefix = "font-weight:"; const QString UBSvgSubsetAdaptor::sFontStylePrefix = "font-style:"; const QString UBSvgSubsetAdaptor::sFormerUniboardDocumentNamespaceUri = "http://www.mnemis.com/uniboard"; const QString tElement = "element"; const QString tGroup = "group"; const QString tStrokeGroup = "strokeGroup"; const QString tGroups = "groups"; const QString aId = "id"; QString UBSvgSubsetAdaptor::toSvgTransform(const QTransform& matrix) { return QString("matrix(%1, %2, %3, %4, %5, %6)") .arg(matrix.m11(), 0 , 'g') .arg(matrix.m12(), 0 , 'g') .arg(matrix.m21(), 0 , 'g') .arg(matrix.m22(), 0 , 'g') .arg(matrix.dx(), 0 , 'g') .arg(matrix.dy(), 0 , 'g'); } QTransform UBSvgSubsetAdaptor::fromSvgTransform(const QString& transform) { QTransform matrix; QString ts = transform; ts.replace("matrix(", ""); ts.replace(")", ""); QStringList sl = ts.split(","); if (sl.size() >= 6) { matrix.setMatrix( sl.at(0).toFloat(), sl.at(1).toFloat(), 0, sl.at(2).toFloat(), sl.at(3).toFloat(), 0, sl.at(4).toFloat(), sl.at(5).toFloat(), 1); } return matrix; } static bool itemZIndexComp(const QGraphicsItem* item1, const QGraphicsItem* item2) { return item1->data(UBGraphicsItemData::ItemOwnZValue).toReal() < item2->data(UBGraphicsItemData::ItemOwnZValue).toReal(); } void UBSvgSubsetAdaptor::upgradeScene(std::shared_ptr proxy, const int pageIndex) { //4.2 QDomDocument doc = loadSceneDocument(proxy, pageIndex); QDomElement elSvg = doc.documentElement(); // SVG tag QString ubVersion = elSvg.attributeNS(UBSettings::uniboardDocumentNamespaceUri, "version", "4.1"); // default to 4.1 if (ubVersion.startsWith("4.1") || ubVersion.startsWith("4.2") || ubVersion.startsWith("4.3")) { // migrate to 4.2.1 (or latter) std::shared_ptr scene = loadScene(proxy, pageIndex); scene->setModified(true); persistScene(proxy, scene, pageIndex); } } QDomDocument UBSvgSubsetAdaptor::loadSceneDocument(std::shared_ptr proxy, const int pPageIndex) { QString fileName = proxy->persistencePath() + UBFileSystemUtils::digitFileFormat("/page%1.svg",pPageIndex); QFile file(fileName); QDomDocument doc("page"); if (file.exists()) { if (!file.open(QIODevice::ReadOnly)) { qWarning() << "Cannot open file " << fileName << " for reading ..."; return doc; } doc.setContent(&file, true); file.close(); } return doc; } void UBSvgSubsetAdaptor::setSceneUuid(std::shared_ptr proxy, const int pageIndex, QUuid pUuid) { QString fileName = proxy->persistencePath() + UBFileSystemUtils::digitFileFormat("/page%1.svg",pageIndex); QFile file(fileName); if (!file.exists() || !file.open(QIODevice::ReadOnly)) return; QTextStream textReadStream(&file); QString xmlContent = textReadStream.readAll(); int uuidIndex = xmlContent.indexOf("uuid"); if (-1 == uuidIndex) { qWarning() << "Cannot read UUID from file" << fileName << "to set new UUID"; file.close(); return; } int quoteStartIndex = xmlContent.indexOf('"', uuidIndex); if (-1 == quoteStartIndex) { qWarning() << "Cannot read UUID from file" << fileName << "to set new UUID"; file.close(); return; } int quoteEndIndex = xmlContent.indexOf('"', quoteStartIndex + 1); if (-1 == quoteEndIndex) { qWarning() << "Cannot read UUID from file" << fileName << "to set new UUID"; file.close(); return; } file.close(); QString newXmlContent = xmlContent.left(quoteStartIndex + 1); newXmlContent.append(UBStringUtils::toCanonicalUuid(pUuid)); newXmlContent.append(xmlContent.right(xmlContent.length() - quoteEndIndex)); if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { QTextStream textWriteStream(&file); textWriteStream << newXmlContent; file.close(); } else { qWarning() << "Cannot open file" << fileName << "to write UUID"; } } QString UBSvgSubsetAdaptor::uniboardDocumentNamespaceUriFromVersion(int mFileVersion) { return mFileVersion >= 40200 ? UBSettings::uniboardDocumentNamespaceUri : sFormerUniboardDocumentNamespaceUri; } std::shared_ptr UBSvgSubsetAdaptor::loadScene(std::shared_ptr proxy, const int pageIndex) { UBApplication::showMessage(QObject::tr("Loading scene (%1/%2)").arg(pageIndex+1).arg(proxy->pageCount())); QString fileName = proxy->persistencePath() + UBFileSystemUtils::digitFileFormat("/page%1.svg", pageIndex); qInfo() << "loading scene. Filename is : " << fileName; QFile file(fileName); if (file.exists()) { if (!file.open(QIODevice::ReadOnly)) { qWarning() << "Cannot open file " << fileName << " for reading ..."; return 0; } std::shared_ptr scene = loadScene(proxy, file.readAll()); file.close(); return scene; } return 0; } QByteArray UBSvgSubsetAdaptor::loadSceneAsText(std::shared_ptr proxy, const int pageIndex) { QString fileName = proxy->persistencePath() + UBFileSystemUtils::digitFileFormat("/page%1.svg", pageIndex); qDebug() << fileName; QFile file(fileName); if (file.exists()) { if (!file.open(QIODevice::ReadOnly)) { qWarning() << "Cannot open file " << fileName << " for reading ..."; return ""; } return file.readAll(); file.close(); } return ""; } QUuid UBSvgSubsetAdaptor::sceneUuid(std::shared_ptr proxy, const int pageIndex) { QString fileName = proxy->persistencePath() + UBFileSystemUtils::digitFileFormat("/page%1.svg", pageIndex); QFile file(fileName); QUuid uuid; if (file.exists()) { if (!file.open(QIODevice::ReadOnly)) { qWarning() << "Cannot open file " << fileName << " for reading ..."; return QUuid(); } QXmlStreamReader xml(file.readAll()); bool foundSvg = false; while (!xml.atEnd() && !foundSvg) { xml.readNext(); if (xml.isStartElement()) { if (xml.name().toString() == "svg") { auto svgSceneUuid = xml.attributes().value(UBSettings::uniboardDocumentNamespaceUri, "uuid"); if (svgSceneUuid.isNull()) svgSceneUuid = xml.attributes().value("http://www.mnemis.com/uniboard", "uuid"); if (!svgSceneUuid.isNull()) uuid = QUuid(svgSceneUuid.toString()); foundSvg = true; } } } file.close(); } return uuid; } std::shared_ptr UBSvgSubsetAdaptor::loadScene(std::shared_ptr proxy, const QByteArray& pArray) { UBSvgSubsetReader reader(proxy, UBTextTools::cleanHtmlCData(QString(pArray)).toUtf8()); return reader.loadScene(proxy); } std::shared_ptr UBSvgSubsetAdaptor::prepareLoadingScene(std::shared_ptr proxy, const int pageIndex) { const auto fileContent = loadSceneAsText(proxy, pageIndex); auto context = std::make_shared(proxy, fileContent); return context; } UBSvgSubsetAdaptor::UBSvgSubsetReader::UBSvgSubsetReader(std::shared_ptr pProxy, const QByteArray& pXmlData) : mXmlReader(pXmlData) , mProxy(pProxy) , mDocumentPath(pProxy->persistencePath()) , mGroupHasInfo(false) { // NOOP } std::shared_ptr UBSvgSubsetAdaptor::UBSvgSubsetReader::loadScene(std::shared_ptr proxy) { QElapsedTimer time; time.start(); start(); while (!mXmlReader.atEnd()) { processElement(); } qDebug() << "spent milliseconds: " << time.elapsed(); return scene(); } void UBSvgSubsetAdaptor::UBSvgSubsetReader::start() { qDebug() << "loadScene() : starting reading..."; mScene = nullptr; UBGraphicsWidgetItem *currentWidget = 0; //bool pageDpiSpecified = true; saveSceneAfterLoading = false; mFileVersion = 40100; // default to 4.1.0 } bool UBSvgSubsetAdaptor::UBSvgSubsetReader::isFinished() { return mXmlReader.atEnd(); } void UBSvgSubsetAdaptor::UBSvgSubsetReader::processElement() { mXmlReader.readNext(); if (mXmlReader.isStartElement()) { QString name = mXmlReader.name().toString(); if (name == "svg") { if (!mScene) { mScene = std::make_shared(mProxy, false); mScene->setModified(false); } // introduced in UB 4.2 auto svgUbVersion = mXmlReader.attributes().value(UBSettings::uniboardDocumentNamespaceUri, "version"); if (!svgUbVersion.isNull()) { QString ubVersion = svgUbVersion.toString(); //may look like : 4 or 4.1 or 4.2 or 4.2.1, etc QStringList parts = ubVersion.split("."); if (parts.length() > 0) { mFileVersion = parts.at(0).toInt() * 10000; } if (parts.length() > 1) { mFileVersion += parts.at(1).toInt() * 100; } if (parts.length() > 2) { mFileVersion += parts.at(2).toInt(); } } mNamespaceUri = uniboardDocumentNamespaceUriFromVersion(mFileVersion); auto svgSceneUuid = mXmlReader.attributes().value(mNamespaceUri, "uuid"); if (!svgSceneUuid.isNull()) mScene->setUuid(QUuid(svgSceneUuid.toString())); // introduced in UB 4.0 auto svgViewBox = mXmlReader.attributes().value("viewBox"); if (!svgViewBox.isNull()) { QStringList ts = svgViewBox.toString().split(QLatin1Char(' '), UB::SplitBehavior::SkipEmptyParts); QRectF sceneRect; if (ts.size() >= 4) { sceneRect.setX(ts.at(0).toFloat()); sceneRect.setY(ts.at(1).toFloat()); sceneRect.setWidth(ts.at(2).toFloat()); sceneRect.setHeight(ts.at(3).toFloat()); mScene->setSceneRect(sceneRect); } else { qWarning() << "cannot make sense of 'viewBox' value " << svgViewBox.toString(); } } auto pageDpi = mXmlReader.attributes().value("pageDpi"); if (!pageDpi.isNull()) mProxy->setPageDpi(pageDpi.toInt()); else if (mProxy->pageDpi() == 0) { mProxy->setPageDpi(UBApplication::displayManager->logicalDpi(ScreenRole::Control)); //pageDpiSpecified = false; } bool darkBackground = false; bool crossedBackground = false; bool ruledBackground = false; auto ubDarkBackground = mXmlReader.attributes().value(mNamespaceUri, "dark-background"); if (!ubDarkBackground.isNull()) darkBackground = (ubDarkBackground.toString() == xmlTrue); auto ubCrossedBackground = mXmlReader.attributes().value(mNamespaceUri, "crossed-background"); if (!ubCrossedBackground.isNull()) crossedBackground = (ubCrossedBackground.toString() == xmlTrue); auto ubGridSize = mXmlReader.attributes().value(mNamespaceUri, "grid-size"); if (!ubGridSize.isNull()) { int gridSize = ubGridSize.toInt(); mScene->setBackgroundGridSize(gridSize); } if (crossedBackground) { auto ubIntermediateLines = mXmlReader.attributes().value(mNamespaceUri, "intermediate-lines"); if (!ubIntermediateLines.isNull()) { bool intermediateLines = ubIntermediateLines.toInt(); mScene->setIntermediateLines(intermediateLines); } } auto ubRuledBackground = mXmlReader.attributes().value(mNamespaceUri, "ruled-background"); if (!ubRuledBackground.isNull()) ruledBackground = (ubRuledBackground.toString() == xmlTrue); if (ruledBackground && !crossedBackground) { // if for some reason both are true, the background will be a grid auto ubIntermediateLines = mXmlReader.attributes().value(mNamespaceUri, "intermediate-lines"); if (!ubIntermediateLines.isNull()) { bool intermediateLines = ubIntermediateLines.toInt(); mScene->setIntermediateLines(intermediateLines); } } UBPageBackground bg; if (crossedBackground) bg = UBPageBackground::crossed; else if (ruledBackground) bg = UBPageBackground::ruled; else bg = UBPageBackground::plain; mScene->setBackground(darkBackground, bg); auto pageNominalSize = mXmlReader.attributes().value(mNamespaceUri, "nominal-size"); if (!pageNominalSize.isNull()) { QStringList ts = pageNominalSize.toString().split(QLatin1Char('x'), UB::SplitBehavior::SkipEmptyParts); QSize sceneSize; if (ts.size() >= 2) { sceneSize.setWidth(ts.at(0).toInt()); sceneSize.setHeight(ts.at(1).toInt()); mScene->setNominalSize(sceneSize); } else { qWarning() << "cannot make sense of 'nominal-size' value " << pageNominalSize.toString(); } } } else if (name == "g") { strokesGroup = new UBGraphicsStrokesGroup(); graphicsItemFromSvg(strokesGroup); bool hasZValue; qreal zValue = normalizedZValue(&hasZValue); if (hasZValue) { mGroupZIndex = zValue; mGroupHasInfo = true; } auto ubFillOnDarkBackground = mXmlReader.attributes().value(mNamespaceUri, "fill-on-dark-background"); if (!ubFillOnDarkBackground.isNull()) { #if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) mGroupDarkBackgroundColor = QColor::fromString(ubFillOnDarkBackground.toString()); #else mGroupDarkBackgroundColor.setNamedColor(ubFillOnDarkBackground.toString()); #endif } auto ubFillOnLightBackground = mXmlReader.attributes().value(mNamespaceUri, "fill-on-light-background"); if (!ubFillOnLightBackground.isNull()) { #if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) mGroupLightBackgroundColor = QColor::fromString(ubFillOnLightBackground.toString()); #else mGroupLightBackgroundColor.setNamedColor(ubFillOnLightBackground.toString()); #endif } auto ubUuid = mXmlReader.attributes().value(mNamespaceUri, "uuid"); if (!ubUuid.isNull()) strokesGroup->setUuid(QUuid(ubUuid.toString())); else strokesGroup->setUuid(QUuid::createUuid()); QString uuid_stripped = strokesGroup->uuid().toString().replace("}","").replace("{",""); if (!mStrokesList.contains(uuid_stripped)) mStrokesList.insert(uuid_stripped, strokesGroup); } else if (name == "polygon" || name == "line") { UBGraphicsPolygonItem* polygonItem = 0; QString parentId = mXmlReader.attributes().value(mNamespaceUri, "parent").toString(); if (name == "polygon") polygonItem = polygonItemFromPolygonSvg(mScene->isDarkBackground() ? Qt::white : Qt::black); else if (name == "line") polygonItem = polygonItemFromLineSvg(mScene->isDarkBackground() ? Qt::white : Qt::black); if(parentId.isEmpty() && strokesGroup) parentId = strokesGroup->uuid().toString(); if(parentId.isEmpty()) parentId = QUuid::createUuid().toString(); if (polygonItem) { polygonItem->setData(UBGraphicsItemData::ItemLayerType, QVariant(UBItemLayerType::Graphic)); UBGraphicsStrokesGroup* group; if(!mStrokesList.contains(parentId)){ group = new UBGraphicsStrokesGroup(); mStrokesList.insert(parentId,group); group->setTransform(polygonItem->transform()); UBGraphicsItem::assignZValue(group, polygonItem->zValue()); } else group = mStrokesList.value(parentId); if (!currentStroke) currentStroke = new UBGraphicsStroke(); if(polygonItem->transform().isIdentity()) polygonItem->setTransform(group->transform()); group->addToGroup(polygonItem); polygonItem->setStrokesGroup(group); polygonItem->setStroke(currentStroke); polygonItem->show(); group->addToGroup(polygonItem); } } else if (name == "polyline") { QList polygonItems = polygonItemsFromPolylineSvg(mScene->isDarkBackground() ? Qt::white : Qt::black); QString parentId = mXmlReader.attributes().value(mNamespaceUri, "parent").toString(); if(parentId.isEmpty() && strokesGroup) parentId = strokesGroup->uuid().toString(); if(parentId.isEmpty()) parentId = QUuid::createUuid().toString(); foreach(UBGraphicsPolygonItem* polygonItem, polygonItems) { polygonItem->setData(UBGraphicsItemData::ItemLayerType, QVariant(UBItemLayerType::Graphic)); UBGraphicsStrokesGroup* group; if(!mStrokesList.contains(parentId)){ group = new UBGraphicsStrokesGroup(); mStrokesList.insert(parentId,group); group->setTransform(polygonItem->transform()); UBGraphicsItem::assignZValue(group, polygonItem->zValue()); } else group = mStrokesList.value(parentId); if (!currentStroke) currentStroke = new UBGraphicsStroke(); if(polygonItem->transform().isIdentity()) polygonItem->setTransform(group->transform()); group->addToGroup(polygonItem); polygonItem->setStrokesGroup(group); polygonItem->setStroke(currentStroke); polygonItem->show(); group->addToGroup(polygonItem); } } else if (name == "image") { auto imageHref = mXmlReader.attributes().value(nsXLink, "href"); if (!imageHref.isNull()) { QString href = imageHref.toString(); auto ubBackground = mXmlReader.attributes().value(mNamespaceUri, "background"); bool isBackground = (!ubBackground.isNull() && ubBackground.toString() == xmlTrue); if (href.endsWith(".svg")) { UBGraphicsSvgItem* svgItem = svgItemFromSvg(); if (svgItem) { svgItem->setSourceUrl(QUrl::fromLocalFile(href)); svgItem->setFlag(QGraphicsItem::ItemIsMovable, true); svgItem->setFlag(QGraphicsItem::ItemIsSelectable, true); mScene->addItem(svgItem); if (isBackground) mScene->setAsBackgroundObject(svgItem); svgItem->show(); } } else { UBGraphicsPixmapItem* pixmapItem = pixmapItemFromSvg(); if (pixmapItem) { pixmapItem->setSourceUrl(QUrl::fromLocalFile(href)); pixmapItem->setFlag(QGraphicsItem::ItemIsMovable, true); pixmapItem->setFlag(QGraphicsItem::ItemIsSelectable, true); mScene->addItem(pixmapItem); if (isBackground) mScene->setAsBackgroundObject(pixmapItem); pixmapItem->show(); } } } } else if (name == "audio") { UBGraphicsMediaItem* audioItem = audioItemFromSvg(); if (audioItem) { audioItem->setFlag(QGraphicsItem::ItemIsMovable, true); audioItem->setFlag(QGraphicsItem::ItemIsSelectable, true); mScene->addItem(audioItem); audioItem->show(); audioItem->play(); audioItem->pause(); } } else if (name == "video") { UBGraphicsMediaItem* videoItem = videoItemFromSvg(); if (videoItem) { videoItem->setFlag(QGraphicsItem::ItemIsMovable, true); videoItem->setFlag(QGraphicsItem::ItemIsSelectable, true); mScene->addItem(videoItem); videoItem->show(); } } else if (name == "text")//This is for backward compatibility with proto text field prior to version 4.3 { UBGraphicsTextItem* textItem = textItemFromSvg(); if (textItem) { textItem->setFlag(QGraphicsItem::ItemIsMovable, true); textItem->setFlag(QGraphicsItem::ItemIsSelectable, true); mScene->addItem(textItem); textItem->show(); } } else if (name == "curtain") { UBGraphicsCurtainItem* mask = curtainItemFromSvg(); if (mask) { mScene->addItem(mask); mScene->registerTool(mask); } } else if (name == "ruler") { UBGraphicsRuler *ruler = rulerFromSvg(); if (ruler) { mScene->addItem(ruler); mScene->registerTool(ruler); } } else if (name == "axes") { UBGraphicsAxes *axes = axesFromSvg(); if (axes) { mScene->addItem(axes); } } else if (name == "compass") { UBGraphicsCompass *compass = compassFromSvg(); if (compass) { mScene->addItem(compass); mScene->registerTool(compass); } } else if (name == "protractor") { UBGraphicsProtractor *protractor = protractorFromSvg(); if (protractor) { mScene->addItem(protractor); mScene->registerTool(protractor); } } else if (name == "triangle") { UBGraphicsTriangle *triangle = triangleFromSvg(); if (triangle) { mScene->addItem(triangle); mScene->registerTool(triangle); } } else if (name == "cache") { UBGraphicsCache* cache = cacheFromSvg(); if(cache) { mScene->addItem(cache); mScene->registerTool(cache); UBApplication::boardController->notifyCache(true); } } else if (name == "foreignObject") { QString href = mXmlReader.attributes().value(nsXLink, "href").toString(); QString src = mXmlReader.attributes().value(mNamespaceUri, "src").toString(); QString type = mXmlReader.attributes().value(mNamespaceUri, "type").toString(); bool isBackground = mXmlReader.attributes().value(mNamespaceUri, "background").toString() == xmlTrue; qreal foreignObjectWidth = mXmlReader.attributes().value("width").toString().toFloat(); qreal foreignObjectHeight = mXmlReader.attributes().value("height").toString().toFloat(); if (href.contains(".pdf")) { UBGraphicsPDFItem* pdfItem = pdfItemFromPDF(); if (pdfItem) { qreal currentDpi = UBApplication::displayManager->logicalDpi(ScreenRole::Control); // qDebug() << "currentDpi = " << currentDpi; qreal pdfScale = qreal(mProxy->pageDpi())/currentDpi; // qDebug() << "pdfScale " << pdfScale; // If the PDF is in the background, it occupies the whole page; so we can simply // use that information to calculate its scale. if (isBackground) { qreal pageWidth = mScene->nominalSize().width(); qreal pageHeight = mScene->nominalSize().height(); qreal scaleX = pageWidth / pdfItem->sceneBoundingRect().width(); qreal scaleY = pageHeight / pdfItem->sceneBoundingRect().height(); pdfScale = (scaleX+scaleY)/2.; } pdfItem->setScale(pdfScale); pdfItem->setFlag(QGraphicsItem::ItemIsMovable, true); pdfItem->setFlag(QGraphicsItem::ItemIsSelectable, true); mScene->addItem(pdfItem); if (isBackground) mScene->setAsBackgroundObject(pdfItem); pdfItem->show(); currentWidget = 0; } } else if (src.contains(".wdgt")) // NOTE @letsfindaway obsolete { UBGraphicsAppleWidgetItem* appleWidgetItem = graphicsAppleWidgetFromSvg(); if (appleWidgetItem) { appleWidgetItem->setFlag(QGraphicsItem::ItemIsSelectable, true); appleWidgetItem->resize(foreignObjectWidth, foreignObjectHeight); mScene->addItem(appleWidgetItem); appleWidgetItem->show(); currentWidget = appleWidgetItem; } } else if (src.contains(".wgt")) { UBGraphicsW3CWidgetItem* w3cWidgetItem = graphicsW3CWidgetFromSvg(); if (w3cWidgetItem) { // check compatibility QUuid uuid(mXmlReader.attributes().value(mNamespaceUri, "uuid").toString()); if (!mProxy->isWidgetCompatible(uuid)) { // show a substitute image and freeze the widget QPixmap pixmap(w3cWidgetItem->size().toSize()); pixmap.fill(Qt::lightGray); QPainter painter(&pixmap); QFont font = painter.font(); font.setPixelSize(16); painter.setFont(font); const QRectF rectangle(QPointF(0, 0), w3cWidgetItem->size()); std::string incompatibleWidgetMessage = QString("Incompatible widget (%1).").arg(w3cWidgetItem->metadatas().name).toStdString(); painter.drawText(rectangle, Qt::AlignCenter, QObject::tr(incompatibleWidgetMessage.c_str())); w3cWidgetItem->setSnapshot(pixmap, true); // disable user interactions w3cWidgetItem->setFreezable(false); w3cWidgetItem->setCanBeTool(false); } w3cWidgetItem->setFlag(QGraphicsItem::ItemIsSelectable, true); w3cWidgetItem->resize(foreignObjectWidth, foreignObjectHeight); mScene->addItem(w3cWidgetItem); w3cWidgetItem->show(); currentWidget = w3cWidgetItem; } } else if (type == "text") { UBGraphicsTextItem* textItem = textItemFromSvg(); if (textItem) { textItem->setFlag(QGraphicsItem::ItemIsMovable, true); textItem->setFlag(QGraphicsItem::ItemIsSelectable, true); textItem->activateTextEditor(false); mScene->addItem(textItem); textItem->show(); } } else { qWarning() << "Ignoring unknown foreignObject:" << href; } } else if (currentWidget && (name == "preference")) { QString key = mXmlReader.attributes().value("key").toString(); QString value = mXmlReader.attributes().value("value").toString(); currentWidget->setPreference(key, value); } else if (currentWidget && (name == "datastoreEntry")) { QString key = mXmlReader.attributes().value("key").toString(); QString value = mXmlReader.attributes().value("value").toString(); currentWidget->setDatastoreEntry(key, value); } else if (name == tGroups) { //considering groups section at the end of the document readGroupRoot(); } else { // NOOP } } else if (mXmlReader.isEndElement()) { if (mXmlReader.name().toString() == "g") { mGroupHasInfo = false; mGroupDarkBackgroundColor = QColor(); mGroupLightBackgroundColor = QColor(); strokesGroup = NULL; currentStroke = NULL; } } if (mXmlReader.atEnd()) { mMustFinalize = true; } } std::shared_ptr UBSvgSubsetAdaptor::UBSvgSubsetReader::scene() { if (mMustFinalize) { if (mXmlReader.hasError()) { qWarning() << "error parsing file " << mXmlReader.errorString(); } qDebug() << "Number of detected strokes: " << mStrokesList.count(); if (mScene) { QHashIterator iterator(mStrokesList); while (iterator.hasNext()) { iterator.next(); mScene->addItem(iterator.value()); } mScene->setModified(saveSceneAfterLoading); mScene->enableUndoRedoStack(); qDebug() << "loadScene() : created scene and read file"; } mMustFinalize = false; } return mScene; } UBGraphicsGroupContainerItem* UBSvgSubsetAdaptor::UBSvgSubsetReader::readGroup() { UBGraphicsGroupContainerItem *group = new UBGraphicsGroupContainerItem(); QList groupContainer; QString id = mXmlReader.attributes().value(aId).toString(); id = id.mid(1,id.length()-2); bool shouldSkipSubElements = false; if(mStrokesList.contains(id)) shouldSkipSubElements = true; QString ubLocked = mXmlReader.attributes().value(mNamespaceUri, "locked").toString(); if (!ubLocked.isEmpty()) { bool isLocked = ubLocked.contains(xmlTrue); group->setData(UBGraphicsItemData::ItemLocked, QVariant(isLocked)); } QString ubHiddenOnDisplay = mXmlReader.attributes().value(mNamespaceUri, "hidden-on-display").toString(); if (!ubHiddenOnDisplay.isEmpty()) { bool isHiddenOnDisplay = ubHiddenOnDisplay.contains(xmlTrue); group->setData(UBGraphicsItemData::ItemIsHiddenOnDisplay, QVariant(isHiddenOnDisplay)); } auto ubLayer = mXmlReader.attributes().value(mNamespaceUri, "layer"); if (!ubLayer.isNull()) { bool ok; int layerAsInt = ubLayer.toString().toInt(&ok); if (ok) group->setData(UBGraphicsItemData::ItemLayerType, QVariant(layerAsInt)); } auto ubZValue = mXmlReader.attributes().value(mNamespaceUri, "z-value"); if (!ubZValue.isNull()) { bool ok; qreal zValue = ubZValue.toString().toFloat(&ok); if (ok) { group->setZValue(zValue); group->setData(UBGraphicsItemData::ItemOwnZValue, QVariant(zValue)); } } mXmlReader.readNext(); while (!mXmlReader.atEnd()) { if (mXmlReader.isEndElement()) { mXmlReader.readNext(); break; } else if (mXmlReader.isStartElement()) { if (mXmlReader.name() == tGroup) { UBGraphicsGroupContainerItem *curGroup = readGroup(); if (curGroup) groupContainer.append(curGroup); } else if (mXmlReader.name() == tElement && !shouldSkipSubElements) { QString id = mXmlReader.attributes().value(aId).toString(); QGraphicsItem *curItem = readElementFromGroup(); // Explanation: the second condition discriminate the old storage version that should // not be interpreted anymore if(curItem && id.count("{") < 2) groupContainer.append(curItem); } else { mXmlReader.skipCurrentElement(); } } else { mXmlReader.readNext(); } } foreach(QGraphicsItem* item, groupContainer) group->addToGroup(item); if (group->childItems().count()) { // mScene->addItem(group); if (1 == group->childItems().count()) { group->destroy(false); } } return group; } void UBSvgSubsetAdaptor::UBSvgSubsetReader::readGroupRoot() { mXmlReader.readNext(); while (!mXmlReader.atEnd()) { if (mXmlReader.isEndElement()) { mXmlReader.readNext(); break; } else if (mXmlReader.isStartElement()) { if (mXmlReader.name() == tGroup) { UBGraphicsGroupContainerItem *curGroup = readGroup(); if (curGroup) { mScene->addGroup(curGroup); } } else { mXmlReader.skipCurrentElement(); } } else { mXmlReader.readNext(); } } } QGraphicsItem *UBSvgSubsetAdaptor::UBSvgSubsetReader::readElementFromGroup() { QGraphicsItem *result = 0; QString id = mXmlReader.attributes().value(aId).toString(); QString uuid = id.right(QUuid().toString().size()); result = mScene->itemForUuid(QUuid(uuid)); if(!result) result = mStrokesList.take(uuid.replace("}","").replace("{","")); mXmlReader.skipCurrentElement(); mXmlReader.readNext(); return result; } void UBSvgSubsetAdaptor::persistScene(std::shared_ptr proxy, std::shared_ptr pScene, const int pageIndex) { UBSvgSubsetWriter writer(proxy, pScene, pageIndex); writer.persistScene(proxy, pageIndex); } UBSvgSubsetAdaptor::UBSvgSubsetWriter::UBSvgSubsetWriter(std::shared_ptr proxy, std::shared_ptr pScene, const int pageIndex) : mScene(pScene) , mDocumentPath(proxy->persistencePath()) , mPageIndex(pageIndex) { // NOOP } void UBSvgSubsetAdaptor::UBSvgSubsetWriter::writeSvgElement(std::shared_ptr proxy) { mXmlWriter.writeStartElement("svg"); mXmlWriter.writeAttribute("version", "1.1"); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "version", UBSettings::currentFileVersion); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "uuid", UBStringUtils::toCanonicalUuid(mScene->uuid())); int margin = UBSettings::settings()->svgViewBoxMargin->get().toInt(); QRect normalized = mScene->normalizedSceneRect().toRect(); normalized.translate(margin * -1, margin * -1); normalized.setWidth(normalized.width() + (margin * 2)); normalized.setHeight(normalized.height() + (margin * 2)); mXmlWriter.writeAttribute("viewBox", QString("%1 %2 %3 %4").arg(normalized.x()).arg(normalized.y()).arg(normalized.width()).arg(normalized.height())); QSize pageNominalSize = mScene->nominalSize(); if (pageNominalSize.isValid()) { mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "nominal-size", QString("%1x%2").arg(pageNominalSize.width()).arg(pageNominalSize.height())); } mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "dark-background", mScene->isDarkBackground() ? xmlTrue : xmlFalse); bool crossedBackground = mScene->pageBackground() == UBPageBackground::crossed; bool ruledBackground = mScene->pageBackground() == UBPageBackground::ruled; mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "crossed-background", crossedBackground ? xmlTrue : xmlFalse); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "ruled-background", ruledBackground ? xmlTrue : xmlFalse); int gridSize = mScene->backgroundGridSize(); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "grid-size", QString::number(gridSize)); if (crossedBackground || ruledBackground) { bool intermediateLines = mScene->intermediateLines(); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "intermediate-lines", QString::number(intermediateLines)); } if (proxy->pageDpi() == 0) proxy->setPageDpi(UBApplication::displayManager->logicalDpi(ScreenRole::Control)); mXmlWriter.writeAttribute("pageDpi", QString::number(proxy->pageDpi())); mXmlWriter.writeStartElement("rect"); mXmlWriter.writeAttribute("fill", mScene->isDarkBackground() ? "black" : "white"); mXmlWriter.writeAttribute("x", QString::number(normalized.x())); mXmlWriter.writeAttribute("y", QString::number(normalized.y())); mXmlWriter.writeAttribute("width", QString::number(normalized.width())); mXmlWriter.writeAttribute("height", QString::number(normalized.height())); mXmlWriter.writeEndElement(); } bool UBSvgSubsetAdaptor::UBSvgSubsetWriter::persistScene(std::shared_ptr proxy, int pageIndex) { Q_UNUSED(pageIndex); //Creating dom structure to store information QDomDocument groupDomDocument; QDomElement groupRoot = groupDomDocument.createElement(tGroups); groupDomDocument.appendChild(groupRoot); QBuffer buffer; buffer.open(QBuffer::WriteOnly); mXmlWriter.setDevice(&buffer); mXmlWriter.setAutoFormatting(true); mXmlWriter.writeStartDocument(); mXmlWriter.writeDefaultNamespace(nsSvg); mXmlWriter.writeNamespace(nsXLink, "xlink"); mXmlWriter.writeNamespace(UBSettings::uniboardDocumentNamespaceUri, "ub"); mXmlWriter.writeNamespace(nsXHtml, "xhtml"); writeSvgElement(proxy); // Get the items from the scene QList items = mScene->items(); std::sort(items.begin(), items.end(), itemZIndexComp); UBGraphicsStroke *openStroke = 0; bool groupHoldsInfo = false; while (!items.empty()) { QGraphicsItem *item = items.takeFirst(); // Is the item a polygon? UBGraphicsPolygonItem *polygonItem = qgraphicsitem_cast (item); if (polygonItem && polygonItem->isVisible()) { UBGraphicsStroke* currentStroke = polygonItem->stroke(); if (openStroke && (currentStroke != openStroke)) { mXmlWriter.writeEndElement(); //g openStroke = 0; groupHoldsInfo = false; } bool firstPolygonInStroke = currentStroke && !openStroke; if (firstPolygonInStroke) { mXmlWriter.writeStartElement("g"); openStroke = currentStroke; UBGraphicsStroke* stroke = dynamic_cast(currentStroke); if (stroke) { QColor colorOnDarkBackground = polygonItem->colorOnDarkBackground(); QColor colorOnLightBackground = polygonItem->colorOnLightBackground(); UBGraphicsStrokesGroup * sg = polygonItem->strokesGroup(); if (colorOnDarkBackground.isValid() && colorOnLightBackground.isValid() && sg) { mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "z-value" , QString("%1").arg(polygonItem->strokesGroup()->zValue())); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri , "fill-on-dark-background", colorOnDarkBackground.name()); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri , "fill-on-light-background", colorOnLightBackground.name()); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "uuid", UBStringUtils::toCanonicalUuid(sg->uuid())); QVariant locked = sg->data(UBGraphicsItemData::ItemLocked); if (!locked.isNull() && locked.toBool()) mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "locked", xmlTrue); QVariant hiddenOnDisplay = sg->data(UBGraphicsItemData::ItemIsHiddenOnDisplay); if (!hiddenOnDisplay.isNull() && hiddenOnDisplay.toBool()) mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "hidden-on-display", xmlTrue); else mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "hidden-on-display", xmlFalse); QVariant layer = sg->data(UBGraphicsItemData::ItemLayerType); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "layer", QString("%1").arg(layer.toInt())); QTransform matrix = sg->sceneTransform(); if (!matrix.isIdentity()) mXmlWriter.writeAttribute("transform", toSvgTransform(matrix)); qDebug() << "Attributes written"; groupHoldsInfo = true; } } if (stroke && !stroke->hasPressure()) { strokeToSvgPolyline(stroke, groupHoldsInfo); //we can dequeue all polygons belonging to that stroke foreach(UBGraphicsPolygonItem* gi, stroke->polygons()) { items.removeOne(gi); } continue; } } UBGraphicsStroke* stroke = dynamic_cast(currentStroke); if (stroke && stroke->hasPressure()) polygonItemToSvgPolygon(polygonItem, groupHoldsInfo); else if (polygonItem->isNominalLine()) polygonItemToSvgLine(polygonItem, groupHoldsInfo); continue; } if (openStroke) { mXmlWriter.writeEndElement(); //g groupHoldsInfo = false; openStroke = 0; } // Is the item a picture? UBGraphicsPixmapItem *pixmapItem = qgraphicsitem_cast (item); if (pixmapItem && pixmapItem->isVisible()) { pixmapItemToLinkedImage(pixmapItem); continue; } // Is the item a shape? UBGraphicsSvgItem *svgItem = qgraphicsitem_cast (item); if (svgItem && svgItem->isVisible()) { svgItemToLinkedSvg(svgItem); continue; } UBGraphicsVideoItem * videoItem = qgraphicsitem_cast (item); if (videoItem && videoItem->isVisible()) { videoItemToLinkedVideo(videoItem); continue; } UBGraphicsAudioItem * audioItem = qgraphicsitem_cast (item); if (audioItem && audioItem->isVisible()) { audioItemToLinkedAudio(audioItem); continue; } // Is the item an app? // NOTE @letsfindaway obsolete UBGraphicsAppleWidgetItem *appleWidgetItem = qgraphicsitem_cast (item); if (appleWidgetItem && appleWidgetItem->isVisible()) { graphicsAppleWidgetToSvg(appleWidgetItem); continue; } // Is the item a W3C? UBGraphicsW3CWidgetItem *w3cWidgetItem = qgraphicsitem_cast (item); if (w3cWidgetItem && w3cWidgetItem->isVisible()) { graphicsW3CWidgetToSvg(w3cWidgetItem); continue; } // Is the item a PDF? UBGraphicsPDFItem *pdfItem = qgraphicsitem_cast (item); if (pdfItem && pdfItem->isVisible()) { pdfItemToLinkedPDF(pdfItem); continue; } // Is the item a text? UBGraphicsTextItem *textItem = qgraphicsitem_cast (item); if (textItem && textItem->isVisible()) { textItemToSvg(textItem); continue; } // Is the item a curtain? UBGraphicsCurtainItem *curtainItem = qgraphicsitem_cast (item); if (curtainItem && curtainItem->isVisible()) { curtainItemToSvg(curtainItem); continue; } // Is the item a ruler? UBGraphicsRuler *ruler = qgraphicsitem_cast (item); if (ruler && ruler->isVisible()) { rulerToSvg(ruler); continue; } // Is the item a axes? UBGraphicsAxes *axes = qgraphicsitem_cast (item); if (axes && axes->isVisible()) { axesToSvg(axes); continue; } // Is the item a cache? UBGraphicsCache* cache = qgraphicsitem_cast(item); if(cache && cache->isVisible()) { cacheToSvg(cache); continue; } // Is the item a compass UBGraphicsCompass *compass = qgraphicsitem_cast (item); if (compass && compass->isVisible()) { compassToSvg(compass); continue; } // Is the item a protractor? UBGraphicsProtractor *protractor = qgraphicsitem_cast (item); if (protractor && protractor->isVisible()) { protractorToSvg(protractor); continue; } // Is the item a triangle? UBGraphicsTriangle *triangle = qgraphicsitem_cast (item); if (triangle && triangle->isVisible()) { triangleToSvg(triangle); continue; } // Is the item a group? UBGraphicsGroupContainerItem *groupItem = qgraphicsitem_cast(item); if (groupItem && groupItem->isVisible()) { persistGroupToDom(groupItem, &groupRoot, &groupDomDocument); continue; } } if (openStroke) { mXmlWriter.writeEndElement(); openStroke = 0; } //writing group data if (groupRoot.hasChildNodes()) { mXmlWriter.writeStartElement(tGroups); QDomElement curElement = groupRoot.firstChildElement(); while (!curElement.isNull()) { if (curElement.hasAttribute(aId)) { mXmlWriter.writeStartElement(curElement.tagName()); mXmlWriter.writeAttribute(aId, curElement.attribute(aId)); if(curElement.hasAttribute("locked")){ mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri,"locked",curElement.attribute("locked")); } if(curElement.hasAttribute("hidden-on-display")){ mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri,"hidden-on-display", curElement.attribute("hidden-on-display")); } if(curElement.hasAttribute("layer")){ mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri,"layer",curElement.attribute("layer")); } if(curElement.hasAttribute("z-value")){ mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri,"z-value",curElement.attribute("z-value")); } QDomElement curSubElement = curElement.firstChildElement(); while (!curSubElement.isNull()) { if (curSubElement.hasAttribute(aId)) { mXmlWriter.writeStartElement(curSubElement.tagName()); mXmlWriter.writeAttribute(aId, curSubElement.attribute(aId)); mXmlWriter.writeEndElement(); curSubElement = curSubElement.nextSiblingElement(); } } mXmlWriter.writeEndElement(); } curElement = curElement.nextSiblingElement(); } mXmlWriter.writeEndElement(); } mXmlWriter.writeEndDocument(); QString fileName = mDocumentPath + UBFileSystemUtils::digitFileFormat("/page%1.svg", mPageIndex); QFile file(fileName); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { qCritical() << "cannot open " << fileName << " for writing. Error : " << file.errorString(); return false; } file.write(buffer.data()); file.flush(); file.close(); return true; } void UBSvgSubsetAdaptor::UBSvgSubsetWriter::persistGroupToDom(QGraphicsItem *groupItem, QDomElement *curParent, QDomDocument *groupDomDocument) { QUuid uuid = UBGraphicsScene::getPersonalUuid(groupItem); if (!uuid.isNull()) { QDomElement curGroupElement = groupDomDocument->createElement(tGroup); curGroupElement.setAttribute(aId, uuid.toString()); UBGraphicsGroupContainerItem* group = dynamic_cast(groupItem); if(group && group->Delegate()){ if(group->Delegate()->isLocked()) curGroupElement.setAttribute("locked", xmlTrue); else curGroupElement.setAttribute("locked", xmlFalse); if (group->data(UBGraphicsItemData::ItemIsHiddenOnDisplay).toBool()) curGroupElement.setAttribute("hidden-on-display", xmlTrue); else curGroupElement.setAttribute("hidden-on-display", xmlFalse); curGroupElement.setAttribute("layer", group->data(UBGraphicsItemData::ItemLayerType).toString()); QString zs; zs.setNum(group->zValue(), 'f'); // 'f' keeps precision curGroupElement.setAttribute("z-value", zs); } curParent->appendChild(curGroupElement); foreach (QGraphicsItem *item, groupItem->childItems()) { QUuid tmpUuid = UBGraphicsScene::getPersonalUuid(item); if (!tmpUuid.isNull()) { if (item->type() == UBGraphicsGroupContainerItem::Type && item->childItems().count()) persistGroupToDom(item, curParent, groupDomDocument); else { QDomElement curSubElement = groupDomDocument->createElement(tElement); curSubElement.setAttribute(aId, tmpUuid.toString()); curGroupElement.appendChild(curSubElement); } } } } } void UBSvgSubsetAdaptor::UBSvgSubsetWriter::polygonItemToSvgLine(UBGraphicsPolygonItem* polygonItem, bool groupHoldsInfo) { mXmlWriter.writeStartElement("line"); QLineF line = polygonItem->originalLine(); mXmlWriter.writeAttribute("x1", QString::number(line.p1().x(), 'f', 2)); mXmlWriter.writeAttribute("y1", QString::number(line.p1().y(), 'f', 2)); // SVG renderers (Chrome) do not like line where (x1, y1) == (x2, y2) qreal x2 = line.p2().x(); if (line.p1() == line.p2()) x2 += 0.01; mXmlWriter.writeAttribute("x2", QString::number(x2, 'f', 2)); mXmlWriter.writeAttribute("y2", QString::number(line.p2().y(), 'f', 2)); mXmlWriter.writeAttribute("stroke-width", QString::number(polygonItem->originalWidth(), 'f', -1)); mXmlWriter.writeAttribute("stroke", polygonItem->brush().color().name()); qreal alpha = polygonItem->brush().color().alphaF(); if (alpha < 1.0) mXmlWriter.writeAttribute("stroke-opacity", QString::number(alpha, 'f', 2)); mXmlWriter.writeAttribute("stroke-linecap", "round"); if (!groupHoldsInfo) { mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "z-value", QString("%1").arg(polygonItem->zValue())); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "fill-on-dark-background", polygonItem->colorOnDarkBackground().name()); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "fill-on-light-background", polygonItem->colorOnLightBackground().name()); } mXmlWriter.writeEndElement(); } void UBSvgSubsetAdaptor::UBSvgSubsetWriter::strokeToSvgPolyline(UBGraphicsStroke* stroke, bool groupHoldsInfo) { QList pols = stroke->polygons(); if (pols.length() > 0) { mXmlWriter.writeStartElement("polyline"); QVector points; foreach(UBGraphicsPolygonItem* polygon, pols) { points << polygon->originalLine().p1(); } points << pols.last()->originalLine().p2(); // SVG renderers (Chrome) do not like line withe where x1/y1 == x2/y2 if (points.size() == 2 && (points.at(0) == points.at(1))) { points[1] = QPointF(points[1].x() + 0.01, points[1].y()); } QString svgPoints = pointsToSvgPointsAttribute(points); mXmlWriter.writeAttribute("points", svgPoints); UBGraphicsPolygonItem* firstPolygonItem = pols.at(0); mXmlWriter.writeAttribute("fill", "none"); mXmlWriter.writeAttribute("stroke-width", QString::number(firstPolygonItem->originalWidth(), 'f', 2)); mXmlWriter.writeAttribute("stroke", firstPolygonItem->brush().color().name()); mXmlWriter.writeAttribute("stroke-opacity", QString("%1").arg(firstPolygonItem->brush().color().alphaF())); mXmlWriter.writeAttribute("stroke-linecap", "round"); if (!groupHoldsInfo) { mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "z-value", QString("%1").arg(firstPolygonItem->zValue())); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri , "fill-on-dark-background", firstPolygonItem->colorOnDarkBackground().name()); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri , "fill-on-light-background", firstPolygonItem->colorOnLightBackground().name()); } mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "uuid", UBStringUtils::toCanonicalUuid(firstPolygonItem->uuid())); if (firstPolygonItem->parentItem()) { mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "parent", UBStringUtils::toCanonicalUuid(UBGraphicsItem::getOwnUuid(firstPolygonItem->strokesGroup()))); } mXmlWriter.writeEndElement(); } } void UBSvgSubsetAdaptor::UBSvgSubsetWriter::strokeToSvgPolygon(UBGraphicsStroke* stroke, bool groupHoldsInfo) { QList pis = stroke->polygons(); if (pis.length() > 0) { QPolygonF united; foreach(UBGraphicsPolygonItem* pi, pis) { united = united.united(pi->polygon()); } QScopedPointer clone(static_cast(pis.at(0)->deepCopy())); clone->setPolygon(united); polygonItemToSvgPolygon(clone.get(), groupHoldsInfo); } } void UBSvgSubsetAdaptor::UBSvgSubsetWriter::polygonItemToSvgPolygon(UBGraphicsPolygonItem* polygonItem, bool groupHoldsInfo) { QPolygonF polygon = polygonItem->polygon(); int pointsCount = polygon.size(); if (pointsCount > 0) { mXmlWriter.writeStartElement("polygon"); QString points = pointsToSvgPointsAttribute(polygon); mXmlWriter.writeAttribute("points", points); mXmlWriter.writeAttribute("transform",toSvgTransform(polygonItem->transform())); mXmlWriter.writeAttribute("fill", polygonItem->brush().color().name()); qreal alpha = polygonItem->brush().color().alphaF(); mXmlWriter.writeAttribute("fill-opacity", QString::number(alpha, 'f', 2)); // we trick SVG antialiasing, to avoid seeing light gaps between polygons if (alpha < 1.0) { qreal trickedAlpha = trickAlpha(alpha); mXmlWriter.writeAttribute("stroke", polygonItem->brush().color().name()); mXmlWriter.writeAttribute("stroke-width", "1"); mXmlWriter.writeAttribute("stroke-opacity", QString::number(trickedAlpha, 'f', 2)); } // svg default fill rule is nonzero, but Qt is evenodd // //http://www.w3.org/TR/SVG11/painting.html //http://doc.trolltech.com/4.5/qgraphicspolygonitem.html#fillRule // if (polygonItem->fillRule() == Qt::OddEvenFill) mXmlWriter.writeAttribute("fill-rule", "evenodd"); else mXmlWriter.writeAttribute("fill-rule", "winding"); if (!groupHoldsInfo) { mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "z-value", QString("%1").arg(polygonItem->zValue())); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri , "fill-on-dark-background", polygonItem->colorOnDarkBackground().name()); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri , "fill-on-light-background", polygonItem->colorOnLightBackground().name()); } mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "uuid", UBStringUtils::toCanonicalUuid(polygonItem->uuid())); UBGraphicsStrokesGroup* sg = polygonItem->strokesGroup(); if (sg) mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "parent", UBStringUtils::toCanonicalUuid(sg->uuid())); mXmlWriter.writeEndElement(); } } UBGraphicsPolygonItem* UBSvgSubsetAdaptor::UBSvgSubsetReader::polygonItemFromPolygonSvg(const QColor& pDefaultColor) { UBGraphicsPolygonItem* polygonItem = new UBGraphicsPolygonItem(); graphicsItemFromSvg(polygonItem); auto svgPoints = mXmlReader.attributes().value("points"); QPolygonF polygon; if (!svgPoints.isNull()) { QStringList ts = svgPoints.toString().split(QLatin1Char(' '), UB::SplitBehavior::SkipEmptyParts); foreach(const QString sPoint, ts) { QStringList sCoord = sPoint.split(QLatin1Char(','), UB::SplitBehavior::SkipEmptyParts); if (sCoord.size() == 2) { QPointF point; point.setX(sCoord.at(0).toFloat()); point.setY(sCoord.at(1).toFloat()); polygon << point; } else if (sCoord.size() == 4){ //This is the case on system were the "," is used to seperate decimal QPointF point; QString x = sCoord.at(0) + "." + sCoord.at(1); QString y = sCoord.at(2) + "." + sCoord.at(3); point.setX(x.toFloat()); point.setY(y.toFloat()); polygon << point; } else { qWarning() << "cannot make sense of a 'point' value" << sCoord; } } } else { qWarning() << "cannot make sense of 'points' value " << svgPoints.toString(); } polygonItem->setPolygon(polygon); auto svgFill = mXmlReader.attributes().value("fill"); QColor brushColor = pDefaultColor; if (!svgFill.isNull()) #if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) brushColor = QColor::fromString(svgFill.toString()); #else brushColor.setNamedColor(svgFill.toString()); #endif auto svgFillOpacity = mXmlReader.attributes().value("fill-opacity"); qreal opacity = 1.0; if (!svgFillOpacity.isNull()) { opacity = svgFillOpacity.toString().toFloat(); brushColor.setAlphaF(opacity); } polygonItem->setColor(brushColor); auto ubFillOnDarkBackground = mXmlReader.attributes().value(mNamespaceUri, "fill-on-dark-background"); if (!ubFillOnDarkBackground.isNull()) { #if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) QColor color = QColor::fromString(ubFillOnDarkBackground.toString()); #else QColor color; color.setNamedColor(ubFillOnDarkBackground.toString()); #endif if (!color.isValid()) color = Qt::white; color.setAlphaF(opacity); polygonItem->setColorOnDarkBackground(color); } else { QColor color = mGroupDarkBackgroundColor; color.setAlphaF(opacity); polygonItem->setColorOnDarkBackground(color); } auto ubFillOnLightBackground = mXmlReader.attributes().value(mNamespaceUri, "fill-on-light-background"); if (!ubFillOnLightBackground.isNull()) { #if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) QColor color = QColor::fromString(ubFillOnLightBackground.toString()); #else QColor color; color.setNamedColor(ubFillOnLightBackground.toString()); #endif if (!color.isValid()) color = Qt::black; color.setAlphaF(opacity); polygonItem->setColorOnLightBackground(color); } else { QColor color = mGroupLightBackgroundColor; color.setAlphaF(opacity); polygonItem->setColorOnLightBackground(color); } // Before SaiCore v1.4, fill rule was only saved if it was "Even-odd". Therefore if no fill rule // is specified, we assume that it should be Winding fill. auto fillRule = mXmlReader.attributes().value("fill-rule"); if (!fillRule.isNull() && fillRule.toString() == "evenodd") polygonItem->setFillRule(Qt::OddEvenFill); else polygonItem->setFillRule(Qt::WindingFill); return polygonItem; } UBGraphicsPolygonItem* UBSvgSubsetAdaptor::UBSvgSubsetReader::polygonItemFromLineSvg(const QColor& pDefaultColor) { auto svgX1 = mXmlReader.attributes().value("x1"); auto svgY1 = mXmlReader.attributes().value("y1"); auto svgX2 = mXmlReader.attributes().value("x2"); auto svgY2 = mXmlReader.attributes().value("y2"); QLineF line; if (!svgX1.isNull() && !svgY1.isNull() && !svgX2.isNull() && !svgY2.isNull()) { qreal x1 = svgX1.toString().toFloat(); qreal y1 = svgY1.toString().toFloat(); qreal x2 = svgX2.toString().toFloat(); qreal y2 = svgY2.toString().toFloat(); line.setLine(x1, y1, x2, y2); } else { qWarning() << "cannot make sense of 'line' value"; return 0; } auto strokeWidth = mXmlReader.attributes().value("stroke-width"); qreal lineWidth = 1.; if (!strokeWidth.isNull()) { lineWidth = strokeWidth.toString().toFloat(); } UBGraphicsPolygonItem* polygonItem = new UBGraphicsPolygonItem(line, lineWidth); graphicsItemFromSvg(polygonItem); auto svgStroke = mXmlReader.attributes().value("stroke"); QColor brushColor = pDefaultColor; if (!svgStroke.isNull()) { #if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) brushColor = QColor::fromString(svgStroke.toString()); #else brushColor.setNamedColor(svgStroke.toString()); #endif } auto svgStrokeOpacity = mXmlReader.attributes().value("stroke-opacity"); qreal opacity = 1.0; if (!svgStrokeOpacity.isNull()) { opacity = svgStrokeOpacity.toString().toFloat(); brushColor.setAlphaF(opacity); } polygonItem->setColor(brushColor); auto ubFillOnDarkBackground = mXmlReader.attributes().value(mNamespaceUri, "fill-on-dark-background"); if (!ubFillOnDarkBackground.isNull()) { #if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) QColor color = QColor::fromString(ubFillOnDarkBackground.toString()); #else QColor color; color.setNamedColor(ubFillOnDarkBackground.toString()); #endif if (!color.isValid()) color = Qt::white; color.setAlphaF(opacity); polygonItem->setColorOnDarkBackground(color); } else { QColor color = mGroupDarkBackgroundColor; color.setAlphaF(opacity); polygonItem->setColorOnDarkBackground(color); } auto ubFillOnLightBackground = mXmlReader.attributes().value(mNamespaceUri, "fill-on-light-background"); if (!ubFillOnLightBackground.isNull()) { #if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) QColor color = QColor::fromString(ubFillOnLightBackground.toString()); #else QColor color; color.setNamedColor(ubFillOnLightBackground.toString()); #endif if (!color.isValid()) color = Qt::black; color.setAlphaF(opacity); polygonItem->setColorOnLightBackground(color); } else { QColor color = mGroupLightBackgroundColor; color.setAlphaF(opacity); polygonItem->setColorOnLightBackground(color); } return polygonItem; } QList UBSvgSubsetAdaptor::UBSvgSubsetReader::polygonItemsFromPolylineSvg(const QColor& pDefaultColor) { auto strokeWidth = mXmlReader.attributes().value("stroke-width"); qreal lineWidth = 1.; if (!strokeWidth.isNull()) { lineWidth = strokeWidth.toString().toFloat(); } QColor brushColor = pDefaultColor; auto svgStroke = mXmlReader.attributes().value("stroke"); if (!svgStroke.isNull()) { #if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) brushColor = QColor::fromString(svgStroke.toString()); #else brushColor.setNamedColor(svgStroke.toString()); #endif } qreal opacity = 1.0; auto svgStrokeOpacity = mXmlReader.attributes().value("stroke-opacity"); if (!svgStrokeOpacity.isNull()) { opacity = svgStrokeOpacity.toString().toFloat(); brushColor.setAlphaF(opacity); } bool hasZValue; qreal zValue = normalizedZValue(&hasZValue); if (!hasZValue) { zValue = mGroupZIndex; } QColor colorOnDarkBackground = mGroupDarkBackgroundColor; auto ubFillOnDarkBackground = mXmlReader.attributes().value(mNamespaceUri, "fill-on-dark-background"); if (!ubFillOnDarkBackground.isNull()) { #if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) colorOnDarkBackground = QColor::fromString(ubFillOnDarkBackground.toString()); #else colorOnDarkBackground.setNamedColor(ubFillOnDarkBackground.toString()); #endif } if (!colorOnDarkBackground.isValid()) colorOnDarkBackground = Qt::white; colorOnDarkBackground.setAlphaF(opacity); QColor colorOnLightBackground = mGroupLightBackgroundColor; auto ubFillOnLightBackground = mXmlReader.attributes().value(mNamespaceUri, "fill-on-light-background"); if (!ubFillOnLightBackground.isNull()) { #if (QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)) colorOnLightBackground = QColor::fromString(ubFillOnLightBackground.toString()); #else colorOnLightBackground.setNamedColor(ubFillOnLightBackground.toString()); #endif } if (!colorOnLightBackground.isValid()) colorOnLightBackground = Qt::black; colorOnLightBackground.setAlphaF(opacity); auto svgPoints = mXmlReader.attributes().value("points"); QList polygonItems; if (!svgPoints.isNull()) { QStringList ts = svgPoints.toString().split(QLatin1Char(' '), UB::SplitBehavior::SkipEmptyParts); QList points; foreach(const QString sPoint, ts) { QStringList sCoord = sPoint.split(QLatin1Char(','), UB::SplitBehavior::SkipEmptyParts); if (sCoord.size() == 2) { QPointF point; point.setX(sCoord.at(0).toFloat()); point.setY(sCoord.at(1).toFloat()); points << point; } else if (sCoord.size() == 4){ //This is the case on system were the "," is used to seperate decimal QPointF point; QString x = sCoord.at(0) + "." + sCoord.at(1); QString y = sCoord.at(2) + "." + sCoord.at(3); point.setX(x.toFloat()); point.setY(y.toFloat()); points << point; } else { qWarning() << "cannot make sense of a 'point' value" << sCoord; } } for (int i = 0; i < points.size() - 1; i++) { UBGraphicsPolygonItem* polygonItem = new UBGraphicsPolygonItem(QLineF(points.at(i), points.at(i + 1)), lineWidth); polygonItem->setColor(brushColor); UBGraphicsItem::assignZValue(polygonItem, zValue); polygonItem->setColorOnDarkBackground(colorOnDarkBackground); polygonItem->setColorOnLightBackground(colorOnLightBackground); polygonItems <uuid().toString() + ".*"}); if (imageFiles.size() >= 1) { mXmlWriter.writeStartElement("image"); QString fileName = UBPersistenceManager::imageDirectory + "/" + imageFiles.last(); mXmlWriter.writeAttribute(nsXLink, "href", fileName); graphicsItemToSvg(pixmapItem); mXmlWriter.writeEndElement(); } } UBGraphicsPixmapItem* UBSvgSubsetAdaptor::UBSvgSubsetReader::pixmapItemFromSvg() { UBGraphicsPixmapItem* pixmapItem = nullptr; auto imageHref = mXmlReader.attributes().value(nsXLink, "href"); if (!imageHref.isNull()) { pixmapItem = new UBGraphicsPixmapItem(); QString href = imageHref.toString(); QImageReader rdr(mDocumentPath + "/" + UBFileSystemUtils::normalizeFilePath(href)); rdr.setAutoTransform(true); QImage img = rdr.read(); QPixmap pix = QPixmap::fromImage(img); pixmapItem->setPixmap(pix); graphicsItemFromSvg(pixmapItem); } else { qWarning() << "cannot make sense of image href value"; } return pixmapItem; } void UBSvgSubsetAdaptor::UBSvgSubsetWriter::svgItemToLinkedSvg(UBGraphicsSvgItem* svgItem) { mXmlWriter.writeStartElement("image"); QString fileName = UBPersistenceManager::imageDirectory + "/" + svgItem->uuid().toString() + ".svg"; mXmlWriter.writeAttribute(nsXLink, "href", fileName); graphicsItemToSvg(svgItem); mXmlWriter.writeEndElement(); } UBGraphicsSvgItem* UBSvgSubsetAdaptor::UBSvgSubsetReader::svgItemFromSvg() { UBGraphicsSvgItem* svgItem = 0; auto imageHref = mXmlReader.attributes().value(nsXLink, "href"); if (!imageHref.isNull()) { QString href = imageHref.toString(); svgItem = new UBGraphicsSvgItem(mDocumentPath + "/" + UBFileSystemUtils::normalizeFilePath(href)); } else { qWarning() << "cannot make sens of image href value"; return 0; } graphicsItemFromSvg(svgItem); return svgItem; } void UBSvgSubsetAdaptor::UBSvgSubsetWriter::pdfItemToLinkedPDF(UBGraphicsPDFItem* pdfItem) { mXmlWriter.writeStartElement("foreignObject"); mXmlWriter.writeAttribute("requiredExtensions", "http://ns.adobe.com/pdf/1.3/"); QString fileName = UBPersistenceManager::objectDirectory + "/" + pdfItem->fileUuid().toString() + ".pdf"; QString path = mDocumentPath + "/" + fileName; if (!QFile::exists(path)) { QDir dir; dir.mkdir(mDocumentPath + "/" + UBPersistenceManager::objectDirectory); QFile file(path); if (!file.open(QIODevice::WriteOnly)) { qWarning() << "cannot open file for writing embeded pdf content " << path; return; } file.write(pdfItem->fileData()); file.close(); } mXmlWriter.writeAttribute(nsXLink, "href", fileName + "#page=" + QString::number(pdfItem->pageNumber())); graphicsItemToSvg(pdfItem); mXmlWriter.writeEndElement(); } UBGraphicsPDFItem* UBSvgSubsetAdaptor::UBSvgSubsetReader::pdfItemFromPDF() { UBGraphicsPDFItem* pdfItem = 0; QString href = mXmlReader.attributes().value(nsXLink, "href").toString(); QStringList parts = href.split("#page="); if (parts.count() != 2) { qWarning() << "invalid pdf href value" << href; return 0; } QString pdfPath = parts[0]; QUuid uuid(QFileInfo(pdfPath).baseName()); int pageNumber = parts[1].toInt(); pdfItem = new UBGraphicsPDFItem(PDFRenderer::rendererForUuid(uuid, mDocumentPath + "/" + UBFileSystemUtils::normalizeFilePath(pdfPath)), pageNumber); graphicsItemFromSvg(pdfItem); return pdfItem; } void UBSvgSubsetAdaptor::UBSvgSubsetWriter::audioItemToLinkedAudio(UBGraphicsAudioItem *audioItem) { mXmlWriter.writeStartElement("audio"); graphicsItemToSvg(audioItem); if (audioItem->playerState() == QMediaPlayer::PausedState && (audioItem->mediaDuration() - audioItem->mediaPosition()) > 0) { qint64 pos = audioItem->mediaPosition(); mXmlWriter.writeAttribute(UBSettings::uniboardDocumentNamespaceUri, "position", QString("%1").arg(pos)); } QString audioFileHref = "audios/" + audioItem->mediaFileUrl().fileName(); mXmlWriter.writeAttribute(nsXLink, "href", audioFileHref); mXmlWriter.writeEndElement(); } void UBSvgSubsetAdaptor::UBSvgSubsetWriter::videoItemToLinkedVideo(UBGraphicsVideoItem* videoItem) { /* w3c sample * *