iobroker.squeezeboxrpc
Version:
ioBroker Logitech Squeezebox Adapter over JSON/RPC-Protocol
417 lines (400 loc) • 14.3 kB
JavaScript
;
/* globals $,window,document */
/*
var fontStyle = '12px "Courier New", monospace'
var lineHeight = 15
var picWidth = 50;
var picHeight = 50;
var test1 = "Squeeze Work";
var test2 = "SqueezeWork";
var test3 = "aaa- aaa - aaa aaa aaa";
$('body').append(createTextImage(test1,fontStyle,picWidth,picHeight));
*/
/**
* Searches for the first occurrence of a given regular expression in the
* string from the given start position. If the regex is not found, returns -1.
*
* @param regex The regular expression to search for
* @param [startpos] The position to start the search from
* @returns The position of the first match or -1 if not found
*/
String.prototype.regexIndexOf = function (regex, startpos) {
const indexOf = this.substring(startpos || 0).search(regex);
return indexOf >= 0 ? indexOf + (startpos || 0) : indexOf;
};
/**
* Wordwrap a given string, with options to wrap camel case words and set
* the width of the output.
*
* @param str - The string to wordwrap
* @param width - The width of the output
* @param [opt] - Options object
* @param [opt.wrapCamelCase] - Whether to wrap camel case words
* @returns The wordwrapped string
*/
function wordwrap(str, width, opt) {
opt = opt || {};
const splitChars = [' ', '-', '\t'];
if (opt.wrapCamelCase) {
str = str.replace(/([a-z])([A-Z])/gm, '$1 $2');
}
const words = explode(str, splitChars);
let curLineLength = 0;
let strBuilder = '';
for (let i = 0; i < words.length; i += 1) {
let word = words[i];
if (curLineLength + word.length > width) {
if (curLineLength > 0) {
strBuilder += '\n';
curLineLength = 0;
}
while (word.Length > width) {
strBuilder += `${word.substring(0, width - 1)}-`;
word = word.substring(width - 1);
strBuilder += '\n';
}
word = word.trimStart();
}
if (curLineLength == 0 && /^\s+$/.test(word)) {
word = word.trimStart();
}
strBuilder += word;
curLineLength += word.length;
}
return strBuilder;
}
/**
* Split a string into parts where each part is either a word or a whitespace/dash character.
* If a whitespace/dash character occurs between two words, it is put into its own part.
* If a whitespace/dash character occurs at the end of a word, it is put into the same part as the word.
*
* @param str The string to split.
* @returns An array of string parts.
*/
function explode(str) {
const parts = [];
let startIndex = 0;
while (true) {
const index = str.regexIndexOf(/[ |\t|-]/gm, startIndex);
if (index == -1) {
parts.push(str.substring(startIndex));
return parts;
}
const word = str.substring(startIndex, startIndex + index - startIndex);
const nextChar = str.substring(index, index + 1)[0];
// Dashes and the likes should stick to the word occuring before it. Whitespace doesn't have to.
if (/^\s+$/.test(nextChar)) {
parts.push(word);
parts.push(nextChar);
} else {
parts.push(word + nextChar);
}
startIndex = index + 1;
}
}
/**
* Calculates the height metrics of the text using the specified font.
*
* This function creates a temporary DOM structure to measure the ascent,
* descent, and total height of the text "Hg" rendered with the given font.
*
* @param font - An object representing the font, which provides a method
* to get the font string.
* @returns An object containing the ascent, descent, and total height
* of the text, with properties:
* - ascent: The distance from the baseline to the top of the text.
* - descent: The distance from the baseline to the bottom of the text.
* - height: The total height of the text.
*/
const getTextHeight = function (font) {
const text = $('<span>Hg</span>').css({ font: font.getfont() });
const block = $('<div style="display: inline-block; width: 1px; height: 0px;"></div>');
const div = $('<div></div>');
div.append(text, block);
const body = $('body');
body.append(div);
const result = {};
try {
block.css({ verticalAlign: 'baseline' });
result.ascent = block.offset().top - text.offset().top;
block.css({ verticalAlign: 'bottom' });
result.height = block.offset().top - text.offset().top;
result.descent = result.height - result.ascent;
} finally {
div.remove();
}
return result;
};
/**
* @param text - The text to measure.
* @param font - The font to use when measuring the text.
* @returns The width of the text.
*/
export function getTextWidth(text, font) {
const canvas = getTextWidth.canvas || (getTextWidth.canvas = document.createElement('canvas'));
const context = canvas.getContext('2d');
context.font = font.getfont();
const metrics = context.measureText(text);
return metrics.width;
}
/** Class representing a Font. */
export class Font {
/**
* Creates a Font object for the given element.
*
* @param elem the element to get the font string from
*/
constructor(elem) {
this.span = document.createElement('span');
const fontstr = this.getFontString(elem[0]);
this.span.style.font = fontstr;
this.measure = getTextHeight(this);
this.measure.width = getTextWidth('M', this);
}
/**
* Retrieves the ascent value of the font.
*
* @returns {number} The ascent value of the font.
*/
getAscent() {
return this.measure.ascent;
}
/**
* Retrieves the descent value of the font.
*
* @returns {number} The descent value of the font.
*/
getDescent() {
return this.measure.descent;
}
/**
* Retrieves the width of the font.
*
* @returns {number} The width of the font.
*/
getWidth() {
return this.measure.width;
}
/**
* Retrieves the height value of the font.
*
* @returns {number} The height value of the font.
*/
getHeight() {
return this.measure.height;
}
/**
* Returns the current font string for the font.
*
* @returns The current font string.
*/
getfont() {
return this.span.style.font;
}
/**
* Set the font string for the font.
*
* @param fontstr the new font string
* @returns The Font object, to allow chaining.
*/
setfont(fontstr) {
this.span.style.font = fontstr;
return this;
}
/**
* Increase the font size of the font by one pixel.
*
* @returns The Font object, to allow chaining.
*/
incFontSize() {
this.span.style.fontSize = `${parseInt(this.span.style.fontSize) + 1}px`;
return this;
}
/**
* Decrease the font size of the font by one pixel.
*
* @returns The Font object, to allow chaining.
*/
decFontSize() {
this.span.style.fontSize = `${parseInt(this.span.style.fontSize) - 1}px`;
return this;
}
/**
* Given an element, returns the CSS font string for that element.
*
* @param elem - The element to get the font string for.
* @returns The CSS font string, or 'normal 12px sans-serif' if no font style is set.
*/
getFontString(elem) {
const style = window.getComputedStyle(elem);
let elementFont = style.getPropertyValue('font');
if (elementFont) {
return elementFont;
}
const fontStyle = style.getPropertyValue('font-style');
const fontVariant = style.getPropertyValue('font-variant');
const fontWeight = style.getPropertyValue('font-weight');
const fontSize = style.getPropertyValue('font-size');
const fontFamily = style.getPropertyValue('font-family');
elementFont = `${fontStyle} ${fontVariant} ${fontWeight} ${fontSize} ${fontFamily}`.replace(/ +/g, ' ').trim();
return elementFont ? elementFont : 'normal 12px sans-serif';
}
}
/**
* Adjusts the font size to ensure that the given lines of text fit within the specified width.
*
* @param lines - An array of strings, each representing a line of text.
* @param picWidth - The maximum width in pixels that the text should occupy.
* @param font - An object representing the font, which has methods to increase and decrease font size.
* @returns The adjusted font object with a size that fits the lines within the specified width.
*/
function getGoodFontSize(lines, picWidth, font) {
while (true) {
const maxWidth = getMaxPixelWidth(lines, font);
if (maxWidth > picWidth) {
font.decFontSize();
} else {
return font;
}
}
}
/**
* Create a canvas with the given lines, using the given font and dimensions,
* but using a font size which will fit the text into the canvas.
*
* @param lines the lines of text to be rendered
* @param font the font to use, or a string describing it
* @param picWidth the width of the canvas
* @param picHeight the height of the canvas
* @param lineHeight the line height of the text
* @param [opt] options, as follows:
* @param [opt.style] css style to use for the canvas
* @param [opt.style.direction] direction of text, either 'ltr' or 'rtl'
* @param [opt.style.textAlign] text alignment, either 'start', 'end', 'left', 'right', or 'center'
* @param [opt.style.color] color of the text
* @param [opt.style.backgroundcolor] background color of the canvas
* @returns the canvas element
*/
function createCanvas(lines, font, picWidth, picHeight, lineHeight, opt) {
opt = opt || {};
const canvas = document.createElement('canvas');
$('body').append(canvas);
canvas.width = picWidth;
canvas.height = picHeight;
const ctx = canvas.getContext('2d');
ctx.fillStyle = opt.style ? opt.backgroundcolor : 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.font = font.getfont();
ctx.fillStyle = opt.style ? opt.style.color : 'white';
ctx.textAlign = opt.style ? opt.style.textAlign : 'center';
let x = 0;
if (opt.style) {
if (opt.style.direction == 'ltr') {
if (opt.style.textAlign == 'start' || opt.style.textAlign == 'left') {
x = 0;
}
if (opt.style.textAlign == 'end' || opt.style.textAlign == 'right') {
x = picWidth;
}
if (opt.style.textAlign == 'center') {
x = picWidth / 2;
}
}
if (opt.style.direction == 'rtl') {
if (opt.style.textAlign == 'end' || opt.style.textAlign == 'left') {
x = 0;
}
if (opt.style.textAlign == 'start' || opt.style.textAlign == 'right') {
x = picWidth;
}
if (opt.style.textAlign == 'center') {
x = picWidth / 2;
}
}
} else {
x = picWidth / 2;
}
const y = font.getDescent() + Math.floor((picHeight - lines.length * lineHeight) / 2);
ctx.textBaseline = 'top';
for (let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], x, y + i * lineHeight);
}
return canvas;
}
/**
* Return the maximum number of characters in any line of the given array of
* strings.
*
* @param lines an array of strings, each a line of text.
* @returns the maximum number of characters in any line.
*/
function getMaxChars(lines) {
return lines.reduce(
function (acc, cur) {
return Math.max(acc, cur.length);
}.bind(this),
0,
);
}
/**
* Return the maximum width in pixels of the given lines of text, using the
* given font. The width is calculated by summing the widths of each line of
* text.
*
* @param lines an array of strings, each a line of text.
* @param font the font to use, or a string describing it.
*/
function getMaxPixelWidth(lines, font) {
return lines.reduce(
function (acc, cur) {
return Math.max(acc, getTextWidth(cur.trim(), font));
}.bind(this),
0,
);
}
/**
* Create a canvas with the given text, using the given font and dimensions,
* but using a font size which will fit the text into the canvas.
*
* @param text the text to be rendered
* @param font the font to use, or a string describing it
* @param picWidth the width of the canvas
* @param picHeight the height of the canvas
* @param [opt] options, as follows:
* @param [opt.style] css style to use for the canvas
* @param [opt.style.direction] direction of text, either 'ltr' or 'rtl'
* @param [opt.style.textAlign] text alignment, either 'start', 'end', 'left', 'right', or 'center'
* @param [opt.style.color] color of the text
* @param [opt.style.backgroundcolor] background color of the canvas
* @param [opt.wrapCamelCase] whether to wrap camel case words
* @returns the canvas element
*/
export function createTextImage(text, font, picWidth, picHeight, opt) {
opt = opt || {};
font = typeof font == 'string' ? new Font(font) : font;
const lineHeight = font.getHeight(); //getTextHeight(font).height;
const charWidth = font.getWidth(); //getTextWidth("M", font);
const maxChars = picWidth / charWidth;
let maxLines = picHeight / lineHeight - 1;
maxLines = maxLines < 0 ? 1 : maxLines;
let lines;
if (picHeight >= picWidth) {
for (
let textWidth = 1;
(lines = wordwrap(text, textWidth, opt).split('\n')) && maxLines + 1 < lines.length;
textWidth++
) {
//count textWidth
}
} else {
for (
let textWidth = 1;
(lines = wordwrap(text, textWidth, opt).split('\n')) && getMaxChars(lines) < maxChars && lines.length > 1;
textWidth++
) {
/* empty */
}
}
const goodFont = getGoodFontSize(lines, picWidth, font);
return createCanvas(lines, goodFont, picWidth, picHeight, lineHeight, opt);
}