HTML Canvas images with text differ across operating systems - javascript

I'm building a small JS class that holds a <canvas> element and uses it to draw some images dynamically. Particularly those images have both background (fill) and text.
After doing this, I wanted to write some unit tests around this to ensure that, given some input, the generated output (as a base64 string) is always the same. That means that if I want to generate an image with a pink background and white text, the generated output is always a pink image with white text, and the dimensions are the same. In other words, the base64 encoded strings are equal.
I can run these tests locally and they pass (MacOS) but they fail on my Jenkins integration job (runs on Linux). That's because the image's font is slightly different. After reading a lot on this, I suspect this is because the fonts are implemented differently across different OSs.
See the images:
Expected, obtained locally in MacOS:
Actual, obtained in Linux:
You can see that the actual image I'm obtaining is a little bit taller than the expected (bottom padding below the text is smaller). Current font I'm using is "44px Arial"
See code:
Implementation
generateImage(backgroundColor, fontColor = "#000000") {
if (backgroundColor) {
this.drawBackgroundWithColor(backgroundColor);
}
this.drawTexOverBackground("Text to draw", X_POSITION, Y_POSITION, fontColor, "44px Arial");
return this.getCurrentCanvasAsBase64String();
}
drawBackgroundWithColor(backgroundColor){
if (backgroundColor) {
const context = this.getCanvasContext();
context.save();
context.fillStyle = backgroundColor;
context.fillRect(0, 0, this.width, this.height);
context.restore();
}
}
drawTexOverBackground(text, x, y, hexColor, font = this.font) {
const context = this.getCanvasContext();
context.save();
context.font = font;
context.textBaseline = "top";
context.fillStyle = hexColor;
context.fillText(text, x, y);
context.restore();
}
Unit test:
it('should create a basic pink background with black text', () => {
const backgroundGenerator = new ImageGenerator();
const createdBackground = backgroundGenerator.generateImage("#FFC0CB");
assert.equal(createdBackground, expectedTransparentImageWithText);
});
Being expectedTransparentImageWithText a global variable of the module that holds the string base64 value I expect and obtained in MacOS.
I must clarify that for unit testing this I'm using the canvas npm module which runs on a node and as the docs say it "implements that API as closely as possible" (referring to the Canvas browser API). I suspect that that's the reason why the fonts differ. I also got here and found that fonts DO differ between operating systems, and there's no 100% compatibility among them.
Is there any suggestion you can give me to unit test this better and take into account the fonts a well? Right now I'm only testing images without text, so I can avoid running into the explained scenario, but of course, not the best thing to do.

Related

Chrome warning willReadFrequently attribute set to true

Chrome keep printing this warning: "Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true.". I checked the code, where the warning triggers, and you can see I set the willReadFrequently attribute to true. What could be the problem? There was this warning in other places, but there the willReadFrequently attribute solved it.
The problem in Chrome 104-108 exists for sure. Btw, I am in a WebWorker. Could this be a chrome bug?
const offdesireCtx = offDesire.getContext("2d", { willReadFrequently: true });
if (!offdesireCtx) {
throw new Error("Desired OffscrenCanvas ctx undefined");
}
const offGetCtx = offGet.getContext("2d", { willReadFrequently: true });
if (!offGetCtx) {
throw new Error("Get OffscrenCanvas ctx undefined");
}
var imgd = offdesireCtx.getImageData(0, 0, tileSize, tileSize), pix = imgd.data; //Warning triggers
var imgdGet = offGetCtx.getImageData(0, 0, tileSize, tileSize), pixGet = imgdGet.data; //Warning triggers
As MDN says about willReadFrequently:
This will force the use of a software (instead of hardware accelerated) 2D canvas and can save memory when calling getImageData() frequently.
This means the canvas must be created, drawn to, and read from entirely on the CPU. Calling getContext provides a handle to the canvas buffer on the GPU by default, and if that call ever occurs with this canvas earlier on, that data is already on the GPU and it would have to be copied back to the CPU, defeating the goal of the performance warning.
I found in my case, I was creating the canvas and writing to it in one function, then returning the canvas. Later in my code, the result of that function call took the same canvas and created another context. It applied the
{ willReadFrequently: true } options argument to that call, but that was the second time getContext was called for this canvas. That means the texture buffer was already living on the GPU by this point and the second getContext call was ignoring the willReadFrequently suggestion as the data was already on the GPU.
So you need to trace back to where your canvas is first created, and then where its first getContext is called and drawn to. Subsequent canvas.getContext("2d", { willReadFrequently: true }) calls are too late to matter (and are likely even fine to omit the option). Think about when the texture buffer is being created and trace through your data flow to make sure it all lives on the CPU since its inception.

React with node canvas: registerFont causesTypeError: Object(...) is not a function

I'm making a word game with React and I need to draw the game on a canvas element then serve that as an image. It mostly works, except adding a font to the image.
More specifically, my game is built with React on next.js, and it works locally with just using the system fonts. But I also need it to work with server side rendering, and on the server the system fonts aren't installed. Which means it draws the image and text with little squares where the text should be.
My issue is with registerFont in the canvas package. The relevant part of my code is
import { registerFont, createCanvas } from 'canvas';
//import RobotoR from '../../public/Roboto/Roboto-Regular.ttf';
export const drawCanvas = ({ sentence, cards, workingCards, width, height }) => {
// default canvas size
let cw = 1200 // canvas width
let ch = 630 // canvas height; this is a minimun, it might change
~~~
registerFont('../../public/Roboto/Roboto-Regular.ttf', { family: 'Roboto' })
const canvas = createCanvas(cw, ch)
const ctx = canvas.getContext('2d')
The call to createCanvas works, the call to registerFont doesn't. I looked in node_modules/canvas and registerFont is exported slightly differently, but it seems to work for literally everyone else.
node_modules/canvas/index.js:
function createCanvas (width, height, type) {
return new Canvas(width, height, type)
}
~
/**
* Resolve paths for registerFont. Must be called *before* creating a Canvas
* instance.
* #param src {string} Path to font file.
* #param fontFace {{family: string, weight?: string, style?: string}} Object
* specifying font information. `weight` and `style` default to `"normal"`.
*/
function registerFont (src, fontFace) {
// TODO this doesn't need to be on Canvas; it should just be a static method
// of `bindings`.
return Canvas._registerFont(fs.realpathSync(src), fontFace)
}
~
module.exports = {
Canvas,
...
registerFont,
...
createCanvas,
...
I'm probably making some very stupid little mistake, but I can't find it. Help?
Answering myself in case anyone runs into the same issue. I asked someone who knows more than I do, and after puzzling through it together for a half hour he realized that node-canvas has a separate entry point if it's run in a browser. I did not know this was a thing, but it is.
In the node-canvas package.json, there is a line, '"browser": "browser.js"', which tells it to export a limited set of functions when run in a browser.
My drawing function is used in the browser and in a separate server side api process. But I don't need to register fonts in the browser, since there are already fonts installed, so I import "registerFont", then check if it comes back undefined. If it's defined, I use it, if not, I must be in a browser and don't need it.

Using Processing's loadImage in JavaScript

I am using the Processing API to draw an image to my HTML canvas, which I can use later in the code. The JavaScript code that I have is:
var sketchProc = function(processingInstance) {
with (processingInstance) {
/* #pjs preload="images/hot-air.png" */
size(innerWidth, innerHeight);
var testImage = loadImage("images/hot-air.png");
draw = function() {
image(testImage, 0, 0, 500, 500);
}
}
}
var canvas = document.getElementById("canvas");
var processingInstance = new Processing(canvas, sketchProc);
The console says that the image has dimensions 0x0. I tried loading with Processing's directives, but I am still getting an image dimensions of 0x0. However, when I call loadImage() inside the draw loop, the program recognizes the image's dimensions of 512x512.
I do not want to continuously call loadImage() inside the draw loop. What should I do to make sure that the image loads properly outside the draw loop?
You can find a minimal working example here.
First off, thanks for posting an MCVE for us to play with.
I believe the problem is that, for some reason, the preload directive, and maybe the loadImage() function itself, doesn't work when you're writing JavaScript-only Processing.js code. I've tested this in various editors and versions of Processing.js.
So it appears that to use the loadImage() function, you should use pure Processing code. Here is a CodePen that shows how you'd do that:
<script type="application/processing">
/* #pjs preload="https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; */
PImage testImage;
void setup(){
size(500, 500);
testImage = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg");
println(testImage.height);
}
void draw() {
background(100);
image(testImage, 0, 0, 250, 250);
}
</script>
<canvas> </canvas>
Just for comparison, here is the same code using JavaScript-only syntax. This doesn't work.
But taking a step back: if you're comfortable using JavaScript, then why are you using Processing.js? Processing.js is designed for Processing (Java) developers who want to write Java syntax that's automagically converted to JavaScript. At this point Processing.js is pretty old and no longer maintained.
Instead, I'd recommend using P5.js. P5.js allows you to write JavaScript syntax to create web-first Processing sketches. P5.js is much newer and is still being actively developed.
Here is the same code in P5.js:
var testImage;
function preload(){
testImage = loadImage("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg");
}
function setup() {
createCanvas(400, 400);
}
function draw() {
background(100);
image(testImage, 0, 0, 250, 250);
}
Shameless self-promotion: I wrote a tutorial on the differences between Processing, Processing.js, and P5.js available here.

Fabric.js - how to use custom cursors without drawing mode

I have no idea how to set up cursor image for drawing on the Canvas. I have noticed that I can set it only when
FABRICCANVAS.isDrawingMode = true;
However, the problem is that I have created dedicated drawing tools and I don't want to use those that are built into the Fabric.js.
Sample of my code (which doesn't work properly):
const FABRICCANVAS = new fabric.Canvas('canvas-draft');
const DRAFT = document.querySelector(".upper-canvas");
button.addEventListener('click', () => {
DRAFT.style.cursor = 'url(img/cursors/image.png) 0 34, auto';
});
But when I set isDrawingMode to true, it works. Unfortunately, I don't want to use built-in drawing tools because they leave paths (that can then be moved later, when FABRICCANVAS.selection = true).
Do you know any solution for this problem?
For the Canvas you can set different cursors:
e.g.
canvas.hoverCursor
canvas.defaultCursor
canvas.moveCursor
You can use absolute or relative paths to your cursor image:
canvas.moveCursor = 'url("...") 10 10, crosshair';

Finding the variable of a Canvas Element in unobfuscated Javascript

I am working with a GameMaker Studio and exporting a game in HTML5.
A missing feature in GameMaker Studio is the ability to export an image of the screen to the photo album of iOS and Android devices.
My workaround idea is to use a 3rd party plugin for HTML5 that will allow me to save a Canvas.
However GameMaker obfuscates the final output of the javascript and canvas it creates.
All I need to do is find which global variable contains the Canvas and I can capture it.
I have gone through a sample of unobfuscated Javascript but I can't read the code well enough to know which global variable I am supposed to use to call the canvas in question (and I don't want to override the context of it will wipe the canvas).
I found where the Canvas is attached to the context. Can anyone help here?
A sample looks like this:
_j61.prototype._q61 = function () {
_C8 = document.getElementById("canvas").getContext("2d");
if (_C8) {
this._yr = new _e9._B9._ua();
this._yr._Gp(_C8);
this._yr._Jp(1.0 / this._xW);
this._yr._Rp(0.1);
this._yr._Mp(1.0);
this._yr._Bp(_e9._B9._ua._2q | _e9._B9._ua._3q);
this._Fm._xr(this._yr)
}
};
In my function (for example) what goes here?
HELPME.fillStyle = "#FF0000";
HELPME.fillRect(0,0,150,75);

Categories

Resources