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($('