logo
Welcome Guest! To enable all features please Login or Register.

Notification

Icon
Error

Options
Go to last post Go to first unread
Terry  
#1 Posted : Tuesday, September 11, 2018 2:51:02 AM(UTC)
Terry

Rank: Member

Groups: Registered
Joined: 7/26/2016(UTC)
Posts: 25
United Kingdom
Location: Somerset

Thanks: 5 times
Hello,

I have created a PdfBitmap object from a System.Drawing.Bitmap object and the PdfBitmap appears to be correct because when I dump it to disc using
pdfBitmap.Image.Save("image.png", System.Drawing.Imaging.ImageFormat.Png);
the image file is correct.

Now I want to use the PdfBitmap in the appearance stream of a widget annotation, therefore I need to create a stream of type 'XObject' and subtype 'Image'.

So first, can the data buffer of the PdfBitmap be copied directly into the stream (ie. does the data in pdfBitmap.Buffer have the correct format)?

Second, if I can use the data in pdfBitmap.Buffer, how do I create and initialise the stream?
Normally I would use:
PdfTypeStream stream = PdfTypeStream.Create();
PdfTypeDictionary streamDict = PdfTypeDictionary.Create();
stream.Init(data, streamDict);
But this requires the data as a 'byte[]' and pdfBitmap.Buffer provides the data as an 'IntPtr' so the more obvious approach is to use:
PdfTypeStream stream = PdfTypeStream.Create(pdfBitmap.Buffer);
however when I try this it does not appear to work, in particular the stream's associated dictionary object does not appear to created correctly.

I would be very grateful if you could point me in the correct direction as to how to create the Image XObject stream from the PdfBitmap (or create it directly from the System.Drawing.Bitmap object - I assumed the intermediate PdfBitmap was necessary to ensure the data was in the correct format for a valid PDF image).

Many thanks for your help.
Terry.
Paul Rayman  
#2 Posted : Tuesday, September 11, 2018 5:27:55 AM(UTC)
Paul Rayman

Rank: Administration

Groups: Administrators
Joined: 1/5/2016(UTC)
Posts: 1,138

Thanks: 10 times
Was thanked: 133 time(s) in 130 post(s)
Hi,

seems SetRawData is that you are looking for.

Code:
PdfBitmap bmp;
...
var stream = PdfTypeStream.Create();
stream.SetRawData(bmp.Buffer, bmp.Stride * bmp.Height, false);


Also you may try some tricks:

1. Create PdfImageObject, generate its contents. Extract valid XObject from page resource dictionary under FXXn key.
for example
Code:
          var doc = PdfDocument.CreateNew();
            doc.Pages.InsertPageAt(0, 500, 500);

            //1.
            var bmp = new PdfBitmap(300, 300, true);
            PdfImageObject imageObj = PdfImageObject.Create(doc, bmp, 0, 0);
            IntPtr streamHandle = Pdfium.FPDFImageObj_GenerateStream(imageObj.Handle, doc.Pages[0].Handle);

The XObject will be located here:
doc.Pages[0].Dictionary["Resources"] -> XObjectx -> FXX1

2. Same as the first, but you also create PdfFromObject instead of inserting empty page
Code:
            PdfFormObject formObj = PdfFormObject.Create(doc.Pages[0]);
            formObj.PageObjects.Add(imageObj);
            var tmp_stream = PdfTypeStream.Create();
            Pdfium.FPDF_GenerateContentToStream(doc.Handle, formObj.PageObjects.Handle, stream.Handle, IntPtr.Zero);

in this case XObject stream will be located in the resource dictionary of the tmp_stream.
Terry  
#3 Posted : Tuesday, September 11, 2018 8:15:35 AM(UTC)
Terry

Rank: Member

Groups: Registered
Joined: 7/26/2016(UTC)
Posts: 25
United Kingdom
Location: Somerset

Thanks: 5 times
Hello Paul,

Thanks for your fast response.

I tried your first method (using SetRawData) and it almost works correctly - the one problem is that my PdfBitmap contains alpha data and therefore the image XObject stream dictionary needs to contain a 'Mask' image stream, but this is missing and so my image in the PDF has no transparency. On reflection I guess I should have expected this because the stream data set using SetRawData could be anything - Pdfium cannot know that it is image data and needs to be interpreted as such.

If I used one of your other suggested methods, since they make use of PdfImageObject and explicitly generate an image stream (rather than any old arbitrary stream) would they detect that the image contains alpha data and so automatically create a transparency 'Mask' entry?

Thanks for your help.
Terry.
Paul Rayman  
#4 Posted : Tuesday, September 11, 2018 3:59:25 PM(UTC)
Paul Rayman

Rank: Administration

Groups: Administrators
Joined: 1/5/2016(UTC)
Posts: 1,138

Thanks: 10 times
Was thanked: 133 time(s) in 130 post(s)
PDF supports various formats of images.
Generally, there is not necessary to use one where the transparency is set by the mask. You can use the image with alpha channel as well.
Terry  
#5 Posted : Thursday, September 13, 2018 9:07:51 AM(UTC)
Terry

Rank: Member

Groups: Registered
Joined: 7/26/2016(UTC)
Posts: 25
United Kingdom
Location: Somerset

Thanks: 5 times
Hello Paul,

I've now tried your second method, which uses a PdfImageObject to create an image XObject stream in the page resources of a temporary PdfDocument and if I dump this temporary document to a PDF file on disc I can see that the image it contains is correct - because the image in the PdfBitmap object has alpha data an SMask stream is automatically created in the image XObject which is really good news.

I then try to copy the image XObject from the temporary PdfDocument into the appearance stream of the widget annotation in my target document, and this is where things go wrong. When I look at the structure of my target document I can see that the all keys etc of the image XObject stream and its SMask stream have been copied correctly except the lengths of both streams are very slightly different, as are the data buffers of the streams.

So my question is, what is the correct way to copy the data buffer of a stream?

I have tried (where srcStream is the source stream in the temporary document and dstStream is the destination stream in the target document):

PdfTypeStream dstStream = PdfTypeStream.Create();
PdfTypeDictionary dstDict = PdfTypeDictionary.Create();
byte[] streamData = new byte[srcStream.Length];
srcStream.Read(0, srcStream.Length, streamData);
dstStream.Init(streamData, dstDict);

And:

PdfTypeStream dstStream = PdfTypeStream.Create();
dstStream.SetRawData(srcStream.RawData, srcStream.Length, false);
PdfTypeDictionary dstDict = dstStream.Dictionary;

But in both cases the stream length and data change slightly during the copy. Is this something to do with the compression (the 'FlateDecode' encoding filter)? I find it very difficult to know which API calls do encoding/decoding on the fly and which handle the genuinely raw byte data. In particular, srcStream.Length gives the length of the uncompressed/decoded data, and I'm surprised that this is also the case if I try to directly read the 'Length' key of the srcStream dictionary, ie.

int length = ((PdfTypeNumber)(srcStream.Dictionary["Length"])).IntValue;

Alternatively, is there a better way of creating an image XObject from a PdfBitmap (or a System.Drawing.Bitmap) that includes alpha data without setting transparency with a mask, as you hinted at in your last reply?

Thanks for your help.
Terry.


Paul Rayman  
#6 Posted : Thursday, September 13, 2018 11:10:00 PM(UTC)
Paul Rayman

Rank: Administration

Groups: Administrators
Joined: 1/5/2016(UTC)
Posts: 1,138

Thanks: 10 times
Was thanked: 133 time(s) in 130 post(s)
Seems we are lagging behind in providing the functionality you need for a couple of months. We are preparing a significant release, which will include work with annotations through the object model, without the need to use dictionaries.
However, the low-level API (available through Pdfium class) is already released and is available in the latest version.
Actually, you already can use it to easily create an appearance stream of any annotation, including widget annotation.

There is only one point. You will have to use reflection to transition from a low-level API to an existing PdfPageObjectCollection class.

Probably this explanation was incomprehensible, but please, look at the code below. It illustrates how you can easily create an appearance stream of any of annotation.

The main algorithm
Code:
        static void Main(string[] args)
        {
            PdfCommon.Initialize();

            //create test document and page
            var doc = PdfDocument.CreateNew();
            doc.Pages.InsertPageAt(0, 500, 500);
            var page = doc.Pages[0];

            //create annotation array and insert it into page dictionary
            var annotsArray = PdfTypeArray.Create();
            page.Dictionary["Annots"] = annotsArray;
            //Get list of indirect objects for future use.
            var list = PdfIndirectList.FromPdfDocument(doc);

            //create widget annotation
            var widget = PdfTypeDictionary.Create();
            widget["Type"] = PdfTypeName.Create("Annot");
            widget["Subtype"] = PdfTypeName.Create("Widget");
            widget["Name"] = PdfTypeString.Create(Guid.NewGuid().ToString(), false, false);
            //Set up some other keys
            //...
            //add widget annotation to the annot array
            list.Add(widget);
            annotsArray.AddIndirect(list, widget);

            //Create empty appearance stream
            var stream = CreateEmptyAppearance(AppearanceStreamModes.Normal, widget, list);
            //Convert Normal appearance stream to a collection of page objects
            var pageObjectCollection = AppearanceStreamToPageObjectsCollection(page, stream);

            //create test bitmap and set it to image object.
            PdfBitmap bitmap = new PdfBitmap(10, 10, true);
            bitmap.FillRect(0, 0, 10, 10, Color.Green);
            PdfImageObject img = PdfImageObject.Create(doc, bitmap, 10, 10);

            //Insert image object into page objects collection.
            //Note: all other page objects may be added to the collection as well.
            pageObjectCollection.Add(img);    //<----Thus, to create an appearance stream, you simply manipulate the usual page objects.

            //Generate content of page objects collection to the appearance stream
            GenerateAppearance(AppearanceStreamModes.Normal, pageObjectCollection, stream, doc, widget);

            doc.Save(...);
        }



and some helper functions


Code:
        /// <summary>
        /// Creates empty appearance stream
        /// </summary>
        public static PdfTypeStream CreateEmptyAppearance(AppearanceStreamModes mode, PdfTypeDictionary widget, PdfIndirectList list)
        {
            if (mode != AppearanceStreamModes.Normal && mode != AppearanceStreamModes.Down && mode != AppearanceStreamModes.Rollover)
                throw new ArgumentException();

            if (!widget.ContainsKey("AP"))
                widget["AP"] = PdfTypeDictionary.Create();
            var ap = widget["AP"].As<PdfTypeDictionary>();
            var stream = PdfTypeStream.Create();
            stream.InitEmpty();
            int num = list.Add(stream);
            switch (mode)
            {
                case AppearanceStreamModes.Normal: ap.SetIndirectAt("N", list, num); break;
                case AppearanceStreamModes.Down: ap.SetIndirectAt("D", list, num); break;
                case AppearanceStreamModes.Rollover: ap.SetIndirectAt("R", list, num); break;
            }
            return stream;
        }

        /// <summary>
        /// Convert appearance stream to page objects collection which can be used for drawing any annotation.
        /// </summary>
        public static PdfPageObjectsCollection AppearanceStreamToPageObjectsCollection(PdfPage page, PdfTypeStream stream)
        {
            IntPtr resDict = IntPtr.Zero;
            if (page.Dictionary.ContainsKey("Resources"))
                resDict = page.Dictionary["Resources"].Handle;
            return CreatePdfPageObjectsCollection(page.Document, resDict, stream.Handle);
        }

        /// <summary>
        /// Generate content of the specified collection to the specified appearance stream. 
        /// </summary>
        public static void GenerateAppearance(AppearanceStreamModes mode, PdfPageObjectsCollection collection, PdfTypeStream stream, PdfDocument doc, PdfTypeDictionary widget)
        {
            Pdfium.FPDF_GenerateContentToStream(doc.Handle, collection.Handle, stream.Handle, IntPtr.Zero);

            var bbox = CalcBBox(collection);
            stream.Dictionary["BBox"] = RectToArray(bbox);
            stream.Dictionary["Type"] = PdfTypeName.Create("XObject");
            stream.Dictionary["Subtype"] = PdfTypeName.Create("Form");
            stream.Dictionary["FormType"] = PdfTypeNumber.Create(1);
            stream.Dictionary["Matrix"] = MatrixToArray(new FS_MATRIX(1, 0, 0, 1, 0, 0));
            //Actualize annotation rectangle
            widget["Rectangle"] = RectToArray(new FS_RECTF(bbox.left, bbox.top, bbox.right, bbox.bottom));
        }
        
        /// <summary>
        /// Currently PdfPageObjectsCollection's constructors marked as internal, so you should use reflection to create an instance of that class
        /// This behaviour will be fixed in the final release.
        /// </summary>
        private static PdfPageObjectsCollection CreatePdfPageObjectsCollection(PdfDocument document, IntPtr resDict, IntPtr stream)
        {
            BindingFlags flags = BindingFlags.NonPublic | BindingFlags.Instance;
            CultureInfo culture = null; // use InvariantCulture or other if you prefer
            object[] parameters = { document, resDict, stream };
            return (PdfPageObjectsCollection)Activator.CreateInstance(typeof(PdfPageObjectsCollection), flags, null, parameters, culture);
        }

        /// <summary>
        /// Returns an array of 4 numbers specifying the coordinates of rectangle given in the order left edge, bottom edge, right edge, top edge.
        /// </summary>
        public static PdfTypeArray RectToArray(FS_RECTF rect)
        {
            var arr = PdfTypeArray.Create();
            arr.Add(PdfTypeNumber.Create(rect.left));
            arr.Add(PdfTypeNumber.Create(rect.bottom));
            arr.Add(PdfTypeNumber.Create(rect.right));
            arr.Add(PdfTypeNumber.Create(rect.top));
            return arr;
        }

        /// <summary>
        /// Returns an array of 6 numbers specifying the matrix coefficients given in the order a, b, c, d, e, f.
        /// </summary>
        public static PdfTypeArray MatrixToArray(FS_MATRIX matrix)
        {
            var arr = PdfTypeArray.Create();
            arr.Add(PdfTypeNumber.Create(matrix.a));
            arr.Add(PdfTypeNumber.Create(matrix.b));
            arr.Add(PdfTypeNumber.Create(matrix.c));
            arr.Add(PdfTypeNumber.Create(matrix.d));
            arr.Add(PdfTypeNumber.Create(matrix.e));
            arr.Add(PdfTypeNumber.Create(matrix.f));
            return arr;
        }

        /// <summary>
        /// Calculate the resulting bounding box for a collection of PdfPageObjects
        /// </summary>
        /// <param name="collection">Collection of <see cref="PdfPageObject"/></param>
        /// <returns>Overal bounding box for entrie collection of objects</returns>
        public static FS_RECTF CalcBBox(IEnumerable collection)
        {
            float left = float.MaxValue;
            float right = float.MinValue;
            float bottom = float.MaxValue;
            float top = float.MinValue;
            foreach (var obj in collection)
            {
                var bbox = BoundingBox((obj as PdfPageObject));
                left = Math.Min(left, bbox.left);
                right = Math.Max(right, bbox.right);
                top = Math.Max(top, bbox.top);
                bottom = Math.Min(bottom, bbox.bottom);
            }
            return new FS_RECTF(left, top, right, bottom);
        }

        /// <summary>
        /// Gets page object bounding box.
        /// </summary>
        public static FS_RECTF BoundingBox(PdfPageObject pageObject)
        {
            float l, r, t, b;
            Pdfium.FPDFPageObj_GetBBox(pageObject.Handle, null, out l, out t, out r, out b);
            return new FS_RECTF(l, t, r, b);
        }

Edited by user Thursday, September 13, 2018 11:13:38 PM(UTC)  | Reason: Not specified

thanks 1 user thanked Paul Rayman for this useful post.
Terry on 9/18/2018(UTC)
Terry  
#7 Posted : Tuesday, September 18, 2018 8:43:16 AM(UTC)
Terry

Rank: Member

Groups: Registered
Joined: 7/26/2016(UTC)
Posts: 25
United Kingdom
Location: Somerset

Thanks: 5 times
Hi Paul, thanks for your very full and detailed reply.

Actually I have been using the Pdfium low-level API for some time now to create the appearance streams of annotations, but in the past I have always added text or vector graphics. This was the first time I tried to add a bitmap and my only problem was getting the image data (including transparency data) into the correct format for copying into the stream object. As mentioned in my previous posting, I was trying to do a straight byte-wise copy of the encoded stream data but whichever API I used there was always a discrepancy between the data length before and after the copy. Anyhow, I have now solved my problem by simply copying the decoded data using the stream's 'DecodedData' property, although decoding and then re-encoding the same data seems a bit of a needless overhead, but it works!

Personally, I quite like using dictionaries etc and the low-level API rather than the higher level object model since I feel I have more control over what is going on, but many thanks for the code you provided since I'm sure it will be very useful in the future especially if your next release contains new object-level API for handling annotations.

Some higher level functionality that I would greatly welcome in Pdfium is the creation of digital signatures and digitally signing PDFs. I'll keep my fingers crossed!

Again, many thanks for your help.

Terry.
Users browsing this topic
Guest (2)
Forum Jump  
You cannot post new topics in this forum.
You cannot reply to topics in this forum.
You cannot delete your posts in this forum.
You cannot edit your posts in this forum.
You cannot create polls in this forum.
You cannot vote in polls in this forum.