Pdf-lib: [Feature Request]: `opacity` option for `drawText()` method

Created on 3 Oct 2019  路  14Comments  路  Source: Hopding/pdf-lib

I want to draw text with opacity 50% but i can see i cannot pass alpha value in rgb()

function.

export const rgb = (red: number, green: number, blue: number): RGB => {
  assertRange(red, 'red', 0, 1);
  assertRange(green, 'green', 0, 1);
  assertRange(blue, 'blue', 0, 1);
  return { type: ColorTypes.RGB, red, green, blue };
};

 drawText(text: string, options: PDFPageDrawTextOptions = {}): void {
    assertIs(text, 'text', ['string']);
    assertOrUndefined(options.color, 'options.color', [[Object, 'Color']]);
    assertOrUndefined(options.font, 'options.font', [[PDFFont, 'PDFFont']]);
    assertOrUndefined(options.size, 'options.size', ['number']);
    assertOrUndefined(options.rotate, 'options.rotate', [[Object, 'Rotation']]);
    assertOrUndefined(options.xSkew, 'options.xSkew', [[Object, 'Rotation']]);
    assertOrUndefined(options.ySkew, 'options.ySkew', [[Object, 'Rotation']]);
    assertOrUndefined(options.x, 'options.x', ['number']);
    assertOrUndefined(options.y, 'options.y', ['number']);
    assertOrUndefined(options.lineHeight, 'options.lineHeight', ['number']);
    assertOrUndefined(options.maxWidth, 'options.maxWidth', ['number']);
    assertOrUndefined(options.wordBreaks, 'options.wordBreaks', [Array]);

    const [originalFont] = this.getFont();
    if (options.font) this.setFont(options.font);
    const [font, fontKey] = this.getFont();

    const fontSize = options.size || this.fontSize;

    const wordBreaks = options.wordBreaks || this.doc.defaultWordBreaks;
    const textWidth = (t: string) => font.widthOfTextAtSize(t, fontSize);
    const lines =
      options.maxWidth === undefined
        ? cleanText(text).split(/[\r\n\f]/)
        : breakTextIntoLines(text, wordBreaks, options.maxWidth, textWidth);

    const encodedLines = new Array(lines.length) as PDFHexString[];
    for (let idx = 0, len = lines.length; idx < len; idx++) {
      encodedLines[idx] = font.encodeText(lines[idx]);
    }

I cannot see how i can set opacity value.
Does this library support?
If not, it would be really nice if you consider it.

feature-request

Most helpful comment

Hello @akaanish! pdf-lib does not currently provide an API for changing text/shape/image opacity. However, I agree that this would be a useful feature to offer.

If anybody would like to work on this, I'd be happy to merge a PR implementing this feature. Otherwise I'll look into it when I get some more pressing issues taken care of.

All 14 comments

Hello @akaanish! pdf-lib does not currently provide an API for changing text/shape/image opacity. However, I agree that this would be a useful feature to offer.

If anybody would like to work on this, I'd be happy to merge a PR implementing this feature. Otherwise I'll look into it when I get some more pressing issues taken care of.

I desperately need opacity for svgpaths, how could I help implementing it? I don't know anything about PDFs, I'm more of a WebGL guy, where should I start?

Hello @soadzoor!

I'd suggest taking a look at the PDF spec and how other PDF libraries implement transparency. Here's a link to the transparency implementation in pdfkit: https://github.com/foliojs/pdfkit/blob/master/lib/mixins/color.js#L109-L144.

Some sections of the spec I'd recommend taking a look at:

  • Table 52 in section 8.4.1 defines the alpha constant
  • Table A.1 in Annex A defines the gs operator
  • Section 11.6.4.4 explains the CA and ca graphics state parameters that define opacity (you can see them used in the pdfkit implementation I linked to above)

Hello @Hopding !

Thank you for all these information. I've started studying this thing, although it feels like drinking from the firehose. It's a huge ocean I've dived into. I'm pretty sure I'd need a deeper understanding of the spec, and your pdf-lib as well, to be able to do this. This is how far I got so far:

I've added these to your operations.ts:

export const setFillingOpacity = (opacity: number | PDFNumber) =>
  PDFOperator.of(Ops.SetGraphicsStateParams, [asPDFNumber(opacity)]);

export const setStrokingOpacity = (opacity: number | PDFNumber) =>
  PDFOperator.of(Ops.SetGraphicsStateParams, [asPDFNumber(opacity)]);

Although, as the spec says:

While some parameters in the graphics state may be set with individual operators, as shown in Table 57, others may not. The latter may only be set with the generic graphics state operators(PDF 1.2). The operand supplied to this operator shall be the name of a graphics state parameter dictionary whose contents specify the values of one or more graphics state parameters. This name shall be looked up in the ExtGState subdictionary of the current resource dictionary.

And I believe, this is one that doesn't have its individual operator. Unfortunately, I can't see an example in your project where you use ExtGState, or even dictionaries (like pdf-kit does), so this is where I got stuck.

Could you please help?

@soadzoor I can certainly relate to feeling overwhelmed by PDFs and the spec. There's a ton of information in there. It took me a long time to digest it all, and I'm still learning little bits and pieces here and there. But on the bright side, you don't need to know _everything_ to get started. And, generally speaking, pdf-lib provides most of the tricky primitives to get you moving quickly.

You'll need access to the PDFContext of your document in order to create a dictionary and get a reference to it. Here's a basic example:

const pdfDoc = await PDFDocument.create();
const myDict = pdfDoc.context.obj({
  Type: 'Foo',
  Qux: 21,
  Baz: [{ Bar: 45 }],
});
const myDictRef = pdfDoc.context.register(myDict);

You may also find the FileEmbedder to be a useful reference. It creates and embeds some dictionaries and streams. There are a number of other examples strewn around the codebase as well.

Here's a rough example to show how you might create a dictionary and add it to the ExtGState of a particular page. (Note that it doesn't necessarily handle all edge cases and I haven't actually tested it).

const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage();
const { Resources } = page.node.normalizedEntries();

const extGState = pdfDoc.context.obj({
  GS1: { Type: 'ExtGState', ca: 0.7, CA: 0.5 } 
});

// Note: we shouldn't override this if it already exists!
Resources.set(PDFName.of('ExtGState'), extGState);

page.pushOperators(
  pushGraphicsState(),
  // Push an operator that sets the graphics state to `GS1`
);
page.drawSvg(...);
page.pushOperators(popGraphicsState());

You can use the PDFPage.pushOperators(...) method to push your graphics state operators to the page before drawing your SVG to test everything. Of course, ideally the API would expose an opacity option that takes care of all this behind the scenes for you.

Thank you @Hopding ! I'll try to play around with this on Monday, and will let you know how it goes! Cheers!

Hey @Hopding ,

Thank you for the explanations and the examples, you're super helpful! Something is happening with the opacity, but it's not what I'd expect, so I need your help (again, sorry.. :( )

Here's what I'd expect:
expected

And here's the actual result:
result

So, it seems, the opacity is changed, but it only affects the "original PDF's" parts, which I load as an arraybuffer. Which is strange, because I change the graphic state only after it's already loaded (I guess..?). Do you have any idea what's happening, and why?

My code (rougly) looks like this:

const arrayBuffer = await fetch(pdfFileURL).then((res) => res.arrayBuffer());
const pdfDoc = await PDFDocument.load(arrayBuffer);

const pages = pdfDoc.getPages();
const page = pages[0];

this.addIcons(page); // Add pacman, and computer icons (embedding PNGs). No issues here, so I won't bother you with the details

//
// Add polygons
//
const extGState = page.doc.context.obj({
    GS1: {
        Type: 'ExtGState',
        ca: 0.1, // filling
        CA: 0.1 // stroking
    }
});

page.node.normalizedEntries().Resources.set(PDFName.of('ExtGState'), extGState);
page.pushOperators(pushGraphicsState());

for (const polygon of polygons)
{
    const colorObject = polygon.color;
    page.drawSvgPath(polygon.svgPath, {
        x: polygon.x,
        y: polygon.y,
        borderColor: rgb(colorObject.r, colorObject.g, colorObject.b),
        color: rgb(colorObject.r, colorObject.g, colorObject.b),
        borderWidth: 1
    });
}

page.pushOperators(popGraphicsState());

@akaanish There are three steps required to change the opacity:

  1. Create an external graphics state dictionary (with the desired values)
  2. Add that dictionary to the page
  3. Apply the graphics state dictionary via an operator

You appear to be doing the first two steps, but not the third. In my example I left a comment where you need to push the operator, though I suppose it wasn't clear enough. It should be placed directly after pushGraphicsState():

page.pushOperators(
  pushGraphicsState(),
  // Replace this comment with an operator that sets the graphics state to `GS1`
);
page.drawSvg(...);
page.pushOperators(popGraphicsState());

Something like this should work (I haven't tested it though):

const setGraphicsState = (state: string | PDFName) =>
  PDFOperator.of(Ops.SetGraphicsStateParams, [asPDFName(state)]);

...

page.pushOperators(
  pushGraphicsState(),
  setGraphicsState('GS1')
);

@Hopding Oh, I missed that, you're right! Now it works, almost as expected! Thank you very much!
Although the original PDF's lines are still affected with the modified CA value, which makes me a little bit anxious. Maybe if I push another graphics state initally, that should solve it..? I'm not sure

@soadzoor That's probably because the original PDF already has a graphics state called GS1 and you are overwriting it. Recall this snippet of my example:

const extGState = pdfDoc.context.obj({
  GS1: { Type: 'ExtGState', ca: 0.7, CA: 0.5 } 
});

// Note: we shouldn't override this if it already exists!
Resources.set(PDFName.of('ExtGState'), extGState);

As I mentioned, my example does not handle all edge cases. There are two such scenarios that can occur here that you'll need to handle:

  1. The page's Resources dict may already have an ExtGState defined. If it does, then you should add your graphics state dictionary ({ Type: 'ExtGState', ca: 0.7, CA: 0.5 }) onto it, rather than overwriting it with a new one.
  2. It isn't sufficient to just hard code the key GS1 for your graphics state dictionary. This is highly likely to conflict with the name of an existing graphics state dictionary. Instead, you need to generate a random name that has a much higher probability of uniqueness. Generating a random suffix of 10 digits should do the trick. This is what we do for embedded images and fonts:
    https://github.com/Hopding/pdf-lib/blob/9b6fb96deaaeb4381c573e0e5f8e9d56629ced4a/src/api/PDFPage.ts#L959-L960

@Hopding I admire you're patience and persistence towards me! You're fantastic 鉂わ笍

@soadzoor I'm happy to help! If you're able to get it working, I hope you'll consider submitting a PR to add opacity and borderOpacity options to pdf-lib's API so this is easier for everybody to do in the future.

@Hopding Sure, I plan to do that!

Version 1.8.0 is now published. It contains opacity and borderOpacity options for all page drawing methods (thanks @soadzoor!). The full release notes are available here.

You can install this new version with npm:

npm install [email protected]

It's also available on unpkg:

As well as jsDelivr:

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dhollenbeck picture dhollenbeck  路  4Comments

ihmc3jn09hk picture ihmc3jn09hk  路  4Comments

zimt28 picture zimt28  路  5Comments

zimt28 picture zimt28  路  5Comments

jboxxx picture jboxxx  路  4Comments