cesarvr / pdf-generator

Cordova plugin to generate pdf in the client-side
MIT License
107 stars 61 forks source link

Android Base64 #9

Closed Mechanicalee closed 7 years ago

Mechanicalee commented 7 years ago

Is it possible to get the base64 string when creating a PDF in android, also can we prvent the print preview from automatically being called.

Ideally I would like to be able to store the PDFs created in one folder with the app's external storage data, this way the end user can't save them in the wrong place and I can easily display a history of created PDFs.

Kind regards

Lee

cabaird commented 7 years ago

I also have a need for this issue to be fixed. I need to generate the PDF, and add it to an array of files that I am attaching to an email. I am currently able to do this in iOS, but not in android because the service is returning the string "Success" rather than the base64 value that is expected.

Thank you,

Christian

MikeMadez commented 7 years ago

Do you got any luck?

AlexChesser commented 7 years ago

Also throwing my +1 on this. Any idea about what might be required to patch this in?

Edit/Update - so my initial theory here is that the https://github.com/cesarvr/pdf-generator/blob/master/src/android/PDFPrinter.java is what is creating the PDF-print-view. (super early initial research, I'm not sure I 100% get it yet) From that point of view, it looks like the web-page we're sending to the pdf-generator gets rendered in a temporary webview and then automatically sent to a print command with the 'print-to-pdf' option enabled.

https://developer.android.com/reference/android/print/PrintDocumentAdapter.html is the class we're extending.

The question I THINK then becomes - is there any option we have which will get the Base64 result of a printdopcumentadapter. I'm reading the docs now.

@cesarvr please feel free to kick in if you've already followed this line of thought and have found that it won't work or have any additional guidance or suggestions of where I might look. This is the first I'm seeing this code.

update2: looking at https://developer.android.com/reference/android/graphics/pdf/PdfDocument.html right now. android.graphics.pdf looks like it has a writeStream method - which I assume means we could send that to something within android which converts a memory-stream to Base64. The question becomes - is there any way to convert a webview into a PDFDocument, or to GET the webview out of the createPrintDocumentAdapter call.

update3:

Current android workflow:

  1. PDFGenerator class (custom)
    • creates an android webview offscreen
    • renders (string || webpage) to offscreen webview
    • sends offscreen webview content to the PDFPrinterWebView (custom)
  2. PDFPrinterWebView (custom)
    • sends content to the PDFPrinter(custom)
  3. PDFPrinter (custom) extends PrintDocumentAdapter (native)
    • throws webview content into android native print-as-pdf functionality

The proposal: find a way to get the BYTES from an android native PrintDocumentAdapter and return them as BASE64 if b64 flag is set in initial call to PDFGenerator

Based on the documentation for https://developer.android.com/reference/android/print/PrintDocumentAdapter.html

I suspect the lifecycle of this object goes

  1. onStart
  2. onLayout - (PrintAttributes oldAttributes, PrintAttributes newAttributes, CancellationSignal cancellationSignal, PrintDocumentAdapter.LayoutResultCallback callback, Bundle extras)
  3. onWrite -
  4. onfinished

It also seems that we might be able to access the PDF Bytes from within https://developer.android.com/reference/android/print/PrintDocumentAdapter.LayoutResultCallback.html during the onLayoutFinished method

Print document info is one of the params of this function. https://developer.android.com/reference/android/print/PrintDocumentInfo.html

PrintDocumentInfo has a method writeToParcel which appears to function as a stream https://developer.android.com/reference/android/os/Parcel.html

Parcel has a method readByteArray which will get the bytes of the PrintDocumentInfo which we can convert into BASE64 using this one liner from stack overflow. http://stackoverflow.com/questions/2418485/how-do-i-convert-a-byte-array-to-base64-in-java

There are a few unknowns in this ... like if the PrintDocumentInfo is actually just the raw PDF file and if every step of this chain will work without trouble. I've not got a lot of experience with java/android so I'd love a bit of advice if there is any to be had.

cesarvr commented 7 years ago

Hi @AlexChesser, very happy that you want to contribute with this feature.

Here is some info you may find useful:

How it works

We have a Webview that basically renders the content HTML/CSS, this class expose the rendered content trough an adapter method called createDocumentAdapter this basically make the Webview content readable by the PrintManager class that take care of printing the content.

Approaches =========

Creates a PrintDocumentAdapter that provides the content of this WebView for printing. The adapter works by converting the WebView contents to a PDF stream. The WebView cannot be drawn during the conversion process - any such draws are undefined. It is recommended to use a dedicated off screen WebView for the printing. If necessary, an application may temporarily hide a visible WebView by using a custom PrintDocumentAdapter instance wrapped around the object returned and observing the onStart and onFinish methods. See PrintDocumentAdapter for more information.

You can use this project to test the cordova plugin.

Hope this helps, If you need more info let me know.

gameboy9 commented 7 years ago

I'm also interested in a fix for this issue. I found this, but I couldn't get it to work for the life of me: http://www.annalytics.co.uk/android/pdf/2017/04/06/Save-PDF-From-An-Android-WebView/

I don't think this converts it to a base64, but it does output a file. Do you think this helps at all?

cesarvr commented 7 years ago

Hi @gameboy9, if what the blog said is correct, I think you will be able to get a file descriptor back and you can later transform the content of the file to a base64, so i think it worth to give try. If you run the plugin using Android Studio debugging mode you can see how PrintManager interact with PrintAdapter by settings breakpoints in PDFPrint class that would give idea of what missing.

AlexChesser commented 7 years ago

Hey Guys - I've put a day into this so far and am about to put this down for a bit. I've gotten the blog post from Patrick (@gameboy9) implemented here: https://github.com/AlexChesser/pdf-generator/tree/android-base64

I've been running it in a debugger in VSCODE and there are some additional things I have to try.

If the storage-access-permissions setting works for the file creation, I think we're really off to the races

This is absolutely work-in-progress and I am very inexperienced in the android-native space so there may be some ugly stuff going on in here. I do, however, think today's shown some good progress.

cesarvr commented 7 years ago

Hi @AlexChesser , I will give it a look at your code changes as soon as I have some free time, but sound like a great news 👍 .

cheers.

AlexChesser commented 7 years ago

:) it is NOT for merging yet. A long way from it in fact. I might be able to pick up on round two tomorrow or next week.

UPDATE:

OK further progress made today. In the code I'm as far as successfully saving the PDF file to SDCARD. At the moment I'm not able to successfully READ the bytes of the file on the SDCARD (elementary stuff I'm sure, but I'm not really a strong android/java developer)

There's a built in library for converting Bytes to Base64 in android.util.Base64 this should not be confused with java.util.Base64 as cordova devaults its compilation to Java version 1.6 (there were a LOT of hours burned there)

Anyways - if I can figure out how to read the bytes of the saved file then this would actually be working.

Do any android experts know if there is a way of saving files somewhere that does not require STORAGE permissions?

jonneroelofs commented 7 years ago

Hi @AlexChesser , are you able to get the pdf returned as a base64 string on android in the success callback function? That is exactly what we need. With regard to the storage, I figured you could use the cordova file plugin to write the base64 string to a file. I think the file would then be saved in the App sandbox storage on the local file system.

Cheers,

Jonne

cesarvr commented 7 years ago

Hi @AlexChesser , here are some suggestions:

to save the file you save it you can use a temporary location: temporary file in Android

If i'm not mistaken this actually don't require user permissions.

To read the files and save it into a byte array you can use this: File to byte[]

Later you can transform this bytes to base64 to do this you can use this guide.

If you see the last two example I think you can maybe use ByteArrayOutputStream to get the data and transform it to base64 without allocating the byte array.

This should help you start. Cheers.

AlexChesser commented 7 years ago

Hey @jonneroelofs I'm almost there. Probably a couple more days work. If everything goes well, I can finish up "proof of concept" with temp files (thanks for the link Cesar) and get Base64 output by tomorrow EoD ... assuming other urgent work doesn't get in the way.

At the moment I'm successfully writing the file, but when I try to read it, it is coming back as Zero bytes. Though on-disk it clearly is NOT zero bytes.

From there I'd also need a couple days to finesse the solution. Specifically I'll need to wrap my version of the code in all the logical control structures which ensure the behaviour is consistent in iOS and android.

Essentially if the user doesn't specificy type=base64 in the JavaScript it doesn't do this. I also want to try working this as a memory-stream-only operation, but that isn't critical functionality for me. That one is just pride. I'm normally a C# developer so all this Java stuff feels a lot like trying to play a piano with oven mitts on: I know all the things that I want to do, I'm just having trouble hitting the right keys.

I imagine that by late next week I might have a pull request together.

jonneroelofs commented 7 years ago

@AlexChesser Sounds great!

AlexChesser commented 7 years ago

More great news! I just had the breakthrough that officially puts this on the home stretch. Still handling the 'finesse' portion but I am now certain we got this.

It was a few good hours trying to figure out why it wasn't working when I decided I needed to inspect a few variables at a certain breakpoint... so I changed this:

        byte[] bytes = new byte[(int)file.length()];

into this:

        int len = (int)file.length();
        byte[] bytes = new byte[len];

and suddenly everything was working.

Home run fellas!

AlexChesser commented 7 years ago

Learning how to read the JSONArray args now. Will be back at it tomorrow.

AlexChesser commented 7 years ago

I've put together a commit which will achieve successful HTML to Base64 printing https://github.com/cesarvr/pdf-generator/pull/31

cesarvr commented 7 years ago

Hi @AlexChesser looks great, I just found a small detail the base64 feature is returning an empty string I created this example project , to test the features of the plugin, you can use it to debug this issue, but looks very promising.

Thanks.

AlexChesser commented 7 years ago

Interesting. I had it working in the sample project. I wonder if I had some lingering permissions installed on the device I was using.

Will dig into it tomorrow. Will test end to end from scratch and see.

I definitely had it working and returning the full string on Friday so I'm sure it's a small thing.

AlexChesser commented 7 years ago

EDIT: ok - I'm able to replicate your issue with a fresh build. Investigating. Hmm... I can get it to work intermittently. This is really odd.

OKAY! more progress on tracking down the error. If I don't delete the file. the BASE64 shows up on the second run. So the problem is related to something to do with correctly accessing the file within the life-cycle.

So... I'm a bit fuddled for today. Will keep going tomorrow.

cesarvr commented 7 years ago

I was trying the code in local, I wrote a some temporary file mechanism (no permission required) and I was able to create a file only problem is when I open the file was filled with zeros. In your sucessful attempt, did you test transforming the base64 back to binary ?

AlexChesser commented 7 years ago

So far I've found that the file I am saving to disk in external storage IS correct. I'll spend a few hours today testing that the BASE64 I am getting matches this. I've been meaning to use the B64 as an email attachment so that's a step I need to take anyways.

Do you have that temporary file mechanism code in a branch somewhere that I could look at?

AlexChesser commented 7 years ago

UPDATE - I can confirm that the Base64 I am getting out of my version IS the expected PDF (non zero byte filled)

I've pushed the changes to your testing app which has the "email testing" sample https://github.com/AlexChesser/pdf-generator-example/tree/android/base-64

Will continue to see if I can get the PDF to be generated correctly the first time. Would love to see the working tempfile code you've got.

cesarvr commented 7 years ago

Hi @AlexChesser that's great news , sorry for late response I'm traveling with limited connectivity, I will push the code as later this day.

AlexChesser commented 7 years ago

Bit more progress on the file coming back as zeroes issue: I started looking at some logging.

Turns out we have a race-condition going on.

    06-13 13:30:01.323 27034 27034 D PDFtoBase64: onLayoutFinished
    06-13 13:30:01.333 27034 27034 D PDFtoBase64: getAsBase64
    06-13 13:30:01.963 27034 27034 D PDFtoBase64: onWriteFinished

I have to figure out how to ensure the cordova success callback doesn't get executed before the file has finished writing.

UPDATE OK! looks like changing the order of operations has given me an edge here. I think I might have it. Will update the PR with the latest code later today.

UPDATE 2 OK - pull request is updated with code which correctly handles the race condition. https://github.com/cesarvr/pdf-generator/pull/31/commits/d4801e27aad49e9774be13644770000f842bfa14

Stil love to see your code on the temp file though! I suspect yours is better since I'm really not an android developer by default.

cesarvr commented 7 years ago

Hi @AlexChesser , I just pushed the branch with the code doing the temporary folder is inside a class called Utilities , also I have moved some string literals to constants and add passing the CordovaCallback to the constructor of the class PDFtoBase64.

Also added logging methods in the try catch, so if is easy for us to track an app crash, etc.

AlexChesser commented 7 years ago

Thanks! I've incorporated your style tips into my working code and squelched them all into a single commmit: https://github.com/cesarvr/pdf-generator/pull/31 same pull request.

I didn't use the utilities class - but did use that method of creating a temp file. I love the choice of putting the cordova callback into the constructor. In hindsight, I should have seen that myself, but I was mentally stuck with the pattern of returning a string from getAsBase64

I've tested locally on an android device and it seems to work for me. Think we're probably good to merge after your approval & testing cycle.

cesarvr commented 7 years ago

Hi All, I have released a new version of the pdf-generator-plugin now supports Android base64 thanks to @AlexChesser , is a beta functionality by now, so I think we can close this issue.

jonneroelofs commented 7 years ago

@AlexChesser do you have a working example somewhere I could give a try? I cannot get it to work in my project. If I set type to base64 nothing seems to happen. I am not receiving anything in either the success or failure callback.

AlexChesser commented 7 years ago

@jonneroelofs yup, this is what I was testing with https://github.com/AlexChesser/pdf-generator-example/tree/android/base-64 (note that I had to install the spinner plugin manually).

jonneroelofs commented 7 years ago

@AlexChesser For some weird reason its working on every emulator and phone I have tested with except my own ... I have a Oneplus X . A colleague of mine has the same phone with an identical os and its even working on his phone. I was able to track down the problem to the onWriteFailed event. However the error the event provides is NULL, which is not very helpful. Do you have any idea what could be going on?

Ref: https://developer.android.com/reference/android/print/PrintDocumentAdapter.WriteResultCallback.html#onWriteFailed(java.lang.CharSequence)

image

AlexChesser commented 7 years ago

@jonneroelofs If we're talking about one specific device, the only thought I have is that one of the system modules is not up to date on that particular unit.

Is the android system version up to date? (6.0.1)? What about the android system webview APP? (58.0.3029.83)?

I don't have access to any Oneplus devices to see for myself.

jonneroelofs commented 7 years ago

@AlexChesser Thanks! The android system webview App is the problem. After I deleted it, everything started to work on my phone as well.

Cheers,

Jonne