Pdf.js with open, print and save enabled
This post shows how to use Pdf.js in Sketchware project with OPEN, PRINT AND SAVE enabled.
1. Create a new project in Sketchware pro (package name in my project is com.pdf.onweb). In main.xml add a WebView pdfWebView.
2. Switch On AppCompat and design.
3. Download pdf.js from following link:
or
4. Extract the contents of the downloaded zip file.
5. In Sketchware pro project, in Asset Manager, add a sample pdf file and rename it as sample.pdf. Also, create a new folder pdfjs.
6. In pdfjs folder import all the contents extracted from the downloaded zip file.
7. In assets folder, for showing custom error page, add error.html. Below is a sample error.html page.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Error Loading PDF</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
color: #333;
}
.error-container {
max-width: 600px;
width: 100%;
background-color: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 40px;
text-align: center;
position: relative;
overflow: hidden;
}
.error-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 6px;
background: linear-gradient(90deg, #ff6b6b, #4ecdc4, #45b7d1);
}
.error-icon {
font-size: 80px;
margin-bottom: 20px;
color: #ff6b6b;
}
h1 {
font-size: 28px;
margin-bottom: 15px;
color: #2c3e50;
}
.error-message {
font-size: 18px;
line-height: 1.6;
margin-bottom: 25px;
color: #555;
}
.error-details {
background-color: #f8f9fa;
border-left: 4px solid #4ecdc4;
padding: 15px;
margin: 25px 0;
text-align: left;
border-radius: 0 8px 8px 0;
}
.error-details h3 {
margin-bottom: 10px;
color: #2c3e50;
}
.error-details ul {
padding-left: 20px;
}
.error-details li {
margin-bottom: 8px;
}
.action-buttons {
display: flex;
flex-wrap: wrap;
gap: 15px;
justify-content: center;
margin-top: 30px;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background-color: #4ecdc4;
color: white;
}
.btn-primary:hover {
background-color: #3db8af;
transform: translateY(-2px);
}
.btn-secondary {
background-color: #f8f9fa;
color: #495057;
border: 1px solid #dee2e6;
}
.btn-secondary:hover {
background-color: #e9ecef;
transform: translateY(-2px);
}
.btn-icon {
font-size: 18px;
}
.contact-support {
margin-top: 30px;
font-size: 14px;
color: #6c757d;
}
.contact-support a {
color: #4ecdc4;
text-decoration: none;
}
.contact-support a:hover {
text-decoration: underline;
}
@media (max-width: 600px) {
.error-container {
padding: 30px 20px;
}
.action-buttons {
flex-direction: column;
}
.btn {
width: 100%;
}
}
</style>
</head>
<body>
<div class="error-container">
<div class="error-icon">📄❌</div>
<h1>We're having trouble loading this PDF</h1>
<div class="error-message">
The PDF file you're trying to access cannot be loaded. This might be due to one of the following reasons:
</div>
<div class="error-details">
<h3>Possible causes:</h3>
<ul>
<li>The file has been moved or deleted</li>
<li>There's a network connectivity issue</li>
<li>The file is corrupted or in an unsupported format</li>
<li>Your browser doesn't support PDF viewing</li>
</ul>
</div>
<div class="action-buttons">
<button class="btn btn-primary" onclick="window.location.reload()">
<span class="btn-icon">🔄</span> Try Again
</button>
<button class="btn btn-secondary" onclick="history.back()">
<span class="btn-icon">⬅️</span> Go Back
</button>
<button class="btn btn-secondary" onclick="goToHome()">
<span class="btn-icon">🏠</span> Home
</button>
</div>
<div class="contact-support">
If the problem persists, please <a href="mailto:sanjeevk4571@gmail.com">contact our support team</a>.
</div>
</div>
<script>
function goToHome() {
// Load the PDF viewer with sample.pdf
window.location.href = "file:///android_asset/pdfjs/web/viewer.html?file=" + "file:///android_asset/sample.pdf";
}
// You can customize the error message based on the specific error
function setErrorMessage(errorType) {
const titleElement = document.querySelector('h1');
const messageElement = document.querySelector('.error-message');
if (errorType === 'not-found') {
titleElement.textContent = "PDF File Not Found";
messageElement.textContent = "The PDF file you're looking for doesn't exist or has been moved. Please check the URL or contact the document owner.";
} else if (errorType === 'loading-error') {
titleElement.textContent = "Error Loading PDF";
messageElement.textContent = "We encountered an error while trying to load the PDF file. This might be due to a network issue or file corruption.";
}
// Default message is already set in HTML
}
</script>
</body>
</html>
8. In AndroidManifest Manager:
a. Click on Permissions and add following permission
android.permission.PRINT
b. Click on App Components and put following codes.
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
9. In Resource manager, add a new folder xml. Inside xml folder, create a file named file_paths.xml. In this file put following codes:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="cache" path="." />
<files-path name="files" path="." />
</paths>
10. In permission manager, add following permissions.
- android.permission.INTERNET
- android.permission.READ_EXTERNAL_STORAGE
- android.permission.WRITE_EXTERNAL_STORAGE
- android.permission.ACCESS_NETWORK_STATE
- android.permission.PRINT
11. Create a new Java file PrintDocumentAdapterFactory.java and put following codes in it.
package com.pdf.onweb;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.print.PageRange;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintDocumentInfo;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class PrintDocumentAdapterFactory {
public static PrintDocumentAdapter createPrintDocumentAdapter(Context context, Uri pdfUri) {
final Context ctx = context;
final Uri fileUri = pdfUri;
return new PrintDocumentAdapter() {
@Override
public void onLayout(PrintAttributes oldAttributes, PrintAttributes newAttributes,
CancellationSignal cancellationSignal,
LayoutResultCallback callback, Bundle extras) {
PrintDocumentInfo info = new PrintDocumentInfo
.Builder("document.pdf")
.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
.build();
callback.onLayoutFinished(info, true);
}
@Override
public void onWrite(PageRange[] pages, ParcelFileDescriptor destination,
CancellationSignal cancellationSignal,
WriteResultCallback callback) {
try {
ParcelFileDescriptor fd =
ctx.getContentResolver().openFileDescriptor(fileUri, "r");
FileInputStream input = new FileInputStream(fd.getFileDescriptor());
FileOutputStream output = new FileOutputStream(destination.getFileDescriptor());
byte[] buf = new byte[8192];
int size;
while ((size = input.read(buf)) > 0 && !cancellationSignal.isCanceled()) {
output.write(buf, 0, size);
}
callback.onWriteFinished(new PageRange[]{PageRange.ALL_PAGES});
input.close();
output.close();
} catch (IOException e) {
callback.onWriteFailed(e.getMessage());
}
}
};
}
}
12. Create a String variable pdfName.
13. Create three Custom Variables.
a. Modifier: private
Type: ValueCallback<Uri[]>
Name: filePathCallback
b. Modifier: private static final
Type: int
Name: FILE_CHOOSER_REQUEST_CODE
Initializer: 1000
c. Modifier: private
Type: DownloadHelper
Name: downloadHelper
14. Create a new Java file DownloadHelper.java and put following codes in it.
package com.pdf.onweb;
import android.app.DownloadManager;
import android.content.Context;
import android.database.Cursor;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Environment;
import android.os.Handler;
import android.util.Base64;
import android.util.Log;
import android.webkit.CookieManager;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import android.widget.Toast;
import androidx.core.content.FileProvider;
import android.print.PrintManager;
import android.print.PrintDocumentAdapter;
import android.print.PrintAttributes;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class DownloadHelper {
private Context context;
private WebView webView;
private static final String JS_INTERFACE_NAME = "Android";
public DownloadHelper(Context context, WebView webView) {
this.context = context;
this.webView = webView;
setupJavaScriptInterface();
}
// Set up JavaScript interface for WebView communication
private void setupJavaScriptInterface() {
webView.addJavascriptInterface(this, JS_INTERFACE_NAME);
}
// Handle regular HTTP/HTTPS file downloads
public void handleRegularDownload(String url, String userAgent,
String fileName, String mimeType,
long contentLength) {
try {
Uri uri = Uri.parse(url);
DownloadManager.Request request = new DownloadManager.Request(uri);
request.setMimeType(mimeType);
String cookies = CookieManager.getInstance().getCookie(url);
if (cookies != null) {
request.addRequestHeader("cookie", cookies);
}
request.addRequestHeader("User-Agent", userAgent);
request.setTitle(fileName);
request.setDescription("Downloading file");
request.allowScanningByMediaScanner();
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName);
DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
if (dm != null) {
dm.enqueue(request);
Toast.makeText(context, "Download started: " + fileName, Toast.LENGTH_LONG).show();
}
} catch (Exception e) {
Log.e("Download", "Regular download failed: " + e.getMessage());
Toast.makeText(context, "Download failed", Toast.LENGTH_SHORT).show();
}
}
// Handle blob URL downloads using JavaScript fetch API
public void handleBlobUrlDownload(String blobUrl, String fileName, String mimeType) {
try {
final String javascript = "javascript: (function() {" +
"var blobUrl = '" + blobUrl + "';" +
"fetch(blobUrl)" +
".then(response => response.blob())" +
".then(blob => {" +
" var reader = new FileReader();" +
" reader.onloadend = function() {" +
" var base64data = reader.result;" +
" " + JS_INTERFACE_NAME + ".handleBase64Data(base64data, '" + mimeType + "', '" + fileName + "');" +
" };" +
" reader.readAsDataURL(blob);" +
"})" +
".catch(error => {" +
" " + JS_INTERFACE_NAME + ".handleBase64Data('', '" + mimeType + "', '" + fileName + "');" +
"});" +
"})()";
webView.post(new Runnable() {
@Override
public void run() {
webView.loadUrl(javascript);
}
});
} catch (Exception e) {
Log.e("Download", "Blob URL download setup failed: " + e.getMessage());
Toast.makeText(context, "Download setup failed", Toast.LENGTH_SHORT).show();
}
}
// JavaScript interface method to handle base64 data from blob URLs
@JavascriptInterface
public void handleBase64Data(String base64Data, String mimeType, String fileName) {
try {
if (base64Data == null || base64Data.isEmpty()) {
throw new Exception("Empty base64 data received");
}
String pureBase64 = base64Data.substring(base64Data.indexOf(",") + 1);
byte[] fileData = Base64.decode(pureBase64, Base64.DEFAULT);
saveBase64Data(fileData, fileName, mimeType);
} catch (final Exception e) {
Log.e("Download", "Blob download failed: " + e.getMessage());
new Handler(context.getMainLooper()).post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "Download failed: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
});
}
}
// Save base64 data to Downloads directory with duplicate handling
public void saveBase64Data(byte[] data, final String fileName, String mimeType) {
try {
File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
if (!downloadsDir.exists()) {
downloadsDir.mkdirs();
}
File file = new File(downloadsDir, fileName);
int counter = 1;
String baseName = fileName;
String extension = "";
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex > 0) {
baseName = fileName.substring(0, dotIndex);
extension = fileName.substring(dotIndex);
}
String finalFileName = fileName;
while (file.exists()) {
finalFileName = baseName + " (" + counter + ")" + extension;
file = new File(downloadsDir, finalFileName);
counter++;
}
final String savedFileName = finalFileName;
FileOutputStream fos = new FileOutputStream(file);
fos.write(data);
fos.close();
MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()},
new String[]{mimeType}, null);
new Handler(context.getMainLooper()).post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "File saved: " + savedFileName, Toast.LENGTH_LONG).show();
}
});
} catch (final Exception e) {
Log.e("Download", "Save base64 data failed: " + e.getMessage());
new Handler(context.getMainLooper()).post(new Runnable() {
@Override
public void run() {
Toast.makeText(context, "Save failed: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
});
}
}
// Extract filename from URI with multiple fallback methods
public String getFileNameFromUri(Uri uri) {
String fileName = null;
try {
if ("content".equals(uri.getScheme())) {
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndex(android.provider.OpenableColumns.DISPLAY_NAME);
if (nameIndex != -1) {
fileName = cursor.getString(nameIndex);
}
}
} finally {
cursor.close();
}
}
}
if (fileName == null) {
String path = uri.getPath();
if (path != null) {
int cut = path.lastIndexOf('/');
if (cut != -1) {
fileName = path.substring(cut + 1);
}
}
}
if (fileName == null && "file".equals(uri.getScheme())) {
File file = new File(uri.getPath());
fileName = file.getName();
}
if (fileName != null && !fileName.toLowerCase().endsWith(".pdf")) {
fileName += ".pdf";
}
} catch (Exception e) {
Log.e("FilePicker", "Error getting file name from URI: " + e.getMessage());
}
return fileName;
}
// Print PDF file using Android Print Manager
public void printPdf(Context context, String pdfName) {
File pdfFile = new File(context.getCacheDir(), pdfName);
Uri fileUri = FileProvider.getUriForFile(
context,
context.getPackageName() + ".fileprovider",
pdfFile
);
PrintManager printManager = (PrintManager) context.getSystemService(Context.PRINT_SERVICE);
PrintDocumentAdapter printAdapter =
PrintDocumentAdapterFactory.createPrintDocumentAdapter(context, fileUri);
printManager.print("Printing PDF", printAdapter, new PrintAttributes.Builder().build());
}
// Copy file from URI or assets to app cache directory
public File copyToCache(Context context, String outputName, Uri uri, String assetName) {
File outFile = new File(context.getCacheDir(), outputName);
InputStream in = null;
OutputStream out = null;
try {
if (uri != null) {
in = context.getContentResolver().openInputStream(uri);
}
else if (assetName != null) {
in = context.getAssets().open(assetName);
}
else {
throw new IllegalArgumentException("Both uri and assetName are null");
}
if (in == null) {
throw new IOException("Unable to open InputStream");
}
out = new FileOutputStream(outFile);
byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
out.flush();
return outFile;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
try { if (in != null) in.close(); } catch (Exception ignored) {}
try { if (out != null) out.close(); } catch (Exception ignored) {}
}
}
// Clean up JavaScript interface when no longer needed
public void cleanup() {
try {
webView.removeJavascriptInterface(JS_INTERFACE_NAME);
} catch (Exception e) {
Log.e("Download", "Cleanup failed: " + e.getMessage());
}
}
}
15. In onCreate event, put following codes.
// Configure WebView settings for PDF.js viewer
WebSettings settings = binding.pdfWebView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowContentAccess(true);
settings.setAllowFileAccessFromFileURLs(true);
settings.setAllowUniversalAccessFromFileURLs(true);
// Initialize download helper for handling file downloads
downloadHelper = new DownloadHelper(this, binding.pdfWebView);
// Load PDF.js viewer with local PDF file
pdfName = "sample.pdf";
File cachedFile = downloadHelper.copyToCache(this, "temp.pdf", null, "sample.pdf");
String viewerUrl = "file:///android_asset/pdfjs/web/viewer.html?file=" +
"file://" + cachedFile.getAbsolutePath();
binding.pdfWebView.loadUrl(viewerUrl);
binding.pdfWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageStarted(WebView _param1, String _param2, Bitmap _param3) {
final String _url = _param2;
super.onPageStarted(_param1, _param2, _param3);
}
@Override
public void onPageFinished(WebView view, String url) {
// Override window.print() to use Android's print system
view.evaluateJavascript(
"window.print = function() { AndroidPrint.onPrintRequested(); };",
null
);
}
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
Toast.makeText(view.getContext(), "Failed to load PDF: " + error.getDescription(), Toast.LENGTH_LONG).show();
view.loadUrl("file:///android_asset/error.html");
}
});
binding.pdfWebView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams) {
// Handle file chooser for PDF.js "Open File" feature
if (MainActivity.this.filePathCallback != null) {
MainActivity.this.filePathCallback.onReceiveValue(null);
}
MainActivity.this.filePathCallback = filePathCallback;
Intent intent = fileChooserParams.createIntent();
try {
startActivityForResult(intent, FILE_CHOOSER_REQUEST_CODE);
} catch (ActivityNotFoundException e) {
MainActivity.this.filePathCallback = null;
Toast.makeText(MainActivity.this, "Cannot open file chooser", Toast.LENGTH_SHORT).show();
return false;
}
return true;
}
});
// Handle download requests from PDF.js
binding.pdfWebView.setDownloadListener(new DownloadListener() {
@Override
public void onDownloadStart(String url, String userAgent,
String contentDisposition, String mimeType,
long contentLength) {
if (url.startsWith("blob:") || url.startsWith("data:")) {
// Handle blob/data URLs (PDF.js generated downloads)
downloadHelper.handleBlobUrlDownload(url, pdfName, mimeType);
return;
}
// Handle regular HTTP downloads
downloadHelper.handleRegularDownload(url, userAgent, pdfName, mimeType, contentLength);
}
});
// JavaScript interface for handling print requests from PDF.js
binding.pdfWebView.addJavascriptInterface(new Object() {
@JavascriptInterface
public void onPrintRequested() {
runOnUiThread(new Runnable() {
@Override
public void run() {
downloadHelper.printPdf(MainActivity.this, "temp.pdf");
}
});
}
}, "AndroidPrint");
16. In import, add following.
import android.print.PrintManager;
import android.print.PrintJob;
import android.print.PrintDocumentAdapter;
import android.print.PrintAttributes;
17. Add event onActivityResult and put following codes in it.
if (_requestCode == FILE_CHOOSER_REQUEST_CODE) {
if (filePathCallback == null) return;
Uri[] result = null;
if (_resultCode == Activity.RESULT_OK && _data != null) {
if (_data.getData() != null) {
result = new Uri[]{_data.getData()};
}
}
filePathCallback.onReceiveValue(result);
filePathCallback = null;
Uri pickedUri = result[0];
if (pickedUri == null) {
Toast.makeText(this, "No file selected.", Toast.LENGTH_SHORT).show();
return;
}
pdfName = downloadHelper.getFileNameFromUri(pickedUri);
File cachedFile = downloadHelper.copyToCache(this, "temp.pdf", pickedUri, null);
if (cachedFile != null) {
String viewerUrl = "file:///android_asset/pdfjs/web/viewer.html?file=" +
"file://" + cachedFile.getAbsolutePath();
binding.pdfWebView.loadUrl(viewerUrl);
}
}
18. Add onDestroy event and put following codes in it.
if (downloadHelper != null) {
downloadHelper.cleanup();
}
19. Save and run the project.



Comments
Post a Comment