B4J Library [BANanoPDFMake] Client Side PDF Document Generation

Ola

Download

Phew. A dream come through. Very excited right now...

pdf.png


BANanoPDFMake is a wrap of the wonderful and amazing javascript library PDFMake

What I have tried to do here is to simplify this as much as possible. This means one can create a PDF document as simply as..

B4X:
Sub basic
    Dim maker As PDFMaker
    maker.Initialize
    maker.AddString("First paragraph")
    maker.AddString("Another paragraph, this time a little bit longer to make sure, this line will be divided into at least two lines")
    maker.Open
End Sub

There is quiet a lot that the PDFMake JS library does in simple ways. You will note that the code included herein is split into classes that are easy to follow and even enhance. This will get you going in terms of generating PDF documents. This for me will be a MS Word replacement due to the reporting functionality needed on my side. At most PDF seems to be more professional looking than Excel reports at times.

Explore the example code in the pdfIndex code module. I have attached here an example PDF of what that has produced.

What's Available and usable?

  • Multiple Pages
  • Password Protection & other security e.g. copying, annotation
  • Compression
  • QRCode
  • Styling
  • Lists
  • Columns
  • Tables (rowspans, colspans, borders, background etc)
  • Headers & Footers
  • Margins
  • URL Clickable Links
  • Watermark
  • PageSize
  • PageMode - landscape / potrait
  • Write at XY pos (absolutePosition / relativePosition) when necessary
  • ToC (table of contents)
  • Images

Try it out now and tell us your thoughts!

Ta!

PS: Off to add this to the project i'm doing.. One of the things I appreciate about this JS Library is the ability of giving end users flexibility. You can create anything you want with it. Thanks to PDFMake!

You have to experience this for yourself. ;) The code is easy to follow I hope for the examples.

These are pdf and pgindex modules.
 
Last edited:

alwaysbusy

Expert
Licensed User
Longtime User
Maybe useful for the images:

B4X:
Sub Class_Globals
   Private BANano As BANano 'ignore
  
   Private Blobs As Map
   Private NumFiles As Int
End Sub

' START
Sub BuildPDFReport()
   '   Load the images.  The event FilesAreReady is raised when all are loaded an converted to bese64
   ...
   NumFiles = 2
   Blobs.Initialize
  
   GetFileFromServer("./assets/B4X.jpg")
   GetFileFromServer("./assets/BANanoV4.jpg")
End Sub

public Sub FilesAreReady()
   ' we got all the files in base64 format
   For Each k As String In Blobs.Keys
       Log(k)
       Log(Blobs.Get(k))
   Next
  
   ' build the actual report
  ...
End Sub

Sub GetFileFromServer(FileName As String)
   Dim Response As BANanoFetchResponse
   Dim Blob As BANanoObject
  
   ' list (GET is default, and we do not need extra options so we pass Null for the options)
   Dim fetch1 As BANanoFetch
   fetch1.Initialize(FileName, Null)
   fetch1.Then(Response)
       ' we got the response promise, so resolve it to a blob
       fetch1.Return(Response.Blob)
   fetch1.Then(Blob)
       ' we got the blob, read it in a FileReader
       Dim FileReader As BANanoObject
       FileReader.Initialize2("FileReader", Null)
       Dim event As BANanoEvent
       ' when loaded...
       FileReader.AddEventListenerOpen("onload", Array(event))
           ' get the data
           Dim Target As BANanoObject = event.OtherField("target")
           Dim DataUrl As String = Target.GetField("result").Result
           ' save the data per filename
           Blobs.Put(FileName, DataUrl)
           ' if we have them all...
           If Blobs.Size = NumFiles Then
               FilesAreReady
           End If
       ' closing the 'onload' event listener function  
       FileReader.CloseEventListener
       ' get the DataURL, which will call the 'onload' method we've just written
       FileReader.RunMethod("readAsDataURL", Blob)          
   fetch1.End ' always end a fetch with this!  
End Sub

Alwaysbusy
 

Mashiane

Expert
Licensed User
Longtime User
Coming Soon: Images

Thanks to provided code using fetch by @alwaysbusy, image embedding is on point. The possibilities with this is that one can put a company logo on the report. Hoa!

imageManipulation.png


As images that we can use in our report can be used anywhere in the report, it is better just to preload them. By doing this we give each image a unique key in the key value mapping where the key is what we want to name it and the value the URL.

Now depending on what we want, whether to download, open or print the document. The lib compiles the rest.. Let's see the code..

B4X:
Sub images
    Dim maker As PDFMaker
    maker.Initialize
    'preload images before use, we can use them anywhere in our doc by key
    maker.Preload("one", "./assets/1.jpg")
    maker.Preload("seven9", "./assets/79.jpg")
    maker.Preload("yuna", "./assets/yuna.jpg")
    '
    maker.AddString("Image Manipulation")
    maker.AddImage(maker.CreateImage.SetImage("one").SetWidth(150).SetHeight(150))
    
    maker.Download("images.pdf")
End Sub
 

Mashiane

Expert
Licensed User
Longtime User
Opacity and placing images anywhere XY you want, setting the image width & height and fitting images into a rectangle.

imagesx.png


B4X:
Sub images
    Dim maker As PDFMaker
    maker.Initialize
    'preload images before use, we can use them anywhere in our doc by key
    maker.Preload("one", "./assets/1.jpg")
    maker.Preload("seven9", "./assets/79.jpg")
    maker.Preload("yuna", "./assets/yuna.jpg")
    '
    maker.AddString("Image Manipulation")
    maker.AddImage(maker.CreateImage("one").SetWidth(150).SetHeight(150))
    '
    maker.AddString("You can also fit the image inside a rectangle'")
    maker.AddImage(maker.CreateImage("yuna").SetFit(100, 100).SetPageBreakAfter)
    '
    maker.AddString("and opacity is supported")
    maker.AddImage(maker.CreateImage("seven9").SetWidth(150).SetOpacity(0.5))
    '
    maker.AddString("place image at xy")
    maker.AddImage(maker.CreateImage("seven9").SetWidth(60).SetHeight(60).SetAbsolutePosition(500, 50))
    maker.Download("images.pdf")
End Sub

This creates 2 pages because we have SetPageBreakAfter.



xy.png
 

Mashiane

Expert
Licensed User
Longtime User
Table of Contents Creation

toc.png


The table of contents is automatically generated anytime you indicate that the text you add should be in the ToC with .SetTocItem(True)

In the example below we create some global styles and the add some content to the page applying those styles. We add some content on different pages and make some sections of the headers to be in the ToC.

B4X:
Sub toc
    Dim maker As PDFMaker
    maker.Initialize
    
    'add global styles and add them to definition
    Dim header As PDFStyle = maker.CreateStyle("header").SetFontSize(18).SetBold(True)
    maker.AddStyle(header)
    '
    Dim subheader As PDFStyle = maker.CreateStyle("subheader").SetFontSize(15).SetBold(True)
    maker.AddStyle(subheader)
    '
    Dim quotex As PDFStyle = maker.CreateStyle("quote").SetItalics(True)
    maker.AddStyle(quotex)
    '
    Dim small As PDFStyle = maker.CreateStyle("small").SetFontSize(8)
    maker.AddStyle(small)

    '
    maker.AddText(maker.CreateText("This is a TOC example. Text elements marked with tocItem: true will be located in the toc. See below.").SetPageBreakAfter)
    '
    Dim tc As PDFToC = maker.CreateToc.SetTitle(maker.CreateText("TABLE OF CONTENTS").SetStyle("header"))
    tc.SetNumberStyle(maker.CreateStyleOnly.SetBold(True))
    maker.AddToc(tc)
    '
    Dim p1 As PDFText = maker.CreateText("This is a header, using header style").SetStyle("header")
    p1.SetTocItem(True)
    p1.SetPageBreakBefore
    p1.SetTocStyle(maker.CreateStyleOnly.SetItalics(True))
    p1.SetTocMargin(0, 10, 0, 0)
    p1.SetTocNumberStyle(maker.CreateStyleOnly.SetItalics(True).SetDecoration("underline"))
    maker.AddText(p1)
    '
    maker.AddString("Lorem ipsum dolor sit amet, consectetur adipisicing elit. Confectum ponit legam...")
    
    maker.AddText(maker.CreateText("Subheader 1 - using subheader style").SetStyle("subheader").SetTocItem(True).SetPageBreakBefore)
    maker.AddString("Lorem ipsum dolor sit amet, consectetur adipisicing elit. Confectum ponit legam...")
    maker.AddString("Lorem ipsum dolor sit amet, consectetur adipisicing elit. Confectum ponit legam...")
    maker.AddString("Lorem ipsum dolor sit amet, consectetur adipisicing elit. Confectum ponit legam...")
    '
    maker.AddText(maker.CreateText("Subheader 2 - using subheader style").SetStyle("subheader").SetTocItem(True).SetPageBreakBefore)
    maker.AddString("Lorem ipsum dolor sit amet, consectetur adipisicing elit. Confectum ponit legam...")
    maker.AddString("Lorem ipsum dolor sit amet, consectetur adipisicing elit. Confectum ponit legam...")
    '
    maker.AddText(maker.CreateText("You can use multiple styles").SetStyle("quote").SetStyle("small"))
    maker.Download("toc.pdf")
End Sub

This creates multiple pages..
 

Mashiane

Expert
Licensed User
Longtime User
Release 1 available on first post. The github also includes the source code so you can tweak to meet your needs. I have only done this to meet the basic reporting functionality. All the best.

Some silly assumptions:

1. You can compile a BANano Library. You have to compile the source into a library as per download.
2. You are prepared to do some reading about this js library on their website
3. Play around, make mistakes, try all over again.

PS: There is something on their website about generating canvas charts and embedding those. For me that is not important for now, you can explore yourself and even enhance herein.

Ta!
 

Mashiane

Expert
Licensed User
Longtime User
Upcoming Version 1.01

basic.png


For my purposes I need to be able to generate a PDF report, 2. Save the pdf report to the server and 3. view the report on the browser. So we have added PDFView.

This has been further discussed here.

1. We have added an element that we want to render to

B4X:
body.Append($"<iframe id="basic"></iframe>"$)

2. We generate the PDF document as we usually do however instead of .Download, we use .Upload which expects a callback.

Let's take a simple example.

B4X:
Sub display
    maker.Initialize
    maker.AddString("First paragraph")
    maker.AddString("Another paragraph, this time a little bit longer to make sure, this line will be divided into at least two lines")
    '
    maker.Upload(Me, "doUpload", "basic.pdf")
End Sub

The upload code generates the FormData object that we need to upload using Ajax. This gets the .Blob object of the PDF document so that we can save it. We pass this the filename that we want to save our PDF as.

Let's look at doUpload...

B4X:
Sub doUpload(fd As BANanoObject)
    Dim res As Map = BANano.FromJson(BANano.CallAjaxWait("http://localhost/demo/assets/upload.php", "POST", "application/pdf", fd, False, Null))
    Dim status As String = res.Get("status")
    Select Case status
    Case "success"
        Dim view As PDFView
        view.Initialize("basic").SetHREF("./assets/basic.pdf").SetWidth(800).SetHeight(500).view
    Case "error"
    End Select 
End Sub

doUpload executes an Ajax call to upload the file to the server. This returns a map with {status:success} or {status:error}, depending on that we call the PDFView to display the PDF document in our browser.

A loop at upload.php

B4X:
<?php
header("Access-Control-Allow-Origin: *");
set_time_limit(0);
if (isset($_FILES['upload'])) {
    // Example:
    if(move_uploaded_file($_FILES['upload']['tmp_name'], "../assets/" . $_FILES['upload']['name'])){
        echo '{ "status": "success" }';
    } else {
        echo '{ "status": "error" }';
    }
    exit;
} else {
    echo '{ "status": "error" }';
}
?>

The php file expects an object that meets the file structure. This should have the name "upload". Let's see how this was done.

1. You call the maker.Upload sub.

B4X:
'upload the file
Sub Upload(module As Object, methodName As String, fileName As String)
    eventHandler = module
    eventMethod = methodName
    fName = fileName
    'get the blob
    Dim data As Object
    GetBlob(Me, "forUpload", data)
End Sub

2. This calls the GetBlob internal sub which falls back on the forUpload callback

GetBlob, builds the pdf and run runs the getBlob method of pdfmake which falls back to forUpload call.

B4X:
'get the blob of the document
Sub GetBlob(module As Object, methodName As String, data As Object)
    Dim ddy As Map = getDD
    Dim gd As BANanoObject = BANano.CallBack(module, methodName, Array(data))
    pdfMake.RunMethod("createPdf", ddy).RunMethod("getBlob", Array(gd))
End Sub

3. forUpload, receives the Blob object receives and that is what we need to prepare for upload and convert in into FormData.

B4X:
private Sub forUpload(data As Object)
    Dim fd As BANanoObject = PrepareToUpload(data, fName)
    BANano.CallSub(eventHandler, eventMethod, fd)
End Sub

4. We call PrepareForUpload.. (this prepares our blob for upload as a formdata receiving the file name also.

B4X:
'prepare to upload, returns formData
Sub PrepareToUpload(blob As Object, fileName As String) As BANanoObject
    Dim formData As BANanoObject
    formData.Initialize2("FormData",Null)
    formData.RunMethod("append", Array("upload", blob, fileName))
    Return formData
End Sub

5. The result of this formdata is passed to be received by our callback receiving method in our case doUpload

doUpload receives the FormData created by PrepareForUpload, executes the php on it to be received by the server and then display it.

Ta!
 

Mashiane

Expert
Licensed User
Longtime User
Something fresh: Abstract Designer

 
Top