jsPDF icon indicating copy to clipboard operation
jsPDF copied to clipboard

context2d.measureText returns incorrect value

Open SeaRyanC opened this issue 4 years ago • 6 comments

Context2d.measureText says

The measureText() method returns an object that contains the width of the specified text, in pixels.

strokeRect also takes a width in pixels. This program creates a PDF with the text "hello world", and prints a rectangle below it according to its measured width

const jspdf = require("jspdf");
const fs = require("fs");

const doc = new jspdf.jsPDF({
    unit: "mm",
    format: [8.5 * 25.4, 11 * 25.4],
});

const ctx = doc.context2d;

doc.setFont("Helvetica");
doc.setFontSize(12);
doc.setFillColor(0, 0, 0);

const width = ctx.measureText("hello world");
ctx.fillText("hello world", 50, 50);

// Reported width
ctx.strokeRect(50, 55, width.width, 2);
// Correction attempt 1
ctx.strokeRect(50, 60, width.width / 2.8346456, 2);
// Correction attempt 2
ctx.strokeRect(50, 65, width.width / 2.8346456 * (96 / 72), 2);
// Correction attempt 3
ctx.strokeRect(50, 70, width.width / 2.8346456 * (72 / 96), 2);

fs.writeFile("test.pdf", Buffer.from(doc.output("arraybuffer")), () => {});

The top line, shown here, is not the same width as the text: image

The next three lines show how to get from the returned value from the correct value; it looks like a lot of the math inside the implementation isn't necessary.

Additionally, the documented return type is Number, but it actually returns an object of the form { width: number } (as described in the general description).

https://github.com/MrRio/jsPDF/blob/cef97fb34eda41a8704c9f3983e680919a328ce4/src/modules/context2d.js#L1388

SeaRyanC avatar Aug 03 '21 19:08 SeaRyanC

Thanks for reporting this. There is indeed a discrepancy between measureText and strokeRect (and probably all other methods). The issue is that most context2d methods don't respect the document unit. Instead, all parameters are interpreted as if they were given in the document unit instead of in pixels.

The desired behavior should be that all parameters are interpreted as pixels and converted to the document unit. A pull request to fix this would be very appreciated.

HackbrettXXX avatar Aug 09 '21 09:08 HackbrettXXX

Can I work on this one ?

vibhuti019 avatar Oct 09 '21 21:10 vibhuti019

Sure ;)

HackbrettXXX avatar Oct 11 '21 14:10 HackbrettXXX

Thanks @HackbrettXXX .. I am working on it..

vibhuti019 avatar Oct 16 '21 12:10 vibhuti019

There appears to be no problem with measureText(), as it returns for me functionally the same value as the real HTML Canvas context in my browser.

HTML Canvas: measureText(text).width: 214.1666717529297 jsPDF canvas: measureText(text).width: 213.75465599999995

The actual problem appears to be that the font rendering itself isn't applying the proper 96.0 / 72.0 transform between points and canvas pixels.

HTML Canvas image

jsPDF canvas image

This quick monkey patch fixes things for me, so this may be a simple fix in the jsPDF code. It applies the necessary scale to get the text to render at the correct size, and applies the inverse of the scale modifier to the position and width parameter since those are already correctly scaled internally. Note that I only applied x-axis scaling as my understanding is that jsPDF does not support y-axis scaling with text currently.

        const oldFillText = ctx.fillText
        ctx.fillText = (text, x, y, w) => {
            const scale = 96.0 / 72.0
            const invScale = 1.0 / scale

            ctx.save()
            ctx.scale(scale, 1)
            oldFillText.call(ctx, text, x * invScale, y, w * invScale)
            ctx.restore()
        }

jsPDF canvas (patched) image

seanmiddleditch avatar Jul 30 '23 23:07 seanmiddleditch

😊 "I'd be delighted to work on this issue, @SeaRyanC ! Could you please assign it to me?"

TechWizard9999 avatar Sep 30 '23 21:09 TechWizard9999