From e5d4d522d7db4b1fb48f10cf319a39154532e3d7 Mon Sep 17 00:00:00 2001 From: rettinghaus Date: Thu, 21 May 2026 23:40:15 +0200 Subject: [PATCH 1/4] export images --- src/engraving/dom/imageStore.h | 1 + .../internal/export/exportmusicxml.cpp | 117 +++++++++++++++++- 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/src/engraving/dom/imageStore.h b/src/engraving/dom/imageStore.h index 9957f2573435b..4d621b0cc56ac 100644 --- a/src/engraving/dom/imageStore.h +++ b/src/engraving/dom/imageStore.h @@ -56,6 +56,7 @@ class ImageStoreItem bool isUsed() const { return !m_references.empty(); } std::string hashName() const; const muse::ByteArray& hash() const { return m_hash; } + const std::string& type() const { return m_type; } void set(const muse::ByteArray& b, const muse::ByteArray& h) { m_buffer = b; m_hash = h; } private: diff --git a/src/importexport/musicxml/internal/export/exportmusicxml.cpp b/src/importexport/musicxml/internal/export/exportmusicxml.cpp index 5673886f430b2..8844a4ec966e2 100644 --- a/src/importexport/musicxml/internal/export/exportmusicxml.cpp +++ b/src/importexport/musicxml/internal/export/exportmusicxml.cpp @@ -76,6 +76,8 @@ #include "engraving/dom/hammeronpulloff.h" #include "engraving/dom/harmonicmark.h" #include "engraving/dom/harmony.h" +#include "engraving/dom/image.h" +#include "engraving/dom/imageStore.h" #include "engraving/dom/harppedaldiagram.h" #include "engraving/dom/instrchange.h" #include "engraving/dom/jump.h" @@ -347,6 +349,7 @@ class ExportMusicXml : public muse::Contextable void moveToTickIfNeed(const Fraction& t, track_idx_t track, const Fraction& measureTick); void words(TextBase const* const text, staff_idx_t staff); void tboxTextAsWords(TextBase const* const text, const staff_idx_t staff, PointF position); + void image(const Image* const img, staff_idx_t staff); void rehearsal(RehearsalMark const* const rmk, staff_idx_t staff); void harpPedals(HarpPedalDiagram const* const hpd, staff_idx_t staff); void hairpin(Hairpin const* const hp, staff_idx_t staff, const Fraction& tick); @@ -5111,7 +5114,7 @@ void ExportMusicXml::words(TextBase const* const text, staff_idx_t staff) muPrintable(text->plainText())); */ - if (text->plainText() == "") { + if (text->plainText() == u"") { // sometimes empty Texts are present, exporting would result // in invalid MusicXML (as an empty direction-type would be created) return; @@ -5123,6 +5126,107 @@ void ExportMusicXml::words(TextBase const* const text, staff_idx_t staff) directionETag(m_xml, staff); } +//--------------------------------------------------------- +// getImageInfo +//--------------------------------------------------------- + +static void getImageInfo(const ImageStoreItem* isi, String& source, String& type) +{ + source = String::fromStdString(isi->hashName()); + String suffix = String::fromStdString(isi->type()).toLower(); + + if (suffix == u"svg" || suffix == u"svgz") { + type = u"image/svg+xml"; + } else if (suffix == u"png") { + type = u"image/png"; + } else if (suffix == u"jpg" || suffix == u"jpeg") { + type = u"image/jpeg"; + } else if (suffix == u"gif") { + type = u"image/gif"; + } else if (suffix == u"bmp") { + type = u"image/bmp"; + } else if (suffix == u"tif" || suffix == u"tiff") { + type = u"image/tiff"; + } + + if (type.isEmpty()) { + const ByteArray& ba = isi->buffer(); + if (ba.size() >= 4) { + if (ba.at(0) == 0x89 && ba.at(1) == 0x50 && ba.at(2) == 0x4E && ba.at(3) == 0x47) { + type = u"image/png"; + if (suffix.isEmpty()) { + source += u".png"; + } + } else if (ba.at(0) == 0xFF && ba.at(1) == 0xD8 && ba.at(2) == 0xFF) { + type = u"image/jpeg"; + if (suffix.isEmpty()) { + source += u".jpg"; + } + } else if (ba.at(0) == 0x47 && ba.at(1) == 0x49 && ba.at(2) == 0x46 && ba.at(3) == 0x38) { + type = u"image/gif"; + if (suffix.isEmpty()) { + source += u".gif"; + } + } else if (ba.at(0) == 0x42 && ba.at(1) == 0x4D) { + type = u"image/bmp"; + if (suffix.isEmpty()) { + source += u".bmp"; + } + } else if (ba.at(0) == 0x3C && (ba.at(1) == 0x3F || ba.at(1) == 0x73)) { + type = u"image/svg+xml"; + if (suffix.isEmpty()) { + source += u".svg"; + } + } + } + } + + if (type.isEmpty()) { + type = u"application/octet-stream"; + } +} + +//--------------------------------------------------------- +// image +//--------------------------------------------------------- + +void ExportMusicXml::image(const Image* const img, staff_idx_t staff) +{ + ImageStoreItem* isi = img->storeItem(); + if (!isi) { + return; + } + + directionTag(m_xml, m_attr, img); + m_xml.startElement("direction-type"); + + String source; + String type; + getImageInfo(isi, source, type); + + String imgTag = u"image source=\"" + XmlWriter::xmlString(source) + u"\""; + imgTag += u" type=\"" + XmlWriter::xmlString(type) + u"\""; + + double width = img->imageWidth(); + double height = img->imageHeight(); + if (!img->sizeIsSpatium()) { + double sp = img->spatium(); + if (sp > 0.0) { + width /= sp; + height /= sp; + } + } + + imgTag += u" height=\"" + String::number(height * 10.0, 2) + u"\""; + imgTag += u" width=\"" + String::number(width * 10.0, 2) + u"\""; + imgTag += positioningAttributes(img); + + m_xml.tagRaw(imgTag); + m_xml.endElement(); // direction-type + + directionETag(m_xml, staff); +} + //--------------------------------------------------------- // systemText //--------------------------------------------------------- @@ -6525,6 +6629,8 @@ static bool commonAnnotations(ExportMusicXml* exp, const EngravingItem* e, staff exp->harpPedals(toHarpPedalDiagram(e), sstaff); } else if (e->isRehearsalMark()) { exp->rehearsal(toRehearsalMark(e), sstaff); + } else if (e->isImage()) { + exp->image(toImage(e), sstaff); } else if (e->isSystemText()) { exp->systemText(toStaffTextBase(e), sstaff); } @@ -8850,6 +8956,15 @@ static void writeMxlArchive(Score* score, muse::ZipWriter& zip, const String& fi em.write(&dbuf); dbuf.seek(0); zip.addFile(filename.toStdString(), dbuf.data()); + + for (const ImageStoreItem* isi : imageStore) { + if (isi->isUsed(score)) { + String source; + String type; + getImageInfo(isi, source, type); + zip.addFile(source.toStdString(), isi->buffer()); + } + } } bool saveMxl(Score* score, IODevice* device) From 96ac90d0e714f399a64a73ca4e734e2e8f0718bb Mon Sep 17 00:00:00 2001 From: rettinghaus Date: Fri, 22 May 2026 11:55:37 +0200 Subject: [PATCH 2/4] add member for mxl conditions --- .../internal/export/exportmusicxml.cpp | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/importexport/musicxml/internal/export/exportmusicxml.cpp b/src/importexport/musicxml/internal/export/exportmusicxml.cpp index 8844a4ec966e2..a2bb8583d9370 100644 --- a/src/importexport/musicxml/internal/export/exportmusicxml.cpp +++ b/src/importexport/musicxml/internal/export/exportmusicxml.cpp @@ -364,6 +364,7 @@ class ExportMusicXml : public muse::Contextable void tempoSound(TempoText const* const text); void harmony(Harmony const* const, FretDiagram const* const fd, const Fraction& offset = Fraction(0, 1)); Score* score() const { return m_score; } + void setMxl(bool v) { m_isMxl = v; } double getTenthsFromInches(double) const; double getTenthsFromDots(double) const; Fraction tick() const { return m_tick; } @@ -441,6 +442,7 @@ class ExportMusicXml : public muse::Contextable TrillHash m_trillStop; MusicXmlInstrumentMap m_instrMap; PlayingTechniqueType m_currPlayTechnique; + bool m_isMxl = false; }; //--------------------------------------------------------- @@ -5204,24 +5206,30 @@ void ExportMusicXml::image(const Image* const img, staff_idx_t staff) String type; getImageInfo(isi, source, type); - String imgTag = u"image source=\"" + XmlWriter::xmlString(source) + u"\""; - imgTag += u" type=\"" + XmlWriter::xmlString(type) + u"\""; + if (m_isMxl) { + String imgTag = u"image source=\"" + XmlWriter::xmlString(source) + u"\""; + imgTag += u" type=\"" + XmlWriter::xmlString(type) + u"\""; - double width = img->imageWidth(); - double height = img->imageHeight(); - if (!img->sizeIsSpatium()) { - double sp = img->spatium(); - if (sp > 0.0) { - width /= sp; - height /= sp; + double width = img->imageWidth(); + double height = img->imageHeight(); + if (!img->sizeIsSpatium()) { + double sp = img->spatium(); + if (sp > 0.0) { + width /= sp; + height /= sp; + } } - } - imgTag += u" height=\"" + String::number(height * 10.0, 2) + u"\""; - imgTag += u" width=\"" + String::number(width * 10.0, 2) + u"\""; - imgTag += positioningAttributes(img); + imgTag += u" height=\"" + String::number(height * 10.0, 2) + u"\""; + imgTag += u" width=\"" + String::number(width * 10.0, 2) + u"\""; + imgTag += positioningAttributes(img); - m_xml.tagRaw(imgTag); + m_xml.tagRaw(imgTag); + } else { + String otherTag = u"other-direction"; + otherTag += positioningAttributes(img); + m_xml.tagRaw(otherTag, XmlWriter::xmlString(source)); + } m_xml.endElement(); // direction-type directionETag(m_xml, staff); @@ -8953,6 +8961,7 @@ static void writeMxlArchive(Score* score, muse::ZipWriter& zip, const String& fi auto dbuf = Buffer::opened(IODevice::ReadWrite); ExportMusicXml em(score); + em.setMxl(true); em.write(&dbuf); dbuf.seek(0); zip.addFile(filename.toStdString(), dbuf.data()); From 5a1bf4df4a64780dfb8b6144910922c4088b3baf Mon Sep 17 00:00:00 2001 From: rettinghaus Date: Fri, 22 May 2026 12:39:13 +0200 Subject: [PATCH 3/4] unify check for empty strings --- .../musicxml/internal/export/exportmusicxml.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/importexport/musicxml/internal/export/exportmusicxml.cpp b/src/importexport/musicxml/internal/export/exportmusicxml.cpp index a2bb8583d9370..cb3f7712178ba 100644 --- a/src/importexport/musicxml/internal/export/exportmusicxml.cpp +++ b/src/importexport/musicxml/internal/export/exportmusicxml.cpp @@ -5067,7 +5067,7 @@ void ExportMusicXml::playText(PlayTechAnnotation const* const annot, staff_idx_t { const int offset = calculateTimeDeltaInDivisions(annot->tick(), tick(), m_div); - if (annot->plainText() == "") { + if (annot->plainText().empty()) { // sometimes empty Texts are present, exporting would result // in invalid MusicXML (as an empty direction-type would be created) return; @@ -5116,7 +5116,7 @@ void ExportMusicXml::words(TextBase const* const text, staff_idx_t staff) muPrintable(text->plainText())); */ - if (text->plainText() == u"") { + if (text->plainText().empty()) { // sometimes empty Texts are present, exporting would result // in invalid MusicXML (as an empty direction-type would be created) return; @@ -5243,7 +5243,7 @@ void ExportMusicXml::systemText(StaffTextBase const* const text, staff_idx_t sta { const int offset = calculateTimeDeltaInDivisions(text->tick(), tick(), m_div); - if (text->plainText() == "") { + if (text->plainText().empty()) { // sometimes empty Texts are present, exporting would result // in invalid MusicXML (as an empty direction-type would be created) return; @@ -5279,7 +5279,7 @@ String ExportMusicXml::positioningAttributesForTboxText(const PointF position, f void ExportMusicXml::tboxTextAsWords(TextBase const* const text, const staff_idx_t staff, const PointF relativePosition) { - if (text->plainText() == "") { + if (text->plainText().empty()) { // sometimes empty Texts are present, exporting would result // in invalid MusicXML (as an empty direction-type would be created) return; @@ -5308,7 +5308,7 @@ void ExportMusicXml::tboxTextAsWords(TextBase const* const text, const staff_idx void ExportMusicXml::rehearsal(RehearsalMark const* const rmk, staff_idx_t staff) { - if (rmk->plainText() == "") { + if (rmk->plainText().empty()) { // sometimes empty Texts are present, exporting would result // in invalid MusicXML (as an empty direction-type would be created) return; @@ -6300,7 +6300,7 @@ static void directionMarker(XmlWriter& xml, const Marker* const m, const std::ve case MarkerType::CODA: case MarkerType::CODETTA: type = u"coda"; - if (m->label() == "") { + if (m->label().empty()) { sound = u"coda=\"1\""; } else { sound = u"coda=\"" + m->label() + u"\""; @@ -6311,7 +6311,7 @@ static void directionMarker(XmlWriter& xml, const Marker* const m, const std::ve [[fallthrough]]; case MarkerType::SEGNO: type = u"segno"; - if (m->label() == "") { + if (m->label().empty()) { sound = u"segno=\"1\""; } else { sound = u"segno=\"" + m->label() + u"\""; @@ -6325,7 +6325,7 @@ static void directionMarker(XmlWriter& xml, const Marker* const m, const std::ve case MarkerType::TOCODASYM: case MarkerType::DA_CODA: case MarkerType::DA_DBLCODA: { - if (m->xmlText() == "") { + if (m->xmlText().empty()) { words = u"To Coda"; } else { words = m->xmlText(); From d5cab93bae06c8cee57a7c1d107bbec8aefe5453 Mon Sep 17 00:00:00 2001 From: rettinghaus Date: Fri, 22 May 2026 15:30:20 +0200 Subject: [PATCH 4/4] add magic byte detection for tiff --- .../musicxml/internal/export/exportmusicxml.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/importexport/musicxml/internal/export/exportmusicxml.cpp b/src/importexport/musicxml/internal/export/exportmusicxml.cpp index cb3f7712178ba..806330b20ad25 100644 --- a/src/importexport/musicxml/internal/export/exportmusicxml.cpp +++ b/src/importexport/musicxml/internal/export/exportmusicxml.cpp @@ -5174,6 +5174,12 @@ static void getImageInfo(const ImageStoreItem* isi, String& source, String& type if (suffix.isEmpty()) { source += u".bmp"; } + } else if ((ba.at(0) == 0x49 && ba.at(1) == 0x49 && ba.at(2) == 0x2A && ba.at(3) == 0x00) + || (ba.at(0) == 0x4D && ba.at(1) == 0x4D && ba.at(2) == 0x00 && ba.at(3) == 0x2A)) { + type = u"image/tiff"; + if (suffix.isEmpty()) { + source += u".tif"; + } } else if (ba.at(0) == 0x3C && (ba.at(1) == 0x3F || ba.at(1) == 0x73)) { type = u"image/svg+xml"; if (suffix.isEmpty()) {