Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request]: opacity option for drawText() method #210

Closed
akaanish opened this issue Oct 3, 2019 · 14 comments
Closed

[Feature Request]: opacity option for drawText() method #210

akaanish opened this issue Oct 3, 2019 · 14 comments

Comments

@akaanish
Copy link

akaanish commented Oct 3, 2019

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.

@Hopding
Copy link
Owner

Hopding commented Dec 24, 2019

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.

@Hopding Hopding changed the title Issue while setting opacity to drawText() method [Feature Request]: opacity option for drawText() method Jan 1, 2020
@soadzoor
Copy link
Contributor

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?

@Hopding
Copy link
Owner

Hopding commented Jun 11, 2020

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)

@soadzoor
Copy link
Contributor

soadzoor commented Jun 12, 2020

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?

@Hopding
Copy link
Owner

Hopding commented Jun 12, 2020

@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.

@soadzoor
Copy link
Contributor

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

@soadzoor
Copy link
Contributor

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());

@Hopding
Copy link
Owner

Hopding commented Jun 14, 2020

@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')
);

@soadzoor
Copy link
Contributor

soadzoor commented Jun 14, 2020

@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

@Hopding
Copy link
Owner

Hopding commented Jun 14, 2020

@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:

    pdf-lib/src/api/PDFPage.ts

    Lines 959 to 960 in 9b6fb96

    const xObjectKey = addRandomSuffix('Image', 10);
    this.node.setXObject(PDFName.of(xObjectKey), image.ref);

@soadzoor
Copy link
Contributor

@Hopding I admire you're patience and persistence towards me! You're fantastic ❤️

@Hopding
Copy link
Owner

Hopding commented Jun 14, 2020

@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.

@soadzoor
Copy link
Contributor

@Hopding Sure, I plan to do that!

@Hopding
Copy link
Owner

Hopding commented Jun 20, 2020

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 pdf-lib@1.8.0

It's also available on unpkg:

As well as jsDelivr:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants