You are viewing limited content. For full access, please sign in.

Question

Question

How do I add an image to a PDF without having to use DocumentImporter/DocumentExporter ?

asked on October 20, 2022 Show version history

I have a series of 3 SDK scripts.

The first records a token of a filename and a token of a literal system path (think c:\folder\temp) and uses DocumentExporter to export a PDF to a location outside of LF Repository.

That works, the file is saved there, no issue.

The second script uses PDFSharp to apply an image to the PDF located outside of LF Repository and save it.

That works, the file from the previous step now has an image on the PDF, no issue.

The third script is supposed to then grab that PDF-with-an-image from outside LF Repository and import it back into the LF Repository. It appears to run, but the PDF-with-an-image is never imported.
I'm certain there's something obvious I'm missing, any help greatly appreciated.

 

I've looked through answers a lot and this is what I'm currently trying but nothing seems to be working.

 

0 0

Answer

SELECTED ANSWER
replied on October 20, 2022

The docInfo.WriteEdoc should be used exactly how it is set up in the example I posted early as that is a functional example from an in-use process.

The .WriteEdoc method doesn't do the writing, it creates/configures the stream that will be used to do the writing with the .Write method.

2 0

Replies

replied on October 20, 2022

Personally, I usually don't use DocumentImporter in this way. What I prefer to do is to read the PDF into a stream and use the WriteEdoc method.

// Write PDF to document
using(Stream eDocStream = doc.WriteEdoc("application/pdf",ms.ToArray().LongLength)){
    eDocStream.Write(ms.ToArray(),0,ms.ToArray().Length);
}

// Update document extension and save changes
doc.Extension = ".pdf";

// Save changes
doc.Save();

I use this for PDF generation in Workflow. For example, I'll use a DocumentExporter to export the document as a PDF directly into a memory stream.

I then pass the memory stream to the WriteEdoc method and add it to the target document.

2 0
replied on October 20, 2022

the signedPDFPath variable is being used as the path in the repository (the Document.Create call), which I would expect to fail with an "Invalid path" error. Are you trapping exceptions and reporting the error message?

2 0
replied on October 20, 2022

most of the time it errors with "object already exists"; I tried to replace it with Document.Create("\\path\\in\\repo\\" as well as Document.Create("\\path\\in\\repo\\" + filename and still get 'object already exists'

0 0
replied on October 20, 2022

SignedPDFPath appears to be a folder. Aren't you expected to pass a file's full path (including the name) to ImportEdoc?

0 0
replied on October 20, 2022

signedPDFPath is "E:\path\to\filename.pdf" and signedPDFFilename is just "filename".

0 0
replied on October 20, 2022

I am not really certain how to adapt your solution to my use case.

At the step that I am running into a problem, I have a PDF that has been appropriately modified and saved as a PDF in "E:\path\to\filename.pdf". It sounds like you're suggesting that I open that PDF (outside of Repo) as a memory stream, but I am not understanding how I would then write that memory stream to a location inside of the Repo.

Part of my confusion is that the file exists and is done being worked on at this point - I really just need to say "Now take the PDF at 'E:\path\to\filename.pdf' and put it in 'MYREPO\path\to\destination\filename.pdf', which feels like a very straightforward operation - so I am certain that I am either misunderstanding some step or overlooking something.

0 0
replied on October 20, 2022 Show version history

The code sample I provided is specifically the piece that writes back to a document in the repository.

It doesn't matter if the file is in network path, a repo, etc. because you're using the stream to write; whether the source file is inside or outside the repo doesn't affect what you can do with the stream.

The document you want to write it to is "doc" in that code (i.e., doc.WriteEdoc)

So all you need to do is get a memory stream or file stream and of the PDF that can be used in the code I sent.

 

Based on your last statement I think part of the confusion is that you may be viewing the repo and a network folder as a 1-to-1 comparison, when in reality they are apples and oranges.

Although Laserfiche has a folder structure, that structure exists within Laserfiche so moving items into or out of a repo is not at all the same as dragging them between folders, hence the import/export methods.

 

I think a better question might be, why are you exporting it in the first place if you're just bringing it right back in and removing it from the external location.

1 0
replied on October 20, 2022

I will take a look in a moment; just wanted to answer the last part though real quick: PDFSharp can not access the PDF while it is in LF Repo as far as I can tell, but if I export it first it becomes trivial to access it, so that is the option I opted for.

0 0
replied on October 20, 2022

I'd imagine there is an overload that allows you to load the PDF into PDFSharp from a memorystream rather than only having a path option.

You can't load it from a path in Laserfiche because those are not real directory paths, but if you read it out into a stream, you should be able pass that into your PDFSharp code.

1 0
replied on October 20, 2022

PDFSharp does allow you to open a memory stream - if I can not use a path though, how can Laserfiche provide me a memory stream of an in-repo PDF file for PDFSharp to use? 

0 0
replied on October 20, 2022 Show version history

You don't need a path. You would read the PDF from Laserfiche into a stream, then open the document directly from that stream.

For example,

PdfDocument document = PdfReader.Open(stream);

Here's an example of reading the PDF into a stream

// Read eDoc from document in repo
string mimeType = "";
MemoryStream ms = new MemoryStream();
using (LaserficheReadStream sEDocStream = sDoc.ReadEdoc(out mimeType)){
      sEDocStream.CopyTo(ms);
}

 

1 0
replied on October 20, 2022

"You would read the PDF from Laserfiche into a stream" - would this not require a path? as far as I have been able to tell, all I have access to are tokens and all I get with tokens are non-existent paths and entry ids/names, if I need to read the PDF into a stream, how am I getting the 'the PDF'?
Stream ms = new MemoryStream() at minimum seems to requires a byte[] buffer, but then I am back to 'wouldn't i need a path to the PDF to specify to the byte[] buffer?'

0 0
replied on October 20, 2022

I think you're still misunderstanding what a "path" means in Laserfiche.

A Laserfiche "path" is a logical construct; it doesn't actually exist as a folder path in the same way that you would see with network folders.

You don't read the PDF from a path, you read the PDF from the DocumentInfo object, which can be obtained in many different ways.

The same DocumentInfo object you use for the document exporter can be used with the ReadEdoc method in the code above.

You have the export working already which means you already have a viable DocumentInfo object and the only difference is that you would use it in the LaserficheReadStream instead of in the DocumentExporter.

1 0
replied on October 20, 2022

I am completely misunderstanding what a path means in Laserfiche lol...

To be fair though, I am completely misunderstanding what the DocumentInfo object is as well - there isn't a lot of documentation so i'm kind of stumbling through this, sorry. This is what the script would look like if i adapted it to what i believe everyone here is suggesting, using a memory stream: 

            DocumentInfo docInf = (DocumentInfo)this.BoundEntryInfo;

            string mimeType = "pdf";
            MemoryStream ms = new MemoryStream();
            using (LaserficheReadStream eDocStream = docInf.ReadEdoc(out mimeType))
            {
                eDocStream.CopyTo(ms);
            }


            //a ms of the PDF
            PdfDocument inputDocument = PdfReader.Open(ms, PdfDocumentOpenMode.Modify);//PdfReader.Open(filledPDF, PdfDocumentOpenMode.Modify);

            //XGraphics object for drawing
            //this will need to know the page the signature must go on
            XGraphics gfx = XGraphics.FromPdfPage(inputDocument.Pages[2]);

            //get signature datafields (just the first)
            string sigStringPadded = this.WorkflowApi.GetTokenValueFromNameAsString("RetrieveBusinessProcessVariablesSignatureDatafields_datafield1", 1);

            //string to bytes
            byte[] imageBytes = Convert.FromBase64String(sigStringPadded);

            //bytes to stream
            Stream sigStream = new MemoryStream(imageBytes);

            //stream to image
            XImage xImg = XImage.FromStream(sigStream);

            //issue draw command
            DrawPng(gfx, xImg);

            //save outputDocument
            inputDocument.Save(ms);
            inputDocument.Close();

 

0 0
replied on October 20, 2022

Looks like you're at least on the right track.

However, in the second to last step you should save to a new memory stream rather than reusing the original; either that or you'll have to reset the stream.

1 0
replied on October 20, 2022

I'm unsure I understand the mental model of what is happening.

I have 'DocumentInfo' about the current entry.

I read the PDF Electronic Document of the current entry into a LaserficheReadStream.

I copy that PDF Stream into a 'plain' MemoryStream.

I make a PDFDocument by opening the 'plain' MemoryStream.

I make a graphics object using the PDFDocument pages.

I draw issue the draw on the graphics object - the PDFDocument in the 'plain' MemoryStream now has an image on it.

-----------

At this point I have tried:

inputDocument.Save(ms), thinking I am saving the document with the image to the 'plain' MemoryStream.

inputDocument.Save(eDocStream), thinking I am saving the document with the image to the Laserfiche entry [im certain this is wrong, but not understanding the mental model - i tried it].

docInfo.WriteEDoc(mimeType, ms.Length), thinking that if i read the EDoc then I should be writing the EDoc [though given that I can not specify the stream but only its length - this too feels wrong].

I've tried a few more things that I am forgetting, but I think I don't understand the mental model well enough. At this point I have a MemoryStream that contains a PDF document that has an image drawn on it, and I have an entry with a PDF. I am inclined to believe the next step would be taking the Document Info of the entry and writing the PDF-with-image MemoryStream to that entry; does that sound right?

0 0
replied on October 20, 2022

So to answer the last part first, you cannot save back to the same stream you started with without extra steps. When you finish the PDFSharp portion, you either need to reset that "plain" memory stream or replace it with a new one.

The steps should be as follows

  1. Get DocumentInfo object
  2. Read PDF in LaserficheReadStream
  3. Copy contents to first memory stream (ms)
  4. Do all your PDFSharp stuff
  5. Create a new Memory Stream (i.e., not ms) or Refresh ms (ms = new MemoryStream()) This step is very important.
  6. Save PDFSharp document to the new/refreshed stream.
  7. Now, use this final stream in the WriteEdoc code.

 

There's a lot you should research to really get comfortable with this level of coding, but a couple "crash course" items are as follows:

  1. DocumentInfo is the document. A Laserfiche document is a complex object with pages, text, metadata, electronic files, and more, so the DocumentInfo literally is the document entry and the PDF you're working with is just a piece of that object.
  2. Memory Streams are not files. Changes cannot just be saved back to the same stream because the properties of that stream, like the length and position need to change.
1 0
replied on October 20, 2022

Thanks for your help. How is WriteEdoc supposed to be used? From what I can tell the only use is:

docInfo.WriteEdoc(mimeType, ms.Length)

I can't see how that would write anything. I've seen other posts here referencing using it like:

using (System.IO.Stream writeStream = docInfo.WriteEdoc(mimeType, ms.Length)

But doing that leaves me with a System.IO.Stream and a MemoryStream, the MemoryStream containing the PDF-with-image. So I figured I would do this:

writeStream.Write(ms.ToArray(),0,ms.ToArray().Length);

Thinking that I'm writing to the docInfo the contents of ms, but I guess I'm still misunderstanding. And it's tough because nothing is crashing and it's a little hard to 'debug' what exactly is happening. I agree that I need to research more to get comfortable with Laserfiche-coding, which is why I've been on the lookup for documentation beyond the 'SDK Documentation' PDF included with the installation; like it's wonderful to learn the mental model that DocumentInfo is the literal representation of the document from Laserfiches perspective, but I'm unsure where I would have gone to learn that.

0 0
SELECTED ANSWER
replied on October 20, 2022

The docInfo.WriteEdoc should be used exactly how it is set up in the example I posted early as that is a functional example from an in-use process.

The .WriteEdoc method doesn't do the writing, it creates/configures the stream that will be used to do the writing with the .Write method.

2 0
replied on October 21, 2022

I think I am getting a better understanding of DocumentInfo and what goes with working with it. You've been incredibly helpful, as have the others in this thread.
If anyone has hit this thread and been similarly confused, here is a small working SDK Script that assumes you have some png data stored as a string in a token; it uses PDFSharp and applies the image to some given location specified in the draw png call.

namespace WorkflowActivity.Scripting.SDKScript
{
    using System;
    using System.IO;
    using System.Drawing;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Data.SqlClient;
    using System.Text;
    using PdfSharp.Pdf;
    using PdfSharp.Drawing;
    using PdfSharp.Pdf.IO;
    using Laserfiche.RepositoryAccess;
    using Laserfiche.DocumentServices;

    public class Script1 : RAScriptClass104
    {
    public void DrawPng(XGraphics gfx, XImage xImg)
    {
        //manually calculated aspect ratio value for width
        float widthRatio = 2.9464f;

        //some PDFs are landscape, must account for this
        gfx.RotateTransform(-90);
        gfx.DrawImage(xImg, -610, 267, 50 * widthRatio, 50);

    }
        protected override void Execute()
        {
		//get the current representation of a laserfiche document
            DocumentInfo docInf = Document.GetDocumentInfo(this.BoundEntryId, this.RASession);
		
		//assuming you only care about pdf, set mimetype
            string mimeType = "application/pdf";

		//reserve some memory (resizable)
            MemoryStream ms = new MemoryStream();

		//reserve some memory (non-resizable) the size of the pdf electronic document of our laserfiche document, then copy that memory to the prior (resizable) memory stream
            using (LaserficheReadStream eDocStream = docInf.ReadEdoc(out mimeType))
            {
                eDocStream.CopyTo(ms);
            }

            //create a pdf representation using the memory stream, make it modifiable
            PdfDocument inputDocument = PdfReader.Open(ms, PdfDocumentOpenMode.Modify);

            //XGraphics object for drawing
            //this will need to know the page the image must go on (in our case page 3)
            XGraphics gfx = XGraphics.FromPdfPage(inputDocument.Pages[2]);

            //get signature datafields
            string sigStringPadded = this.WorkflowApi.GetTokenValueFromNameAsString("RetrieveBusinessProcessVariablesSignatureDatafields_datafield1", 1);
		//evidently, Convert.FromBase64String expects padding characters

            //string to bytes
            byte[] imageBytes = Convert.FromBase64String(sigStringPadded);

            //bytes to stream
            Stream sigStream = new MemoryStream(imageBytes);

            //stream to image
            XImage xImg = XImage.FromStream(sigStream);

            //issue draw command
            try{
                DrawPng(gfx, xImg);
                this.WorkflowApi.TrackInformation("draw reached");
            }catch{
                this.WorkflowApi.TrackInformation("draw did not succeed");
            }

		//reset the memory stream
            ms = new MemoryStream();

            //save the pdf representation that contains the pdf and image to the memory stream
            inputDocument.Save(ms);

            //reserve some writable memory for a pdf the size of the memory stream from the laserfiche document
            Stream writeStream = docInf.WriteEdoc("application/pdf", ms.ToArray().LongLength);

            //write the memory stream to the laserfiche documents write stream
            writeStream.Write(ms.ToArray(), 0, ms.ToArray().Length);

            //ensure the laserfiche document has a pdf extension
            docInf.Extension = "pdf";

            //save the laserfiche document
            try{
            docInf.Save();
            this.WorkflowApi.TrackInformation("save done");
            }catch{
            this.WorkflowApi.TrackInformation("save failed");
            }finally{
		//ensure other subsequent activities can act on file
            docInf.Unlock();
            }
        }
    }
}

 

0 0
replied on October 22, 2022

Hi Scott,

One last note on this. The using statements I included in my code serve an important function in C# with regard to garbage collection.

The stream objects are disposable, which means the use unmanaged memory resources that are not "cleared out" the same as managed resources.

In most cases, it is best to put disposable objects in a using statement because that automatically disposes of the object to free up the memory after the code it contains has been executed, even if an error occurs.

This may not cause any issues for you, but it is something to be aware of with programming because in processes or activities that use a lot of memory, not disposing of disposables can cause problems.

Using objects that implement IDisposable | Microsoft Learn

1 0
You are not allowed to follow up in this post.

Sign in to reply to this post.