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