Move canvas-based parsing to Color.ts lib

This removed the allowTransparency code, it didn't actually do much
before, just whether the 'advanced parsing' would allow transparency.
Since we already do the common formats, I'm not even sure what other
format it could be.
This commit is contained in:
Daniel Imms
2022-09-24 10:37:40 -07:00
parent 81d68b115c
commit 6368cb4cff
6 changed files with 72 additions and 94 deletions
@@ -78,7 +78,7 @@ describe('xterm-addon-serialize', () => {
terminal.loadAddon(serializeAddon);
selectionService = new TestSelectionService((terminal as any)._core._bufferService);
cm = new ColorManager(document, false);
cm = new ColorManager();
(terminal as any)._core._colorManager = cm;
(terminal as any)._core._selectionService = selectionService;
});
+1 -1
View File
@@ -28,7 +28,7 @@ describe('ColorManager', () => {
return {data: [0, 0, 0, 0xFF]};
}
});
cm = new ColorManager(document, false);
cm = new ColorManager();
});
describe('constructor', () => {
+13 -88
View File
@@ -80,22 +80,11 @@ export const DEFAULT_ANSI_COLORS = Object.freeze((() => {
*/
export class ColorManager implements IColorManager {
public colors: IColorSet;
private _ctx: CanvasRenderingContext2D;
private _litmusColor: CanvasGradient;
private _contrastCache: IColorContrastCache;
private _restoreColors!: IRestoreColorSet;
constructor(document: Document, public allowTransparency: boolean) {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Could not get rendering context');
}
this._ctx = ctx;
this._ctx.globalCompositeOperation = 'copy';
this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1);
constructor() {
this._contrastCache = new ColorContrastCache();
this.colors = {
foreground: DEFAULT_FOREGROUND,
@@ -118,9 +107,6 @@ export class ColorManager implements IColorManager {
case 'minimumContrastRatio':
this._contrastCache.clear();
break;
case 'allowTransparency':
this.allowTransparency = value;
break;
}
}
@@ -132,11 +118,11 @@ export class ColorManager implements IColorManager {
public setTheme(theme: ITheme = {}): void {
this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND);
this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND);
this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true);
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
this.colors.selectionBackgroundTransparent = this._parseColor(theme.selectionBackground, DEFAULT_SELECTION, true);
this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR);
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT);
this.colors.selectionBackgroundTransparent = this._parseColor(theme.selectionBackground, DEFAULT_SELECTION);
this.colors.selectionBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionBackgroundTransparent);
this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent, true);
this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent);
this.colors.selectionInactiveBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionInactiveBackgroundTransparent);
const nullColor: IColor = {
css: '',
@@ -221,76 +207,15 @@ export class ColorManager implements IColorManager {
private _parseColor(
cssString: string | undefined,
fallback: IColor,
allowTransparency: boolean = this.allowTransparency
fallback: IColor
): IColor {
if (cssString === undefined) {
return fallback;
}
// Fast path: avoid parsing via canvas if it looks like #RGB[A] or #RRGGBB[AA]
if (cssString.startsWith('#')) {
const c = css.toColor(cssString);
if (c) {
return c;
if (cssString !== undefined) {
try {
return css.toColor(cssString);
} catch {
// no-op
}
}
// If parsing the value results in failure, then it must be ignored, and the attribute must
// retain its previous value.
// -- https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles
this._ctx.fillStyle = this._litmusColor;
this._ctx.fillStyle = cssString;
if (typeof this._ctx.fillStyle !== 'string') {
console.warn(`Color: ${cssString} is invalid using fallback ${fallback.css}`);
return fallback;
}
this._ctx.fillRect(0, 0, 1, 1);
const data = this._ctx.getImageData(0, 0, 1, 1).data;
// Check if the printed color was transparent
if (data[3] !== 0xFF) {
if (!allowTransparency) {
// Ideally we'd just ignore the alpha channel, but...
//
// Browsers may not give back exactly the same RGB values we put in, because most/all
// convert the color to a pre-multiplied representation. getImageData converts that back to
// a un-premultipled representation, but the precision loss may make the RGB channels unuable
// on their own.
//
// E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns
// into 0x00000000.
//
// "Note: Due to the lossy nature of converting to and from premultiplied alpha color values,
// pixels that have just been set using putImageData() might be returned to an equivalent
// getImageData() as different values."
// -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation
//
// So let's just use the fallback color in this case instead.
console.warn(
`Color: ${cssString} is using transparency, but allowTransparency is false. ` +
`Using fallback ${fallback.css}.`
);
return fallback;
}
// https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color
// the color value has alpha less than 1.0, and the string is the color value in the CSS rgba()
const [r, g, b, a] = this._ctx.fillStyle.substring(5, this._ctx.fillStyle.length - 1).split(',').map(component => Number(component));
const alpha = Math.round(a * 255);
const rgba: number = channels.toRgba(r, g, b, alpha);
return {
rgba,
css: cssString
};
}
return {
// https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color
// if it has alpha equal to 1.0, then the string is a lowercase six-digit hex value, prefixed with a "#" character
css: this._ctx.fillStyle,
rgba: channels.toRgba(data[0], data[1], data[2], data[3])
};
return fallback;
}
}
+1 -1
View File
@@ -502,7 +502,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._instantiationService.setService(ICharSizeService, this._charSizeService);
this._theme = this.options.theme || this._theme;
this._colorManager = new ColorManager(document, this.options.allowTransparency);
this._colorManager = new ColorManager();
this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e, this.optionsService.rawOptions[e])));
this._colorManager.setTheme(this._theme);
+55 -2
View File
@@ -3,6 +3,7 @@
* @license MIT
*/
import { isNode } from 'common/Platform';
import { IColor, IColorRGB } from 'common/Types';
let $r = 0;
@@ -103,7 +104,29 @@ export namespace color {
* Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', '#rrggbbaa').
*/
export namespace css {
let $ctx: CanvasRenderingContext2D | undefined;
let $litmusColor: CanvasGradient | undefined;
if (!isNode) {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
if (ctx) {
$ctx = ctx;
$ctx.globalCompositeOperation = 'copy';
$litmusColor = $ctx.createLinearGradient(0, 0, 1, 1);
}
}
/**
* Converts a css string to an IColor, this should handle all valid CSS color strings and will
* throw if it's invalid. The ideal format to use is `#rrggbb[aa]` as it's the fastest to parse.
*
* Only `#rgb[a]`, `#rrggbb[aa]`, `rgb()` and `rgba()` formats are supported when run in a Node
* environment.
*/
export function toColor(css: string): IColor {
// Formats: #rgb[a] and #rrggbb[aa]
if (css.match(/#[0-9a-f]{3,8}/i)) {
switch (css.length) {
case 4: { // #rgb
@@ -131,15 +154,45 @@ export namespace css {
};
}
}
// Formats: rgb() or rgba()
const rgbaMatch = css.match(/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(,\s*(0|1|\d?\.(\d+))\s*)?\)/);
if (rgbaMatch) { // rgb() or rgba()
if (rgbaMatch) {
$r = parseInt(rgbaMatch[1]);
$g = parseInt(rgbaMatch[2]);
$b = parseInt(rgbaMatch[3]);
$a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF);
return rgba.toColor($r, $g, $b, $a);
}
throw new Error('css.toColor: Unsupported css format');
// Validate the context is available for canvas-based color parsing
if (!$ctx || !$litmusColor) {
throw new Error('css.toColor: Unsupported css format');
}
// Validate the color using canvas fillStyle
// See https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles
$ctx.fillStyle = $litmusColor;
$ctx.fillStyle = css;
if (typeof $ctx.fillStyle !== 'string') {
throw new Error('css.toColor: Unsupported css format');
}
$ctx.fillRect(0, 0, 1, 1);
[$r, $g, $b, $a] = $ctx.getImageData(0, 0, 1, 1).data;
// Validate the color is non-transparent as color hue gets lost when drawn to the canvas
if ($a !== 0xFF) {
throw new Error('css.toColor: Unsupported css format');
}
// Extract the color from the canvas' fillStyle property which exposes the color value in rgba()
// format
// See https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color
return {
rgba: channels.toRgba($r, $g, $b, $a),
css
};
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ interface INavigator {
// we want this module to live in common.
declare const navigator: INavigator;
const isNode = (typeof navigator === 'undefined') ? true : false;
export const isNode = (typeof navigator === 'undefined') ? true : false;
const userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform;