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 : }
|