1 //=============================================================================
2 // MusE Score
3 // Linux Music Score Editor
4 //
5 // Copyright (C) 2002-2015 Werner Schweer and others
6 //
7 // This program is free software; you can redistribute it and/or modify
8 // it under the terms of the GNU General Public License version 2.
9 //
10 // This program 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
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with this program; if not, write to the Free Software
17 // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 //=============================================================================
19
20 /**
21 MusicXML import.
22 */
23
24 #include "thirdparty/qzip/qzipreader_p.h"
25 #include "importmxml.h"
26
27 namespace Ms {
28
29 //---------------------------------------------------------
30 // tupletAssert -- check assertions for tuplet handling
31 //---------------------------------------------------------
32
33 /**
34 Check assertions for tuplet handling. If this fails, MusicXML
35 import will almost certainly break in non-obvious ways.
36 Should never happen, thus it is OK to quit the application.
37 */
38
tupletAssert()39 static void tupletAssert()
40 {
41 if (!(int(TDuration::DurationType::V_BREVE) == int(TDuration::DurationType::V_LONG) + 1
42 && int(TDuration::DurationType::V_WHOLE) == int(TDuration::DurationType::V_BREVE) + 1
43 && int(TDuration::DurationType::V_HALF) == int(TDuration::DurationType::V_WHOLE) + 1
44 && int(TDuration::DurationType::V_QUARTER) == int(TDuration::DurationType::V_HALF) + 1
45 && int(TDuration::DurationType::V_EIGHTH) == int(TDuration::DurationType::V_QUARTER) + 1
46 && int(TDuration::DurationType::V_16TH) == int(TDuration::DurationType::V_EIGHTH) + 1
47 && int(TDuration::DurationType::V_32ND) == int(TDuration::DurationType::V_16TH) + 1
48 && int(TDuration::DurationType::V_64TH) == int(TDuration::DurationType::V_32ND) + 1
49 && int(TDuration::DurationType::V_128TH) == int(TDuration::DurationType::V_64TH) + 1
50 && int(TDuration::DurationType::V_256TH) == int(TDuration::DurationType::V_128TH) + 1
51 && int(TDuration::DurationType::V_512TH) == int(TDuration::DurationType::V_256TH) + 1
52 && int(TDuration::DurationType::V_1024TH) == int(TDuration::DurationType::V_512TH) + 1
53 )) {
54 qFatal("tupletAssert() failed");
55 }
56 }
57
58 //---------------------------------------------------------
59 // initMusicXmlSchema
60 // return false on error
61 //---------------------------------------------------------
62
initMusicXmlSchema(QXmlSchema & schema)63 static bool initMusicXmlSchema(QXmlSchema& schema)
64 {
65 // read the MusicXML schema from the application resources
66 QFile schemaFile(":/schema/musicxml.xsd");
67 if (!schemaFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
68 qDebug("initMusicXmlSchema() could not open resource musicxml.xsd");
69 MScore::lastError = QObject::tr("Internal error: Could not open resource musicxml.xsd\n");
70 return false;
71 }
72
73 // copy the schema into a QByteArray and fixup xs:imports,
74 // using a path to the application resources instead of to www.musicxml.org
75 // to prevent downloading from the net
76 QByteArray schemaBa;
77 QTextStream schemaStream(&schemaFile);
78 while (!schemaStream.atEnd()) {
79 QString line = schemaStream.readLine();
80 if (line.contains("xs:import"))
81 line.replace("http://www.musicxml.org/xsd", "qrc:///schema");
82 schemaBa += line.toUtf8();
83 schemaBa += "\n";
84 }
85
86 // load and validate the schema
87 schema.load(schemaBa);
88 if (!schema.isValid()) {
89 qDebug("initMusicXmlSchema() internal error: MusicXML schema is invalid");
90 MScore::lastError = QObject::tr("Internal error: MusicXML schema is invalid\n");
91 return false;
92 }
93
94 return true;
95 }
96
97
98 //---------------------------------------------------------
99 // musicXMLValidationErrorDialog
100 //---------------------------------------------------------
101
102 /**
103 Show a dialog displaying the MusicXML validation error(s)
104 and asks the user if he wants to try to load the file anyway.
105 Return QMessageBox::Yes (try anyway) or QMessageBox::No (don't)
106 */
107
musicXMLValidationErrorDialog(QString text,QString detailedText)108 static int musicXMLValidationErrorDialog(QString text, QString detailedText)
109 {
110 QMessageBox errorDialog;
111 errorDialog.setIcon(QMessageBox::Question);
112 errorDialog.setText(text);
113 errorDialog.setInformativeText(QObject::tr("Do you want to try to load this file anyway?"));
114 errorDialog.setDetailedText(detailedText);
115 errorDialog.setStandardButtons(QMessageBox::Yes | QMessageBox::No);
116 errorDialog.setDefaultButton(QMessageBox::No);
117 return errorDialog.exec();
118 }
119
120
121 //---------------------------------------------------------
122 // extractRootfile
123 //---------------------------------------------------------
124
125 /**
126 Extract rootfile from compressed MusicXML file \a qf, return true if OK and false on error.
127 */
128
extractRootfile(QFile * qf,QByteArray & data)129 static bool extractRootfile(QFile* qf, QByteArray& data)
130 {
131 MQZipReader f(qf->fileName());
132 data = f.fileData("META-INF/container.xml");
133
134 QDomDocument container;
135 int line, column;
136 QString err;
137 if (!container.setContent(data, false, &err, &line, &column)) {
138 MScore::lastError = QObject::tr("Error reading container.xml at line %1 column %2: %3\n").arg(line).arg(column).arg(err);
139 return false;
140 }
141
142 // extract first rootfile
143 QString rootfile = "";
144 for (QDomElement e = container.documentElement(); !e.isNull(); e = e.nextSiblingElement()) {
145 if (e.tagName() == "container") {
146 for (QDomElement ee = e.firstChildElement(); !ee.isNull(); ee = ee.nextSiblingElement()) {
147 if (ee.tagName() == "rootfiles") {
148 for (QDomElement eee = ee.firstChildElement(); !eee.isNull(); eee = eee.nextSiblingElement()) {
149 if (eee.tagName() == "rootfile") {
150 if (rootfile == "")
151 rootfile = eee.attribute(QString("full-path"));
152 }
153 else
154 domError(eee);
155 }
156 }
157 else
158 domError(ee);
159 }
160 }
161 else
162 domError(e);
163 }
164
165 if (rootfile == "") {
166 qDebug("can't find rootfile in: %s", qPrintable(qf->fileName()));
167 MScore::lastError = QObject::tr("Can't find rootfile\n%1").arg(qf->fileName());
168 return false;
169 }
170
171 // read the rootfile
172 data = f.fileData(rootfile);
173 return true;
174 }
175
176
177 //---------------------------------------------------------
178 // doValidate
179 //---------------------------------------------------------
180
181 /**
182 Validate MusicXML data from file \a name contained in QIODevice \a dev.
183 */
184
doValidate(const QString & name,QIODevice * dev)185 static Score::FileError doValidate(const QString& name, QIODevice* dev)
186 {
187 //QElapsedTimer t;
188 //t.start();
189
190 // initialize the schema
191 ValidatorMessageHandler messageHandler;
192 QXmlSchema schema;
193 schema.setMessageHandler(&messageHandler);
194 if (!initMusicXmlSchema(schema))
195 return Score::FileError::FILE_BAD_FORMAT; // appropriate error message has been printed by initMusicXmlSchema
196
197 // validate the data
198 QXmlSchemaValidator validator(schema);
199 bool valid = validator.validate(dev, QUrl::fromLocalFile(name));
200 //qDebug("Validation time elapsed: %d ms", t.elapsed());
201
202 if (!valid) {
203 qDebug("importMusicXml() file '%s' is not a valid MusicXML file", qPrintable(name));
204 MScore::lastError = QObject::tr("File '%1' is not a valid MusicXML file").arg(name);
205 if (MScore::noGui)
206 return Score::FileError::FILE_NO_ERROR; // might as well try anyhow in converter mode
207 if (musicXMLValidationErrorDialog(MScore::lastError, messageHandler.getErrors()) != QMessageBox::Yes)
208 return Score::FileError::FILE_USER_ABORT;
209 }
210
211 // return OK
212 return Score::FileError::FILE_NO_ERROR;
213 }
214
215 //---------------------------------------------------------
216 // doValidateAndImport
217 //---------------------------------------------------------
218
219 /**
220 Validate and import MusicXML data from file \a name contained in QIODevice \a dev into score \a score.
221 */
222
doValidateAndImport(Score * score,const QString & name,QIODevice * dev)223 static Score::FileError doValidateAndImport(Score* score, const QString& name, QIODevice* dev)
224 {
225 // verify tuplet TDuration::DurationType dependencies
226 tupletAssert();
227
228 // validate the file
229 Score::FileError res;
230 res = doValidate(name, dev);
231 if (res != Score::FileError::FILE_NO_ERROR)
232 return res;
233
234 // actually do the import
235 importMusicXMLfromBuffer(score, name, dev);
236 //qDebug("importMusicXml() return %d", int(res));
237 return res;
238 }
239
240
241 //---------------------------------------------------------
242 // importMusicXml
243 // return Score::FileError::FILE_* errors
244 //---------------------------------------------------------
245
246 /**
247 Import MusicXML file \a name into the Score.
248 */
249
importMusicXml(MasterScore * score,QIODevice * dev,const QString & name)250 Score::FileError importMusicXml(MasterScore* score, QIODevice* dev, const QString& name)
251 {
252 ScoreLoad sl; // suppress warnings for undo push/pop
253
254 if (!dev->open(QIODevice::ReadOnly)) {
255 qDebug("importMusicXml() could not open MusicXML file '%s'", qPrintable(name));
256 MScore::lastError = QObject::tr("Could not open MusicXML file\n%1").arg(name);
257 return Score::FileError::FILE_OPEN_ERROR;
258 }
259
260 // and import it
261 return doValidateAndImport(score, name, dev);
262 }
263
importMusicXml(MasterScore * score,const QString & name)264 Score::FileError importMusicXml(MasterScore* score, const QString& name) {
265
266 ScoreLoad sl; // suppress warnings for undo push/pop
267
268 //qDebug("importMusicXml(%p, %s)", score, qPrintable(name));
269
270 // open the MusicXML file
271 QFile xmlFile(name);
272 if (!xmlFile.exists())
273 return Score::FileError::FILE_NOT_FOUND;
274 if (!xmlFile.open(QIODevice::ReadOnly)) {
275 qDebug("importMusicXml() could not open MusicXML file '%s'", qPrintable(name));
276 MScore::lastError = QObject::tr("Could not open MusicXML file\n%1").arg(name);
277 return Score::FileError::FILE_OPEN_ERROR;
278 }
279
280 // and import it
281 return doValidateAndImport(score, name, &xmlFile);
282 }
283
284 //---------------------------------------------------------
285 // importCompressedMusicXml
286 // return false on error
287 //---------------------------------------------------------
288
289 /**
290 Import compressed MusicXML file \a name into the Score.
291 */
292
importCompressedMusicXml(MasterScore * score,const QString & name)293 Score::FileError importCompressedMusicXml(MasterScore* score, const QString& name)
294 {
295 //qDebug("importCompressedMusicXml(%p, %s)", score, qPrintable(name));
296
297 // open the compressed MusicXML file
298 QFile mxlFile(name);
299 if (!mxlFile.exists())
300 return Score::FileError::FILE_NOT_FOUND;
301 if (!mxlFile.open(QIODevice::ReadOnly)) {
302 qDebug("importCompressedMusicXml() could not open compressed MusicXML file '%s'", qPrintable(name));
303 MScore::lastError = QObject::tr("Could not open compressed MusicXML file\n%1").arg(name);
304 return Score::FileError::FILE_OPEN_ERROR;
305 }
306
307 // extract the root file
308 QByteArray data;
309 if (!extractRootfile(&mxlFile, data))
310 return Score::FileError::FILE_BAD_FORMAT; // appropriate error message has been printed by extractRootfile
311 QBuffer buffer(&data);
312 buffer.open(QIODevice::ReadOnly);
313
314 // and import it
315 return doValidateAndImport(score, name, &buffer);
316 }
317
318 //---------------------------------------------------------
319 // VoiceDesc
320 //---------------------------------------------------------
321
322 // TODO: move somewhere else
323
VoiceDesc()324 VoiceDesc::VoiceDesc() : _staff(-1), _voice(-1), _overlaps(false)
325 {
326 for (int i = 0; i < MAX_STAVES; ++i) {
327 _chordRests[i] = 0;
328 _staffAlloc[i] = -1;
329 _voices[i] = -1;
330 }
331 }
332
incrChordRests(int s)333 void VoiceDesc::incrChordRests(int s)
334 {
335 if (0 <= s && s < MAX_STAVES)
336 _chordRests[s]++;
337 }
338
numberChordRests() const339 int VoiceDesc::numberChordRests() const
340 {
341 int res = 0;
342 for (int i = 0; i < MAX_STAVES; ++i)
343 res += _chordRests[i];
344 return res;
345 }
346
preferredStaff() const347 int VoiceDesc::preferredStaff() const
348 {
349 int max = 0;
350 int res = 0;
351 for (int i = 0; i < MAX_STAVES; ++i)
352 if (_chordRests[i] > max) {
353 max = _chordRests[i];
354 res = i;
355 }
356 return res;
357 }
358
toString() const359 QString VoiceDesc::toString() const
360 {
361 QString res = "[";
362 for (int i = 0; i < MAX_STAVES; ++i)
363 res += QString(" %1").arg(_chordRests[i]);
364 res += QString(" ] overlaps %1").arg(_overlaps);
365 if (_overlaps) {
366 res += " staffAlloc [";
367 for (int i = 0; i < MAX_STAVES; ++i)
368 res += QString(" %1").arg(_staffAlloc[i]);
369 res += " ] voices [";
370 for (int i = 0; i < MAX_STAVES; ++i)
371 res += QString(" %1").arg(_voices[i]);
372 res += " ]";
373 }
374 else
375 res += QString(" staff %1 voice %2").arg(_staff + 1).arg(_voice + 1);
376 return res;
377 }
378 } // namespace Ms
379
380