diff --git a/res/controllers/engine-api.d.ts b/res/controllers/engine-api.d.ts index f406a745a01..d83bfa649e2 100644 --- a/res/controllers/engine-api.d.ts +++ b/res/controllers/engine-api.d.ts @@ -301,4 +301,35 @@ declare namespace engine { * SoftStart with low factors would take a while until sound is audible. [default = 1.0] */ function softStart(deck: number, activate: boolean, factor?: number): void; + + enum WellKnownCharsets { + US_ASCII, + Latin1, + ISO_8859_1, + Latin9, + ISO_8859_15, + UCS2, // with prepended Byte-Order-Mark + ISO_10646_UCS_2, // with prepended Byte-Order-Mark + UTF_8, + UTF_16BE, + UTF_16LE, + } + + /** + * Converts a string into another charset. + * + * This function is useful to display text on a device that does not make use of UTF-8. + * Available charset names are listed here: http://www.iana.org/assignments/character-sets/character-sets.xhtml. + * Characters that are unsupported by target charset will be transformed to null character (0x00). + * @param targetCharset The charset to encode the string into. + * @param value The string to encode + * @returns The converted String as an array of bytes. Will return an empty buffer on conversion error or unavailable charset. + */ + function convertCharset(targetCharset: string, value: string): ArrayBuffer + + /** + * @param value The string to encode + * @returns The converted String as an array of bytes. Will return an empty buffer on conversion error or unavailable charset. + */ + function convertCharset(targetCharset: WellKnownCharsets, value: string): ArrayBuffer } diff --git a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp index 805cc90012e..f1d7ad607a4 100644 --- a/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp +++ b/src/controllers/scripting/legacy/controllerscriptenginelegacy.cpp @@ -338,8 +338,10 @@ bool ControllerScriptEngineLegacy::initialize() { ControllerScriptInterfaceLegacy* legacyScriptInterface = new ControllerScriptInterfaceLegacy(this, m_logger); - engineGlobalObject.setProperty( - "engine", m_pJSEngine->newQObject(legacyScriptInterface)); + auto engine = m_pJSEngine->newQObject(legacyScriptInterface); + auto meta = m_pJSEngine->newQMetaObject(&ControllerScriptInterfaceLegacy::staticMetaObject); + engine.setProperty("WellKnownCharsets", meta); + engineGlobalObject.setProperty("engine", m_pJSEngine->newQObject(legacyScriptInterface)); #ifdef MIXXX_USE_QML if (m_bQmlMode) { diff --git a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp index 2093cb7ffca..4b32cdbe76e 100644 --- a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.cpp @@ -1,5 +1,10 @@ #include "controllerscriptinterfacelegacy.h" +#if QT_VERSION < QT_VERSION_CHECK(6, 4, 0) +#include +#else +#include +#endif #include #include "control/controlobject.h" @@ -1052,3 +1057,45 @@ void ControllerScriptInterfaceLegacy::softStart(int deck, bool activate, double // activate the ramping in scratchProcess() m_ramp[deck] = true; } + +QByteArray ControllerScriptInterfaceLegacy::convertCharset( + const ControllerScriptInterfaceLegacy::WellKnownCharsets targetCharset, + const QString& value) { + switch (targetCharset) { + case WellKnownCharsets::US_ASCII: + return convertCharsetInternal(QStringLiteral("US-ASCII"), value); + case WellKnownCharsets::Latin1: + case WellKnownCharsets::ISO_8859_1: + return convertCharsetInternal(QStringLiteral("ISO-8859-1"), value); + case WellKnownCharsets::Latin9: + case WellKnownCharsets::ISO_8859_15: + return convertCharsetInternal(QStringLiteral("ISO-8859-15"), value); + case WellKnownCharsets::UCS2: + case WellKnownCharsets::ISO_10646_UCS_2: + return convertCharsetInternal(QStringLiteral("ISO-10646-UCS-2"), value); + case WellKnownCharsets::UTF_8: + return convertCharsetInternal(QStringLiteral("UTF-8"), value); + case WellKnownCharsets::UTF_16BE: + return convertCharsetInternal(QStringLiteral("UTF-16BE"), value); + case WellKnownCharsets::UTF_16LE: + return convertCharsetInternal(QStringLiteral("UTF-16LE"), value); + } + m_pScriptEngineLegacy->logOrThrowError(QStringLiteral("Unknown charset specified")); + return QByteArray(); +} + +QByteArray ControllerScriptInterfaceLegacy::convertCharsetInternal( + const QString& targetCharset, const QString& value) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0) + QAnyStringView encoderName = QAnyStringView(targetCharset); +#else + QByteArray encoderNameArray = targetCharset.toUtf8(); + const char* encoderName = encoderNameArray.constData(); +#endif + QStringEncoder fromUtf16 = QStringEncoder(encoderName); + if (!fromUtf16.isValid()) { + m_pScriptEngineLegacy->logOrThrowError(QStringLiteral("Unable to open encoder")); + return QByteArray(); + } + return fromUtf16(value); +} diff --git a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h index b83ca2fa296..6184723780f 100644 --- a/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h +++ b/src/controllers/scripting/legacy/controllerscriptinterfacelegacy.h @@ -17,6 +17,20 @@ class ConfigKey; class ControllerScriptInterfaceLegacy : public QObject { Q_OBJECT public: + enum class WellKnownCharsets { + US_ASCII, + Latin1, + ISO_8859_1, + Latin9, + ISO_8859_15, + UCS2, + ISO_10646_UCS_2, + UTF_8, + UTF_16BE, + UTF_16LE, + }; + Q_ENUM(WellKnownCharsets) + ControllerScriptInterfaceLegacy(ControllerScriptEngineLegacy* m_pEngine, const RuntimeLoggingCategory& logger); @@ -72,6 +86,11 @@ class ControllerScriptInterfaceLegacy : public QObject { const double rate = -10.0); Q_INVOKABLE void softStart(const int deck, bool activate, double factor = 1.0); + Q_INVOKABLE QByteArray convertCharset( + const ControllerScriptInterfaceLegacy::WellKnownCharsets + targetCharset, + const QString& value); + bool removeScriptConnection(const ScriptConnection& conn); /// Execute a ScriptConnection's JS callback void triggerScriptConnection(const ScriptConnection& conn); @@ -84,6 +103,9 @@ class ControllerScriptInterfaceLegacy : public QObject { const QString& name, const QJSValue& callback, bool skipSuperseded = false); + + QByteArray convertCharsetInternal(const QString& targetCharset, const QString& value); + QHash m_controlCache; ControlObjectScript* getControlObjectScript(const QString& group, const QString& name); diff --git a/src/test/controllerscriptenginelegacy_test.cpp b/src/test/controllerscriptenginelegacy_test.cpp index aa88263fc63..9d48025e681 100644 --- a/src/test/controllerscriptenginelegacy_test.cpp +++ b/src/test/controllerscriptenginelegacy_test.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include #include #include @@ -12,6 +14,7 @@ #include "control/controlobject.h" #include "control/controlpotmeter.h" +#include "controllers/scripting/legacy/controllerscriptinterfacelegacy.h" #ifdef MIXXX_USE_QML #include @@ -658,6 +661,84 @@ TEST_F(ControllerScriptEngineLegacyTest, connectionExecutesWithCorrectThisObject EXPECT_DOUBLE_EQ(1.0, pass->get()); } +TEST_F(ControllerScriptEngineLegacyTest, convertCharsetUndefinedOnUnknownCharset) { + const auto result = evaluate("engine.convertCharset('NULL', 'Hello!')"); + + EXPECT_EQ(qjsvalue_cast(result), QByteArrayView("")); +} + +TEST_F(ControllerScriptEngineLegacyTest, convertCharsetCorrectValueWellKnown) { + const auto result = evaluate( + "engine.convertCharset(engine.WellKnownCharsets.Latin9, 'Hello!')"); + + // ISO-8859-15 ecoded 'Hello!' + EXPECT_EQ(qjsvalue_cast(result), + QByteArrayView::fromArray({'\x48', '\x65', '\x6c', '\x6c', '\x6f', '\x21'})); +} + +TEST_F(ControllerScriptEngineLegacyTest, convertCharsetCorrectValueStringCharset) { + const auto result = evaluate("engine.convertCharset('ISO-8859-15', 'Hello!')"); + + // ISO-8859-15 ecoded 'Hello!' + EXPECT_EQ(qjsvalue_cast(result), + QByteArrayView::fromArray({'\x48', '\x65', '\x6c', '\x6c', '\x6f', '\x21'})); +} + +TEST_F(ControllerScriptEngineLegacyTest, convertCharsetUnsupportedChars) { + auto result = qjsvalue_cast( + evaluate("engine.convertCharset('ISO-8859-15', 'مايأ نامز')")); + char sub = '\x1A'; // ASCII/Latin9 SUB character + EXPECT_EQ(result, + QByteArrayView::fromArray( + {sub, sub, sub, sub, '\x20', sub, sub, sub, sub})); +} + +#define COMPLICATEDSTRINGLITERAL "Hello, 世界! שלום! こんにちは! 안녕하세요! 😊" + +static int convertedCharsetForString(ControllerScriptInterfaceLegacy::WellKnownCharsets charset) { + // the expected length after conversion of COMPLICATEDSTRINGLITERAL + using enum ControllerScriptInterfaceLegacy::WellKnownCharsets; + switch (charset) { + case US_ASCII: + case Latin9: + case ISO_8859_15: + return 32; + case Latin1: + case ISO_8859_1: + return 33; + case UTF_8: + return 63; + case UTF_16BE: + case UTF_16LE: + return 66; + case UCS2: + case ISO_10646_UCS_2: + return 68; + } + // unreachable (TODO assert false?) + return 0; +} + +TEST_F(ControllerScriptEngineLegacyTest, convertCharsetAllWellKnownCharsets) { + QMetaEnum charsetEnumEntry = QMetaEnum::fromType< + ControllerScriptInterfaceLegacy::WellKnownCharsets>(); + + for (int i = 0; i < charsetEnumEntry.keyCount(); ++i) { + QString key = charsetEnumEntry.key(i); + auto enumValue = + static_cast( + charsetEnumEntry.value(i)); + QString source = QStringLiteral( + "engine.convertCharset(engine.WellKnownCharsets.%1, " + "'" COMPLICATEDSTRINGLITERAL "')") + .arg(key); + auto result = qjsvalue_cast(evaluate(source)); + EXPECT_EQ(result.size(), convertedCharsetForString(enumValue)) + << "Unexpected length of converted string for encoding: '" + << key.toStdString() << "'"; + } +} + #ifdef MIXXX_USE_QML class MockScreenRender : public ControllerRenderingEngine { public: