LCOV - code coverage report
Current view: top level - src/importexport/imagesexport/internal - svgwriter.cpp Coverage Total Hit
Test: filtered_coverage.info Lines: 0.0 % 146 0
Test Date: 2026-11-05 04:07:02

            Line data    Source code
       1              : /*
       2              :  * SPDX-License-Identifier: GPL-3.0-only
       3              :  * MuseScore-Studio-CLA-applies
       4              :  *
       5              :  * MuseScore Studio
       6              :  * Music Composition & Notation
       7              :  *
       8              :  * Copyright (C) 2021 MuseScore Limited and others
       9              :  *
      10              :  * This program is free software: you can redistribute it and/or modify
      11              :  * it under the terms of the GNU General Public License version 3 as
      12              :  * published by the Free Software Foundation.
      13              :  *
      14              :  * This program is distributed in the hope that it will be useful,
      15              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      16              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      17              :  * GNU General Public License for more details.
      18              :  *
      19              :  * You should have received a copy of the GNU General Public License
      20              :  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
      21              :  */
      22              : 
      23              : #include "svgwriter.h"
      24              : 
      25              : #include <QBuffer>
      26              : 
      27              : #include "draw/painter.h"
      28              : 
      29              : #include "engraving/dom/measure.h"
      30              : #include "engraving/dom/page.h"
      31              : #include "engraving/dom/score.h"
      32              : #include "engraving/dom/staff.h"
      33              : #include "engraving/dom/stafflines.h"
      34              : #include "engraving/dom/system.h"
      35              : #include "engraving/dom/repeatlist.h"
      36              : 
      37              : #include "notation/inotation.h"
      38              : #include "notation/inotationelements.h" // IWYU pragma: keep
      39              : 
      40              : #include "svggenerator.h"
      41              : 
      42              : #include "log.h"
      43              : 
      44              : using namespace mu::iex::imagesexport;
      45              : using namespace mu::project;
      46              : using namespace mu::notation;
      47              : using namespace muse;
      48              : using namespace muse::io;
      49              : 
      50            0 : std::vector<INotationWriter::UnitType> SvgWriter::supportedUnitTypes() const
      51              : {
      52            0 :     return { UnitType::PER_PAGE };
      53              : }
      54              : 
      55            0 : Ret SvgWriter::write(INotationPtr notation, io::IODevice& destinationDevice, const Options& options)
      56              : {
      57            0 :     TRACEFUNC;
      58              : 
      59            0 :     IF_ASSERT_FAILED(notation) {
      60            0 :         return make_ret(Ret::Code::UnknownError);
      61              :     }
      62              : 
      63            0 :     mu::engraving::Score* score = notation->elements()->msScore();
      64            0 :     IF_ASSERT_FAILED(score) {
      65            0 :         return make_ret(Ret::Code::UnknownError);
      66              :     }
      67              : 
      68            0 :     score->setPrinting(true); // don’t print page break symbols etc.
      69              : 
      70            0 :     mu::engraving::MScore::pdfPrinting = true;
      71            0 :     mu::engraving::MScore::svgPrinting = true;
      72              : 
      73            0 :     const std::vector<mu::engraving::Page*>& pages = score->pages();
      74              : 
      75            0 :     const size_t PAGE_NUMBER = muse::value(options, OptionKey::PAGE_NUMBER, Val(0)).toInt();
      76            0 :     if (PAGE_NUMBER >= pages.size()) {
      77            0 :         return false;
      78              :     }
      79              : 
      80            0 :     mu::engraving::Page* page = pages.at(PAGE_NUMBER);
      81              : 
      82            0 :     QByteArray qdata;
      83            0 :     QBuffer buf(&qdata);
      84            0 :     buf.open(QIODevice::WriteOnly);
      85              : 
      86            0 :     SvgGenerator printer;
      87            0 :     QString title(score->name());
      88            0 :     printer.setTitle(pages.size() > 1 ? QString("%1 (%2)").arg(title).arg(PAGE_NUMBER + 1) : title);
      89            0 :     printer.setOutputDevice(&buf);
      90              : 
      91            0 :     printer.setReplaceClipPathWithMask(configuration()->exportSvgWithIllustratorCompat());
      92              : 
      93            0 :     const int TRIM_MARGIN_SIZE = configuration()->trimMarginPixelSize();
      94              : 
      95            0 :     RectF pageRect = page->pageBoundingRect();
      96            0 :     if (TRIM_MARGIN_SIZE >= 0) {
      97            0 :         pageRect = page->tbbox().adjusted(-TRIM_MARGIN_SIZE, -TRIM_MARGIN_SIZE, TRIM_MARGIN_SIZE, TRIM_MARGIN_SIZE);
      98              :     }
      99              : 
     100            0 :     qreal width = pageRect.width();
     101            0 :     qreal height = pageRect.height();
     102            0 :     printer.setSize(QSize(width, height));
     103            0 :     printer.setViewBox(QRectF(0, 0, width, height));
     104              : 
     105            0 :     muse::draw::Painter painter(&printer, "svgwriter");
     106            0 :     painter.setAntialiasing(true);
     107            0 :     if (TRIM_MARGIN_SIZE >= 0) {
     108            0 :         painter.translate(-pageRect.topLeft());
     109              :     }
     110              : 
     111            0 :     const bool TRANSPARENT_BACKGROUND = muse::value(options, OptionKey::TRANSPARENT_BACKGROUND,
     112            0 :                                                     Val(configuration()->exportSvgWithTransparentBackground())).toBool();
     113            0 :     if (!TRANSPARENT_BACKGROUND) {
     114            0 :         painter.fillRect(pageRect, muse::draw::Color::WHITE);
     115              :     }
     116              : 
     117            0 :     engraving::rendering::PaintOptions eopt;
     118            0 :     eopt.isPrinting = true;
     119              : 
     120              :     // 1st pass: StaffLines
     121            0 :     for (const mu::engraving::System* system : page->systems()) {
     122            0 :         size_t stavesCount = system->staves().size();
     123              : 
     124            0 :         for (size_t staffIndex = 0; staffIndex < stavesCount; ++staffIndex) {
     125            0 :             if (!score->staff(staffIndex)->show()) {
     126            0 :                 continue; // ignore invisible staves
     127              :             }
     128              : 
     129            0 :             if (system->staves().empty() || !system->staff(staffIndex)->show()) {
     130            0 :                 continue;
     131              :             }
     132              : 
     133            0 :             mu::engraving::Measure* firstMeasure = system->firstMeasure();
     134            0 :             if (!firstMeasure) { // only boxes, hence no staff lines
     135            0 :                 continue;
     136              :             }
     137              : 
     138              :             // The goal here is to draw SVG staff lines more efficiently.
     139              :             // MuseScore draws staff lines by measure, but for SVG they can
     140              :             // generally be drawn once for each system. This makes a big
     141              :             // difference for scores that scroll horizontally on a single
     142              :             // page.
     143              : 
     144            0 :             mu::engraving::StaffLines* concatenatedSL = nullptr;
     145            0 :             mu::engraving::Shape concatenatedShape;
     146            0 :             mu::engraving::Shape concatenatedMask;
     147            0 :             mu::engraving::StaffType* prevStaffType = nullptr;
     148            0 :             for (mu::engraving::MeasureBase* measure = firstMeasure; measure; measure = system->nextMeasure(measure)) {
     149            0 :                 if (!measure->isMeasure()) {
     150            0 :                     if (concatenatedSL != nullptr) {
     151            0 :                         printer.setElement(concatenatedSL);
     152            0 :                         scoreRenderer()->paintItem(painter, concatenatedSL, eopt);
     153            0 :                         concatenatedSL = nullptr;
     154            0 :                         prevStaffType = nullptr;
     155              :                     }
     156            0 :                     continue;
     157              :                 }
     158              : 
     159            0 :                 Measure* m = mu::engraving::toMeasure(measure);
     160            0 :                 mu::engraving::StaffLines* sl = m->staffLines(static_cast<int>(staffIndex));
     161              : 
     162            0 :                 if ((!m->visible(staffIndex) && !m->isCutawayClef(staffIndex)) || !sl->visible()
     163            0 :                     || (score->staff(staffIndex)->staffType(m->tick()) != prevStaffType)) {
     164            0 :                     if (concatenatedSL != nullptr) {
     165            0 :                         printer.setElement(concatenatedSL);
     166            0 :                         scoreRenderer()->paintItem(painter, concatenatedSL, eopt);
     167            0 :                         concatenatedSL = nullptr;
     168            0 :                         prevStaffType = nullptr;
     169              :                     }
     170              :                 }
     171              : 
     172            0 :                 if (concatenatedSL == nullptr) {
     173            0 :                     if ((m->visible(staffIndex) || m->isCutawayClef(staffIndex)) && sl->visible()) {
     174            0 :                         concatenatedSL = sl->clone();
     175            0 :                         concatenatedShape.add(sl->ldata()->shape());
     176            0 :                         concatenatedMask.add(sl->ldata()->mask());
     177            0 :                         prevStaffType = score->staff(staffIndex)->staffType(m->tick());
     178              :                     }
     179              :                 } else {
     180            0 :                     qreal lastX = sl->ldata()->bbox().right()
     181            0 :                                   + sl->pagePos().x()
     182            0 :                                   - concatenatedSL->pagePos().x();
     183            0 :                     std::vector<muse::LineF> lines = concatenatedSL->lines();
     184            0 :                     for (size_t l = 0, c = lines.size(); l < c; l++) {
     185            0 :                         lines[l].setP2(muse::PointF(lastX, lines[l].p2().y()));
     186              :                     }
     187            0 :                     concatenatedSL->setLines(lines);
     188            0 :                     concatenatedShape.add(sl->ldata()->shape().translated(sl->pagePos() - concatenatedSL->pagePos()));
     189            0 :                     concatenatedMask.add(sl->ldata()->mask().translated(sl->pagePos() - concatenatedSL->pagePos()));
     190            0 :                 }
     191              :             }
     192              : 
     193            0 :             if (concatenatedSL != nullptr) {
     194            0 :                 concatenatedSL->mutldata()->setShape(concatenatedShape);
     195            0 :                 concatenatedSL->mutldata()->setMask(concatenatedMask);
     196            0 :                 printer.setElement(concatenatedSL);
     197            0 :                 scoreRenderer()->paintItem(painter, concatenatedSL, eopt);
     198            0 :                 concatenatedSL = nullptr;
     199            0 :                 prevStaffType = nullptr;
     200              :             }
     201            0 :         }
     202              :     }
     203              : 
     204            0 :     BeatsColors beatsColors = parseBeatsColors(muse::value(options, OptionKey::BEATS_COLORS, Val()).toQVariant());
     205              : 
     206              :     // 2nd pass: Set color for elements on beats
     207            0 :     int beatIndex = 0;
     208            0 :     for (const mu::engraving::RepeatSegment* repeatSegment : score->repeatList()) {
     209            0 :         for (const mu::engraving::Measure* measure : repeatSegment->measureList()) {
     210            0 :             for (mu::engraving::Segment* segment = measure->first(); segment; segment = segment->next()) {
     211            0 :                 if (!segment->isChordRestType()) {
     212            0 :                     continue;
     213              :                 }
     214              : 
     215            0 :                 if (beatsColors.contains(beatIndex)) {
     216            0 :                     for (EngravingItem* element : segment->elist()) {
     217            0 :                         if (!element) {
     218            0 :                             continue;
     219              :                         }
     220              : 
     221            0 :                         if (element->isChord()) {
     222            0 :                             for (Note* note : toChord(element)->notes()) {
     223            0 :                                 note->setColor(beatsColors[beatIndex]);
     224              :                             }
     225            0 :                         } else if (element->isChordRest()) {
     226            0 :                             element->setColor(beatsColors[beatIndex]);
     227              :                         }
     228              :                     }
     229              :                 }
     230              : 
     231            0 :                 beatIndex++;
     232              :             }
     233              :         }
     234              :     }
     235              : 
     236              :     // 3rd pass: the rest of the elements
     237            0 :     std::vector<mu::engraving::EngravingItem*> elements = page->elements();
     238            0 :     std::sort(elements.begin(), elements.end(), mu::engraving::elementLessThan);
     239              : 
     240            0 :     for (const mu::engraving::EngravingItem* element : elements) {
     241              :         // Always exclude invisible elements
     242            0 :         if (!element->collectForDrawing()) {
     243            0 :             continue;
     244              :         }
     245              : 
     246              :         // Match BspTree::items, which checks for bbox intersection
     247              :         // and empty RectF intersects with nothing
     248            0 :         if (element->ldata()->bbox().isEmpty()) {
     249            0 :             continue;
     250              :         }
     251              : 
     252            0 :         mu::engraving::ElementType type = element->type();
     253            0 :         switch (type) { // In future sub-type code, this switch() grows, and eType gets used
     254            0 :         case mu::engraving::ElementType::STAFF_LINES: // Handled in the 1st pass above
     255            0 :             continue; // Exclude from 2nd pass
     256              :             break;
     257            0 :         default:
     258            0 :             break;
     259              :         }
     260              : 
     261              :         // Set the EngravingItem pointer inside SvgGenerator/SvgPaintEngine
     262            0 :         printer.setElement(element);
     263              : 
     264              :         // Paint it
     265            0 :         scoreRenderer()->paintItem(painter, element, eopt);
     266              :     }
     267              : 
     268            0 :     painter.endDraw();
     269              : 
     270            0 :     ByteArray data = ByteArray::fromQByteArrayNoCopy(qdata);
     271            0 :     destinationDevice.write(data);
     272              : 
     273              :     // Clean up and return
     274            0 :     score->setPrinting(false);
     275            0 :     mu::engraving::MScore::pdfPrinting = false;
     276            0 :     mu::engraving::MScore::svgPrinting = false;
     277              : 
     278            0 :     return true;
     279            0 : }
     280              : 
     281            0 : SvgWriter::BeatsColors SvgWriter::parseBeatsColors(const QVariant& obj) const
     282              : {
     283            0 :     QVariantMap map = obj.toMap();
     284            0 :     BeatsColors result;
     285              : 
     286            0 :     for (const QString& beatNumber : map.keys()) {
     287            0 :         result[beatNumber.toInt()] = map[beatNumber].value<QColor>();
     288            0 :     }
     289              : 
     290            0 :     return result;
     291            0 : }
        

Generated by: LCOV version 2.0-1