diff --git a/locales/en/messages.json b/locales/en/messages.json index 3c86f417..660548cb 100644 --- a/locales/en/messages.json +++ b/locales/en/messages.json @@ -2786,19 +2786,28 @@ "osdSetupOpenFont": { "message": "Open Font File" }, - "osdSetupLogo": { - "message": "Logo in font:" + "osdSetupCustomLogoTitle": { + "message": "Boot logo:" }, - "osdSetupReplaceLogo": { - "message": "Replace Logo" + "osdSetupCustomLogoOpenImageButton": { + "message": "Select custom image…" }, - "osdSetupReplaceLogoHelp": { - "message": "Customized logo image has to be 288×72 pixels in size containing black and white pixels only on a completely green background." + "osdSetupCustomLogoInfoTitle": { + "message": "Custom image:" }, - "osdSetupReplaceLogoImageSizeError": { - "message": "Invalid image size; expected $1×$2 instead of $3×$4" + "osdSetupCustomLogoInfoImageSize": { + "message": "Size must be $t(logoWidthPx)×$t(logoHeightPx) pixels" }, - "osdSetupReplaceLogoImageColorsError": { + "osdSetupCustomLogoInfoColorMap": { + "message": "Must contain green, black and white pixels" + }, + "osdSetupCustomLogoInfoUploadHint": { + "message": "Click $t(osdSetupUploadFont.message) to persist custom logo" + }, + "osdSetupCustomLogoImageSizeError": { + "message": "Invalid image size: $1×$2 (expected $t(logoWidthPx)×$t(logoHeightPx))" + }, + "osdSetupCustomLogoColorMapError": { "message": "The image contains an invalid color palette (only green, black and white are allowed)" }, "osdSetupUploadFont": { diff --git a/src/css/tabs/osd.css b/src/css/tabs/osd.css index 5f4d4ea2..f5686a39 100644 --- a/src/css/tabs/osd.css +++ b/src/css/tabs/osd.css @@ -336,11 +336,42 @@ #font-logo-preview-container { background:rgba(0, 255, 0, 0.4); margin-bottom: 10px; + width: 50%; + float: left; } #font-logo-preview { background:rgba(0, 255, 0, 1); - line-height: 0; + line-height: 0; + margin: auto; +} + +#font-logo-info { + margin-left: 18px; + font-size: 125%; + line-height: 150%; + float: left; +} + +#font-logo-info h3 { + margin-bottom: 0.2em; +} + +#font-logo-info ul li:before { + content: '• '; +} + +#font-logo-info ul li.valid:before { + content: '✔ '; +} + +#font-logo-info ul li.invalid:before { + content: '🞨 '; +} + +#font-logo-info #font-logo-info-upload-hint { + margin-top: 1em; + display: none; } .tab-osd .content_wrapper { diff --git a/src/js/tabs/osd.js b/src/js/tabs/osd.js index ce9a0b03..91e91ad5 100755 --- a/src/js/tabs/osd.js +++ b/src/js/tabs/osd.js @@ -76,20 +76,6 @@ FONT.constants = { // white 2: 'rgba(255,255,255, 1)' }, - LOGO: { - TILES_NUM_HORIZ: 24, - TILES_NUM_VERT: 4, - MCM_COLORMAP: { - // background - '0-255-0': '01', - // black - '0-0-0': '00', - // white - '255-255-255': '10', - // fallback - 'default': '01', - }, - }, }; /** @@ -101,6 +87,8 @@ FONT.parseMCMFontFile = function(data) { FONT.data.characters.length = 0; FONT.data.characters_bytes.length = 0; FONT.data.character_image_urls.length = 0; + // reset logo image info when font data is changed + LogoManager.resetImageInfo(); // make sure the font file is valid if (data.shift().trim() != 'MAX7456') { var msg = 'that font file doesnt have the MAX7456 header, giving up'; @@ -163,74 +151,25 @@ FONT.openFontFile = function($preview) { }); }; -// show a file open dialog and yield an Image object -var openLogoImage = function() { - return new Promise(function(resolve, reject) { - var validateImage = function(img) { - return new Promise(function(resolve, reject) { - var expectedWidth = FONT.constants.SIZES.CHAR_WIDTH - * FONT.constants.LOGO.TILES_NUM_HORIZ, - expectedHeight = FONT.constants.SIZES.CHAR_HEIGHT - * FONT.constants.LOGO.TILES_NUM_VERT; - if (img.width != expectedWidth || img.height != expectedHeight) { - reject(i18n.getMessage("osdSetupReplaceLogoImageSizeError", - [expectedWidth, expectedHeight, img.width, img.height])); - return; - } - var canvas = document.createElement('canvas'), - ctx = canvas.getContext('2d'); - canvas.width = img.width; - canvas.height = img.height; - ctx.drawImage(img, 0, 0); - for (var y = 0, Y = canvas.height; y < Y; y++) { - for (var x = 0, X = canvas.width; x < X; x++) { - var rgbPixel = ctx.getImageData(x, y, 1, 1).data.slice(0, 3), - colorKey = rgbPixel.join("-"); - if (!FONT.constants.LOGO.MCM_COLORMAP[colorKey]) { - reject(i18n.getMessage("osdSetupReplaceLogoImageColorsError")); - return; - } - } - } - resolve(); - }); - }; - - chrome.fileSystem.chooseEntry({ type: 'openFile', accepts: [{ extensions: ['png', 'bmp'] }] }, function(fileEntry) { - if (chrome.runtime.lastError) { - console.error(chrome.runtime.lastError.message); - return; - } - var img = new Image(); - img.onload = function() { - validateImage(img).then(function() { - resolve(img); - }).catch(function(error) { - console.error(error); - reject(error); - }); - }; - img.onerror = function(error) { - reject(error); - }; - fileEntry.file(function(file) { - img.src = "file://" + file.path; - }); - }); - }); -}; - -// replaces the logo in the font based on an Image object +/** + * Replaces the logo in the loaded font based on an image. + * + * @param {HTMLImageElement} img + */ FONT.replaceLogoFromImage = function(img) { - // takes image data from an ImageData object and returns an MCM symbol as an array of strings + /** + * Takes an ImageData object and returns an MCM symbol as an array of strings. + * + * @param {ImageData} data + */ var imageToCharacter = function(data) { var char = [], line = ""; for (var i = 0, I = data.length; i < I; i += 4) { var rgbPixel = data.slice(i, i + 3), colorKey = rgbPixel.join("-"); - line += FONT.constants.LOGO.MCM_COLORMAP[colorKey] - || FONT.constants.LOGO.MCM_COLORMAP['default']; + line += LogoManager.constants.MCM_COLORMAP[colorKey] + || LogoManager.constants.MCM_COLORMAP['default']; if (line.length == 8) { char.push(line); line = ""; @@ -238,13 +177,12 @@ FONT.replaceLogoFromImage = function(img) { } var fieldSize = FONT.constants.SIZES.MAX_NVM_FONT_CHAR_FIELD_SIZE; if (char.length < fieldSize) { - var pad = FONT.constants.LOGO.MCM_COLORMAP['default'].repeat(4); + var pad = LogoManager.constants.MCM_COLORMAP['default'].repeat(4); for (var i = 0, I = fieldSize - char.length; i < I; i++) char.push(pad); } return char; }; - // takes an OSD symbol as an array of strings and replaces the in-memory character at charAddress with it var replaceChar = function(lines, charAddress) { var characterBits = []; @@ -262,7 +200,6 @@ FONT.replaceLogoFromImage = function(img) { FONT.data.character_image_urls[charAddress] = null; FONT.draw(charAddress); }; - // loop through an image and replace font symbols var canvas = document.createElement('canvas'), ctx = canvas.getContext('2d'), @@ -270,8 +207,8 @@ FONT.replaceLogoFromImage = function(img) { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); - for (var y = 0; y < FONT.constants.LOGO.TILES_NUM_VERT; y++) { - for (var x = 0; x < FONT.constants.LOGO.TILES_NUM_HORIZ; x++) { + for (var y = 0; y < LogoManager.constants.TILES_NUM_VERT; y++) { + for (var x = 0; x < LogoManager.constants.TILES_NUM_HORIZ; x++) { var imageData = ctx.getImageData( x * FONT.constants.SIZES.CHAR_WIDTH, y * FONT.constants.SIZES.CHAR_HEIGHT, @@ -285,6 +222,220 @@ FONT.replaceLogoFromImage = function(img) { } }; +var LogoManager = LogoManager || { + // DOM elements to cache + elements: { + $preview: "#font-logo-preview", + $uploadHint: "#font-logo-info-upload-hint", + }, + constants: { + TILES_NUM_HORIZ: 24, + TILES_NUM_VERT: 4, + MCM_COLORMAP: { + // background + '0-255-0': '01', + // black + '0-0-0': '00', + // white + '255-255-255': '10', + // fallback + 'default': '01', + }, + }, + // config for logo image selection dialog + acceptFileTypes: [ + { extensions: ['png', 'bmp'] }, + ], +}; + +// custom logo image constraints +LogoManager.constraints = { + // test for image size + imageSize: { + $el: "#font-logo-info-size", + // calculate logo image size at runtime as it may change conditionally in the future + expectedWidth: FONT.constants.SIZES.CHAR_WIDTH + * LogoManager.constants.TILES_NUM_HORIZ, + expectedHeight: FONT.constants.SIZES.CHAR_HEIGHT + * LogoManager.constants.TILES_NUM_VERT, + /** + * @param {HTMLImageElement} img + */ + test: function(img) { + var constraint = LogoManager.constraints.imageSize; + if (img.width != constraint.expectedWidth + || img.height != constraint.expectedHeight) { + GUI.log(i18n.getMessage("osdSetupCustomLogoImageSizeError", [ + img.width, + img.height, + ])); + return false; + } + return true; + }, + }, + // test for pixel colors + colorMap: { + $el: "#font-logo-info-colors", + /** + * @param {HTMLImageElement} img + */ + test: function(img) { + var canvas = document.createElement('canvas'), + ctx = canvas.getContext('2d'); + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + for (var y = 0, Y = canvas.height; y < Y; y++) { + for (var x = 0, X = canvas.width; x < X; x++) { + var rgbPixel = ctx.getImageData(x, y, 1, 1).data.slice(0, 3), + colorKey = rgbPixel.join("-"); + if (!LogoManager.constants.MCM_COLORMAP[colorKey]) { + GUI.log(i18n.getMessage("osdSetupCustomLogoColorMapError")); + return false; + } + } + } + return true; + }, + }, +}; + +LogoManager.resetImageInfo = function() { + LogoManager.hideUploadHint(); + Object.values(LogoManager.constraints).forEach(function(constraint) { + var $el = constraint.$el; + $el.toggleClass("message-negative", false); + $el.toggleClass("invalid", false); + $el.toggleClass("message-positive", false); + $el.toggleClass("valid", false); + }); +}; + +LogoManager.showConstraintNotSatisfied = function(constraint) { + constraint.$el.toggleClass("message-negative", true); + constraint.$el.toggleClass("invalid", true); +}; + +LogoManager.showConstraintSatisfied = function(constraint) { + constraint.$el.toggleClass("message-positive", true); + constraint.$el.toggleClass("valid", true); +}; + +LogoManager.showUploadHint = function() { + LogoManager.elements.$uploadHint.show(); +}; + +LogoManager.hideUploadHint = function() { + LogoManager.elements.$uploadHint.hide(); +}; + +/** + * Show a file open dialog and resolve to an Image object. + * + * @returns {Promise} + */ +LogoManager.openImage = function() { + return new Promise(function(resolve, reject) { + /** + * Validate image using defined constraints and display results on the UI. + * + * @param {HTMLImageElement} img + */ + var validateImage = function(img) { + return new Promise(function(resolve, reject) { + LogoManager.resetImageInfo(); + for (var key in LogoManager.constraints) { + if (!LogoManager.constraints.hasOwnProperty(key)) { + continue; + } + var constraint = LogoManager.constraints[key], + satisfied = constraint.test(img); + if (satisfied) { + LogoManager.showConstraintSatisfied(constraint); + } else { + LogoManager.showConstraintNotSatisfied(constraint); + reject(); + return; + } + } + resolve(); + }); + }, + dialogOptions = { + type: 'openFile', + accepts: LogoManager.acceptFileTypes + }; + + chrome.fileSystem.chooseEntry(dialogOptions, function(fileEntry) { + if (chrome.runtime.lastError) { + console.error(chrome.runtime.lastError.message); + return; + } + // load and validate selected image + var img = new Image(); + img.onload = function() { + validateImage(img).then(function() { + resolve(img); + }).catch(function(error) { + reject(error); + }); + }; + img.onerror = function(error) { + reject(error); + }; + fileEntry.file(function(file) { + img.src = "file://" + file.path; + }); + }); + }); +}; + +/** + * Draw the logo using the loaded font data. + */ +LogoManager.drawPreview = function() { + var $el = LogoManager.elements.$preview; + $el.empty(); + for (var i = SYM.LOGO, I = FONT.constants.MAX_CHAR_COUNT; i < I; i++) { + var url = FONT.data.character_image_urls[i]; + $el.append(''); + } +}; + +/** + * Initialize Logo Manager UI. + */ +LogoManager.init = function() { + // cache DOM elements + Object.keys(LogoManager.elements).forEach(function(key) { + LogoManager.elements[key] = $(LogoManager.elements[key]); + }); + Object.keys(LogoManager.constraints).forEach(function(key) { + LogoManager.constraints[key].$el = $(LogoManager.constraints[key].$el); + }); + // resize logo preview area to match tile size + var logoWidthPx = LogoManager.constraints.imageSize.expectedWidth, + logoHeightPx = LogoManager.constraints.imageSize.expectedHeight; + LogoManager.elements.$preview + .width(logoWidthPx) + .height(logoHeightPx); + // inject logo size variables for dynamic translation strings + var takeFirst = obj => { + if (obj.hasOwnProperty("length") && 0 < obj.length) { + return obj[0]; + } else { + return obj; + } + }; + var lang = takeFirst(i18next.options.fallbackLng), + ns = takeFirst(i18next.options.defaultNS); + i18next.addResourceBundle(lang, ns, { + logoWidthPx: logoWidthPx, + logoHeightPx: logoHeightPx, + }, true, true); +}; + /** * returns a canvas image with the character on it */ @@ -346,16 +497,6 @@ FONT.preview = function($el) { } }; -FONT.logoPreview = function($el) { - $el.empty() - .width(FONT.constants.LOGO.TILES_NUM_HORIZ * FONT.constants.SIZES.CHAR_WIDTH) - .height(FONT.constants.LOGO.TILES_NUM_VERT * FONT.constants.SIZES.CHAR_HEIGHT); - for (var i = SYM.LOGO, I = FONT.constants.MAX_CHAR_COUNT; i < I; i++) { - var url = FONT.data.character_image_urls[i]; - $el.append(''); - } -}; - FONT.symbol = function(hexVal) { return String.fromCharCode(hexVal); }; @@ -1372,6 +1513,9 @@ TABS.osd.initialize = function (callback) { fontbuttons.append($('