diff --git a/resources/glyphEditor.html b/resources/glyphEditor.html new file mode 100644 index 0000000000000000000000000000000000000000..f253a807b0cd971b2493b7469fe1a2a50c737be5 --- /dev/null +++ b/resources/glyphEditor.html @@ -0,0 +1,633 @@ +<!--The MIT License (MIT) + +Copyright (c) 2017 by Xavier Grosjean + +Based on work +Copyright (c) 2016 by Daniel Eichhorn +Copyright (c) 2016 by Fabrice Weinberg + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--> +<!-- + Standalone page to draw icons in matrix and generates the map definition with the format required by + library for OLED SD1306 screen: https://github.com/squix78/esp8266-oled-ssd1306 + 100% quick and dirty vanilla ECS6, no framework or library was injured for this project. + +--> +<html> + <head> + <meta charset="utf-8" /> + <style> + #form, #chars table { + user-select: none; + -moz-user-select: none; + margin-top: 5px; + } + #chars table, tr, td, th { + border-collapse: collapse; + } + #chars td, th { + border: 1px solid #CCC; + width: 12px; + height: 15px; + } + #chars th[action] { + cursor:pointer; + } + #chars th[action="up"], #chars th[action="down"], #chars th[action="left"], #chars th[action="right"] { + font-size: 10px; + } + #chars td.on { + background-color: #00F; + } + + #addChar, #generate { + display: none; + } + body.started #addChar, body.started #generate { + display: inline; + } + body.started #create { + display: none; + } + #page { + position:relative; + } + #page>div { + position: relative; + float: left; + } + + #output { + margin-left: 20px; + margin-top: 12px; + font-size:12px; + user-select: all; + -moz-user-select: all; + max-width:60%; + } + + #header { + margin-top: 10px; + } + #inputText { + width: 100%; + height:120px; + } + + </style> + </head> + <body> + <div id="form"> + <table width="100%"> + <tr> + <td>Font array name:</td><td><input placeholder="Font array name" type="text" id="name" value="My_Font"/></td> + <td width="75%" rowspan="5"> + <textarea id="inputText" placeholder="Or paste a char array font definition here"> +const char My_Font[] PROGMEM = { + 0x0A, // Width: 10 + 0x0D, // Height: 13 + 0x01, // First char: 1 + 0x02, // Number of chars: 2 + // Jump Table: + 0x00, 0x00, 0x13, 0x0A, // 1 + 0x00, 0x13, 0x14, 0x0A, // 2 + // Font Data: + 0xF0, 0x03, 0x10, 0x02, 0xF0, 0x03, 0x10, 0x02, 0xF0, 0x03, 0x10, 0x02, 0xF0, 0x03, 0x10, 0x02, 0xF0, 0x03, 0xC0, // 1 + 0x00, 0x0C, 0x80, 0x0B, 0x70, 0x08, 0x0C, 0x08, 0xF3, 0x0A, 0xF3, 0x0A, 0x0C, 0x08, 0x70, 0x08, 0x80, 0x0B, 0x00, 0x0C, // 2 +}; + </textarea></td> + </tr> + <tr> + <td>First char code:</td><td><input placeholder="First char code" type="number" id="code" value="1" min="1"/></td> + </tr> + <tr> + <td>Width:</td><td><input placeholder="Font width" type="number" id="width" value="10"/></td> + </tr> + <tr> + <td>Height:</td><td><input placeholder="Font height" type="number" id="height" value="13" max="64"/></td> + </tr> + <tr> + <td colspan="3"> </td> <!--slightly improves layout--> + </tr> + <tr> + <td colspan="2"><button id="create">Create</button> <button id="generate">Generate</button> <button id="savetoFile">Save To File</button> </td> + <td><input type="file" id="fileinput" /> <button id="parse">Parse</button></td> + </tr> + </table> + <br/> + </div> + <div id="page"> + <div id="charxContainer" ondragstart="return false;"> + <div id="chars"></div><br/> + <button id="addChar">Add a character</button> + </div> + <div id="output"> + <div class="output" id="header"> </div> + <div class="output" id="jump"> </div> + <div class="output" id="data"> </div> + </div> + </div> + <script> + (function() { + let font; + + class Font { + constructor() { + this.height = parseInt(document.getElementById('height').value); + this.width = parseInt(document.getElementById('width').value); + this.currentCharCode = parseInt(document.getElementById('code').value); + this.fontContainer = document.getElementById('chars'); + this.bytesForHeight = (1 + ((this.height - 1) >> 3)); // number of bytes needed for this font height + document.body.className = "started"; + document.getElementById('height').disabled = true; + document.getElementById('width').disabled = true; + } + + // Should we have a Char and a Pixel class ? not sure it's worth it. + // Let's use the DOM to store everything for now + + static updateCaption(element, code) { + element.textContent = `Char #${code}`; + } + + // Add a pixel matrix to draw a new character + // jumpaData not used for now: some day, use the last byte for optimisation + addChar(jumpData, charData) { + let charContainer = this.fontContainer.appendChild(document.createElement("table")); + charContainer.setAttribute("code", this.currentCharCode); + let caption = charContainer.appendChild(document.createElement("caption")); + Font.updateCaption(caption, this.currentCharCode); + let header = charContainer.appendChild(document.createElement("tr")); + header.innerHTML = '<th title="Delete this char" action="delete">✗</th>' + + '<th title="Add a char above" action="add">+</th>' + + '<th title="Shift pixels to the left" action="left">←</th>' + + '<th title="Shift pixels down" action="down">↓</th>' + + '<th title="Shift pixels up" action="up">↑</th>' + + '<th title="Shift pixels to the right" action="right">→</th>' + + '<th title="Copy from another character" action="copy">©</th>' + + '<th title="Reset all pixels" action="clean">∅</th>'; + // If data is provided, decode it to pixel initialization friendly structure + let pixelInit = []; + if (charData && charData.length) { + // charData byte count needs to be a multiple of bytesForHeight. End bytes with value 0 may have been trimmed + let missingBytes = charData.length % this.bytesForHeight; + for(let b = 0; b < missingBytes ; b++) { + charData.push(0); + } + while(charData.length) { + let row = charData.splice(0, this.bytesForHeight).reverse(); + let pixelRow = []; + for (let b = 0; b < row.length; b++) { + let mask = 0x80; + let byte = row[b]; + for (let bit = 0; bit < 8; bit++) { + if (byte & mask) { + pixelRow.push(1); + } else { + pixelRow.push(0); + } + mask = mask >> 1; + } + } + pixelRow.splice(0, pixelRow.length - this.height); + //Font.output('data', pixelRow); + pixelInit.push(pixelRow.reverse()); + } + } + + for(let r = 0; r < this.height; r++) { + let rowContainer = charContainer.appendChild(document.createElement("tr")); + for(let c = 0; c < this.width; c++) { + let pixContainer = rowContainer.appendChild(document.createElement("td")); + pixContainer.setAttribute('action', 'toggle'); + // If there is some init data, set the pixel accordingly + if (pixelInit.length && pixelInit[c]) { + if (pixelInit[c][r]) { + pixContainer.className = 'on'; + } + } + } + } + this.currentCharCode++; + return charContainer; + } + + static togglePixel(pixel) { + pixel.className = pixel.className === 'on' ? '': 'on'; + } + + // Return anInt as hex string + static toHexString(aByte) { + let zero = aByte < 16?'0':''; + return `0x${zero}${aByte.toString(16).toUpperCase()}` + } + + // Return least significant byte as hex string + static getLsB(anInt) { + return Font.toHexString(anInt & 0xFF); + } + // Return most significant byte as hex string + static getMsB(anInt) { + return Font.toHexString(anInt>>>8); + } + + static output(targetId, msg) { + let output = document.getElementById(targetId); + let line = output.appendChild(document.createElement('div')); + line.textContent = msg; + } + static emptyChars() { + document.getElementById('chars').textContent = ''; + } + static emptyOutput() { + document.getElementById('header').textContent = ''; + document.getElementById('jump').textContent = ''; + document.getElementById('data').textContent = ''; + } + + saveFile() { + let filename = document.getElementById('name').value.replace(/[^a-zA-Z0-9_$]/g, '_') + ".h"; + let data = document.getElementById("output").innerText; + if(data.length < 10) return; + let blobObject = new Blob([data], {type:'text/plain'}); + + if(window.navigator.msSaveBlob) { + window.navigator.msSaveBlob(blobObject, filename); + } else { + let a = document.createElement("a"); + a.setAttribute("href", URL.createObjectURL(blobObject)); + a.setAttribute("download", filename); + if (document.createEvent) { + let event = document.createEvent('MouseEvents'); + event.initEvent('click', true, true); + a.dispatchEvent(event); + } else { + a.click(); + } + } + } + + // Read from the <td> css class the pixels that need to be on + // generates the jump table and font data + generate() { + Font.emptyOutput(); + let chars = this.fontContainer.getElementsByTagName('table'); + let firstCharCode = parseInt(document.getElementById('code').value); + let name = document.getElementById('name').value.replace(/[^a-zA-Z0-9_$]/g, '_'); + + let bits2add = this.bytesForHeight*8 - this.height; // number of missing bits to fill leftmost byte + let charCount = chars.length; + let charAddr = 0; + // Comments are used when parsing back a generated font + Font.output('jump', ' // Jump Table:'); + Font.output('data', ' // Font Data:'); + // Browse each character + for(let ch = 0; ch < charCount; ch++) { + // Fix renumbering in case first char code was modified + Font.updateCaption(chars[ch].getElementsByTagName('caption')[0], ch + firstCharCode); + let charBytes = []; + let charCode = ch + firstCharCode; + let rows = chars[ch].getElementsByTagName('tr'); + let notZero = false; + // Browse each column + for(let col = 0; col < this.width ; col++) { + let bits = ""; // using string because js uses 32b ints when performing bit operations + // Browse each row starting from the bottom one, going up, and accumulate pixels in + // a string: this rotates the glyph + // Beware, row 0 is table headers. + for(let r = rows.length-1; r >=1 ; r--) { + let pixelState = rows[r].children[col].className; + bits += (pixelState === 'on' ? '1': '0'); + } + // Need to complete missing bits to have a sizeof byte multiple number of bits + for(let b = 0; b < bits2add; b++) { + bits = '0' + bits; + } + // Font.output('data', ` // ${bits}`); // Debugging help: rotated bitmap + + // read bytes from the end + for(let b = bits.length - 1; b >= 7; b -= 8) { + //Font.output('data', ` // ${bits.substr(b-7, 8)}`); // Debugging help: rotated bitmap + let byte = parseInt(bits.substr(b-7, 8), 2); + if (byte !== 0) { + notZero = true; + } + charBytes.push(Font.toHexString(byte)); + } + } + // Remove bytes with value 0 at the end of the array. + while(parseInt(charBytes[charBytes.length-1]) === 0 && charBytes.length !== 1) { + charBytes.pop(); + } + + if (notZero) { + Font.output('data', ` ${charBytes.join(', ')}, // ${charCode}`); + // TODO: last param width is not the best value. Need to compute the actual occupied width + Font.output('jump', ` ${Font.getMsB(charAddr)}, ${Font.getLsB(charAddr)}, ${Font.toHexString(charBytes.length)}, ${Font.toHexString(this.width)}, // ${charCode} `); + charAddr += charBytes.length; + } else { + Font.output('jump', ` 0xFF, 0xFF, 0x00, ${Font.toHexString(this.width)}, // ${charCode} `); + } + } + Font.output('data', '};'); + + Font.output('header', "// Font generated or edited with the glyphEditor"); + Font.output('header', `const char ${name}[] PROGMEM = {`); + // Comments are used when parsing back a generated font + Font.output('header', ` ${Font.toHexString(this.width)}, // Width: ${this.width}`); + Font.output('header', ` ${Font.toHexString(this.height)}, // Height: ${this.height}`); + Font.output('header', ` ${Font.toHexString(firstCharCode)}, // First char: ${firstCharCode}`); + Font.output('header', ` ${Font.toHexString(charCount)}, // Number of chars: ${charCount}`); + } + } + + document.getElementById('fileinput').addEventListener('change', function(e) { + let f = e.target.files[0]; + if (f) { + let r = new FileReader(); + r.onload = function(e) { + let contents = e.target.result; + alert( "Got the file.\n" + +"name: " + f.name + "\n" + +"type: " + f.type + "\n" + +"size: " + f.size + " bytes\n" + +"starts with: " + contents.substr(0, contents.indexOf("\n")) + ); + document.getElementById("inputText").value = contents; + }; + r.readAsText(f); + } else { + alert("Failed to load file"); + } + }); + + document.getElementById('savetoFile').addEventListener('click', function() { + font.saveFile(); + }); + + document.getElementById('generate').addEventListener('click', function() { + font.generate(); + }); + document.getElementById('addChar').addEventListener('click', function() { + font.addChar(); + }); + document.getElementById('inputText').addEventListener('click', function(e) { + let target = e.target; + target.select(); + }); + document.getElementById('chars').addEventListener('mousedown', function(e) { + if (e.button !== 0) return; + let target = e.target; + let action = target.getAttribute('action') || ''; + if (action === '') return; + let result, code, nextContainer, previousContainer, pixels ; + let currentContainer = target.parentNode.parentNode; + switch(action) { + case 'add': + code = currentContainer.getAttribute('code'); + nextContainer = font.addChar(); + currentContainer.parentNode.insertBefore(nextContainer, currentContainer); + do { + nextContainer.setAttribute('code', code); + Font.updateCaption(nextContainer.getElementsByTagName('caption')[0], code); + code ++; + } while (nextContainer = nextContainer.nextSibling); + break; + + case 'delete': + result = confirm("Delete this character ?"); + if (!result) return; + code = currentContainer.getAttribute('code'); + nextContainer = currentContainer; + while (nextContainer = nextContainer.nextSibling) { + nextContainer.setAttribute('code', code); + Font.updateCaption(nextContainer.getElementsByTagName('caption')[0], code); + code ++; + } + currentContainer.parentNode.removeChild(currentContainer); + break; + + // Shift pixels to the left + case 'left': + pixels = currentContainer.getElementsByTagName('td'); + for(p = 0; p < pixels.length; p++) { + if((p + 1) % font.width) { + pixels[p].className = pixels[p + 1].className; + } else { + pixels[p].className = ''; + } + } + break; + + // Shift pixels to the right + case 'right': + pixels = currentContainer.getElementsByTagName('td'); + for(p = pixels.length-1; p >=0 ; p--) { + if(p % font.width) { + pixels[p].className = pixels[p - 1].className; + } else { + pixels[p].className = ''; + } + } + break; + + // Shift pixels down + case 'down': + pixels = currentContainer.getElementsByTagName('td'); + for(p = pixels.length-1; p >=0 ; p--) { + if(p >= font.width) { + pixels[p].className = pixels[p - font.width].className; + } else { + pixels[p].className = ''; + } + } break; + + // Shift pixels up + case 'up': + pixels = currentContainer.getElementsByTagName('td'); + for(p = 0; p < pixels.length; p++) { + if(p < font.width*(font.height -1)) { + pixels[p].className = pixels[p + font.width].className; + } else { + pixels[p].className = ''; + } + } + break; + + case 'toggle': + Font.togglePixel(target); + break; + + case 'clean': + result = confirm("Delete the pixels ?"); + if (!result) return; + pixels = currentContainer.getElementsByTagName('td'); + for (let p = 0; p < pixels.length; p++) { + pixels[p].className = ''; + } + break; + + case 'copy': + let charNumber = parseInt(prompt("Source char #: ")); + let chars = font.fontContainer.getElementsByTagName('table'); + let tableOffset = charNumber - parseInt(document.getElementById('code').value); + let srcPixels = chars[tableOffset].getElementsByTagName('td'); + let targetPixels = currentContainer.getElementsByTagName('td'); + for(let i=0; i < srcPixels.length; i++) { + // First tds are in the th row, for editing actions. Skip them + if (targetPixels[i].parentNode.localName === 'th') continue; // In case we give them css at some point... + targetPixels[i].className = srcPixels[i].className; + } + break; + default: + // no. + } + + }); + document.getElementById('chars').addEventListener('mouseover', function(e) { + let target = e.target; + let action = target.getAttribute('action'); + if (action !== 'toggle' || e.buttons !== 1) return; + Font.togglePixel(target); + }); + document.getElementById('chars').addEventListener('dragstart', function() { + return false; + }); + + document.getElementById('create').addEventListener('click', function() { + font = new Font(); + font.addChar(); + }); + + // parse a char array declaration for an existing font. + // parsing heavily relies on comments. + document.getElementById('parse').addEventListener('click', function() { + if (document.getElementById('chars').childElementCount) { + let result = confirm("Confirm you want to overwrite the existing grids ?"); + if (!result) return; + } + let lines = document.getElementById('inputText').value.split('\n'); + let name = ''; + let height = 0; + let width = 0; + let firstCharCode = 0; + let jumpTable = []; + let charData = []; + let readingJumpTable = false; + let readingData = false; + + for(let l = 0 ; l < lines.length; l++) { + // TODO ? keep C compilation directives to copy them (not lowercased :)) to newly generated char array + let line = lines[l].trim(); + //alert(line); + let fields; + + // Declaration line: grab the name + if (line.indexOf('PROGMEM') > 0) { + fields = line.split(' '); + name = fields[2].replace(/[\[\]]/g, ''); + continue; + } + line = line.toLowerCase(); + // Todo: could rely on line order... + // width line: grab the width + if (line.indexOf('width') > 0) { + fields = line.split(','); + width = fields[0]; + continue; + } + // height line: grab the height + if (line.indexOf('height') > 0) { + fields = line.split(','); + height = fields[0]; + continue; + } + // first char code line: grab the first char code + if (line.indexOf('first char') > 0) { + fields = line.split(','); + firstCharCode = fields[0]; + continue; + } + // End of array declaration + // TODO warn if more lines: next fonts are ignored + if (line.indexOf('};') === 0) { + break; + } + + if (readingJumpTable || readingData) { + if (line.indexOf('#') !== 0 && line.length !== 0 && line.indexOf('//') !== 0) { + line = line.replace(/\/\/.*/, ''); // get rid of end of line comments + fields = line.split(','); + let newEntry = []; + for(let f=0; f < fields.length; f++) { + let value = parseInt(fields[f]); + if (isNaN(value)) continue; + if (readingData) { + charData.push(value); + } + if (readingJumpTable) { + newEntry.push(value); + } + } + if (readingJumpTable) { + jumpTable.push(newEntry); + } + } + } + + // Begining of data + if (line.indexOf('font data') > 0) { + readingData = true; + readingJumpTable = false; + } + // Begining of jump table + if (line.indexOf('jump table') > 0) { + readingJumpTable = true; + } + } + if (!name || !height || !width || !firstCharCode) { + alert("Does not look like a parsable font. Try again."); + return; + } + + Font.emptyChars(); + Font.emptyOutput(); + document.getElementById('name').value = name; + document.getElementById('height').value = parseInt(height); + document.getElementById('width').value = parseInt(width); + document.getElementById('code').value = parseInt(firstCharCode); + font = new Font(); + for(let c = 0 ; c < jumpTable.length; c++) { + let jumpEntry = jumpTable[c]; + let charEntry = []; + // displayable character + if (jumpEntry[0] !== 255 || jumpEntry[1] !== 255) { + charEntry = charData.splice(0, jumpEntry[2]); + } + font.addChar(jumpEntry, charEntry); + } + document.body.className = "started"; + }); + + })(); + </script> + </body> +</html> diff --git a/resources/glyphEditor.png b/resources/glyphEditor.png new file mode 100644 index 0000000000000000000000000000000000000000..800efc56f4bd50ba347eab57f3205ec8d0c0dd31 Binary files /dev/null and b/resources/glyphEditor.png differ