parallax / jsPDF

Client-side JavaScript PDF generation for everyone.
https://parall.ax/products/jspdf
MIT License
29.32k stars 4.68k forks source link

context2d.measureText returns incorrect value #3225

Open SeaRyanC opened 3 years ago

SeaRyanC commented 3 years ago

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

HackbrettXXX commented 3 years ago

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.

vibhuti019 commented 3 years ago

Can I work on this one ?

HackbrettXXX commented 3 years ago

Sure ;)

vibhuti019 commented 3 years ago

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

seanmiddleditch commented 1 year ago

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

TechWizard9999 commented 1 year ago

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