galkahana / HummusJS

Node.js module for high performance creation, modification and parsing of PDF files and streams
http://www.pdfhummus.com
Other
1.14k stars 169 forks source link

How to draw partially transparent path? #142

Closed jstogin closed 7 years ago

jstogin commented 7 years ago

I am trying to implement a free-form highlighter by drawing a thick path with partial transparency. In the browser's canvas environment, I accomplish this by setting ctx.globalAlpha=0.5, but I wasn't able to find an equivalent in HummusJS. (I also didn't see any alpha channels in any of the color operators in the PDF documentation.) Did I overlook something? Or is this not possible? Could you suggest a workaround? HummusJS is quite remarkable--thank you for all of your work!

galkahana commented 7 years ago

transparency is not available as a high level feature. you can build transparent paths, though, with some PDF know-how, and the lower level functionality. if you are interested, chapter 7 in the pdf reference manual deals with transparency. i got a handy copy here.

reading briefly, i think that you can get by with just defining a graphic state and an alpha value. see page 551 "Constant Shape and Opacity". create an object defining a graphic state with a "ca" key and the relevant alpha value as its value - 0.5, create a reference from the relevant page resources dictionary. before drawing the relevant graphics call cxt.q() then cxt.gs(gsName), then draw the graphics that you want half transparent using the regualr operators, and then restore the regular graphic state by cxt.Q(). or something like this.

jstogin commented 7 years ago

Ok, I think I partially understand your suggestion, but I haven't gotten everything working yet. First, I wanted to point out that the Extensibility section of your docs mentions the function startIndirectObject(), which I believe should be startNewIndirectObject().

Here's my attempt at creating the graphics state object.

var objCxt = pdfWriter.getObjectsContext();
objCxt.startNewIndirectObject();
objCxt.writeName("type");
objCxt.writeName("ExtGState");
objCxt.endLine();
objCxt.writeName("CA");
objCxt.writeNumber(0.5); // this is why I can't use the dictionary object from startDictionary()
objCxt.endNewIndirectObject();

Did I do this the right way? Is there a way to write the number 0.5 using the dictionary support from objCtx.startDictionary()?

I'm also confused as to what the gsName parameter to cxt.gs(gsName) should be. It doesn't seem to be the reference to the object. Should I have another key value pair in my dictionary that specifies the name?

Thanks!

galkahana commented 7 years ago
  1. Thanks for the note about startIndirectObjec. you are absolutely right.
  2. You have to create a dictionary otherwise there's no dictionary and the objects are just sitting there. It is OK to mix objCxt calls with dictionary context. doing something like this is perfectly legitimate:
var objCxt = pdfWriter.getObjectsContext();
var gsId = objCxt.startNewIndirectObject();
var dict = objCxt.startDictionary()
dict.writeKey("type");
dict.writeNameValue("ExtGState");
dict.writeKey("ca");
objCxt.writeNumber(0.5);
objCxt.endLine();
objCxt.endDictionary(dict);
  1. As for cxt.gs(gsName). What you should be doing is registering the created indirect object in the page resource dictionary. that should give you the name that you need. Note that gsId holds the object id in the previous section. something like this should follow:
var resourcesDict = pdfPage.getResourcesDictionary();
var gsName = resourcesDict.addExtGStateMapping(gsId);

that's it.

Gal.

jstogin commented 7 years ago

Thank you for the very helpful and detailed solution. Just in case anyone else ends up reading here, I needed one slight modification:

change dict.writeKey("ca"); to dict.writeKey("CA"); (ca is for nonstroking operations, while CA is for stroking operations)

Thank you again for your help--this is quite a fantastic library!