Skip to content

Commit 694158d

Browse files
committed
fix: android header and footer positioning
1 parent 962b83a commit 694158d

5 files changed

Lines changed: 698 additions & 145 deletions

File tree

android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,5 @@ dependencies {
116116
implementation "com.facebook.react:react-android"
117117
implementation project(":react-native-nitro-modules")
118118
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
119+
implementation "com.tom-roush:pdfbox-android:2.0.27.0"
119120
}
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
package android.print
2+
3+
import android.content.Context
4+
import android.os.Build
5+
import android.os.Handler
6+
import android.os.Looper
7+
import android.os.ParcelFileDescriptor
8+
import android.util.Log
9+
import android.webkit.WebView
10+
import android.webkit.WebViewClient
11+
import com.tom_roush.pdfbox.pdmodel.PDDocument
12+
import java.io.File
13+
14+
class PdfConverter private constructor() : Runnable {
15+
16+
companion object {
17+
private const val TAG = "PdfConverter"
18+
19+
@Volatile
20+
private var sInstance: PdfConverter? = null
21+
22+
@JvmStatic
23+
fun getInstance(): PdfConverter {
24+
return sInstance ?: synchronized(this) {
25+
sInstance ?: PdfConverter().also { sInstance = it }
26+
}
27+
}
28+
}
29+
30+
interface ConversionCallback {
31+
fun onSuccess(filePath: String)
32+
fun onFailure(error: String)
33+
}
34+
35+
private var mContext: Context? = null
36+
private var mHtmlString: String? = null
37+
private var mPdfFile: File? = null
38+
private var mPdfPrintAttrs: PrintAttributes? = null
39+
private var mIsCurrentlyConverting = false
40+
private var mWebView: WebView? = null
41+
private var mCallback: ConversionCallback? = null
42+
private var mBaseURL: String? = null
43+
private var mTimeoutHandler: Handler? = null
44+
private var mTimeoutRunnable: Runnable? = null
45+
private val CONVERSION_TIMEOUT_MS = 30000L
46+
47+
override fun run() {
48+
try {
49+
mContext?.let { context ->
50+
mWebView = WebView(context).apply {
51+
webViewClient = object : WebViewClient() {
52+
override fun onPageFinished(view: WebView?, url: String?) {
53+
try {
54+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
55+
throw RuntimeException("call requires API level 19")
56+
} else {
57+
val documentAdapter = createPrintDocumentAdapter()
58+
documentAdapter.onLayout(
59+
null,
60+
getPdfPrintAttrs(),
61+
null,
62+
object : PrintDocumentAdapter.LayoutResultCallback() {
63+
override fun onLayoutFailed(error: CharSequence?) {
64+
Log.e(TAG, "PDF layout failed: $error")
65+
mCallback?.onFailure(error?.toString() ?: "Layout failed")
66+
destroy()
67+
}
68+
},
69+
null
70+
)
71+
72+
documentAdapter.onWrite(
73+
arrayOf(PageRange.ALL_PAGES),
74+
getOutputFileDescriptor(),
75+
null,
76+
object : PrintDocumentAdapter.WriteResultCallback() {
77+
override fun onWriteFinished(pages: Array<PageRange>?) {
78+
try {
79+
mPdfFile?.let { file ->
80+
val myDocument = PDDocument.load(file)
81+
myDocument.close()
82+
mCallback?.onSuccess(file.absolutePath)
83+
}
84+
} catch (e: Exception) {
85+
Log.e(TAG, "Error finishing PDF write", e)
86+
mCallback?.onFailure(e.message ?: "Write error")
87+
} finally {
88+
mIsCurrentlyConverting = false
89+
destroy()
90+
}
91+
}
92+
93+
override fun onWriteFailed(error: CharSequence?) {
94+
val errorResult = error?.toString() ?: "Write failed"
95+
Log.e(TAG, "PDF write failed: $errorResult")
96+
mCallback?.onFailure(errorResult)
97+
mIsCurrentlyConverting = false
98+
destroy()
99+
}
100+
101+
override fun onWriteCancelled() {
102+
Log.d(TAG, "PDF write cancelled")
103+
mCallback?.onFailure("PDF generation was cancelled")
104+
mIsCurrentlyConverting = false
105+
destroy()
106+
}
107+
}
108+
)
109+
}
110+
} catch (e: Exception) {
111+
Log.e(TAG, "Error in onPageFinished", e)
112+
mCallback?.onFailure("Error processing loaded page: ${e.message}")
113+
mIsCurrentlyConverting = false
114+
destroy()
115+
}
116+
}
117+
118+
override fun onReceivedError(view: WebView?, errorCode: Int, description: String?, failingUrl: String?) {
119+
Log.e(TAG, "WebView error: $description (code: $errorCode)")
120+
mCallback?.onFailure("WebView error: $description")
121+
mIsCurrentlyConverting = false
122+
destroy()
123+
}
124+
}
125+
126+
settings.apply {
127+
textZoom = 100
128+
defaultTextEncodingName = "utf-8"
129+
allowFileAccess = true
130+
javaScriptEnabled = true
131+
}
132+
133+
mHtmlString?.let { html ->
134+
loadDataWithBaseURL(mBaseURL, html, "text/HTML", "utf-8", null)
135+
}
136+
}
137+
}
138+
} catch (e: Exception) {
139+
Log.e(TAG, "Error in run method", e)
140+
mCallback?.onFailure("Failed to setup WebView for PDF conversion: ${e.message}")
141+
mIsCurrentlyConverting = false
142+
destroy()
143+
}
144+
}
145+
146+
fun getPdfPrintAttrs(): PrintAttributes? {
147+
return mPdfPrintAttrs ?: getDefaultPrintAttrs()
148+
}
149+
150+
fun setPdfPrintAttrs(printAttrs: PrintAttributes?) {
151+
mPdfPrintAttrs = printAttrs
152+
}
153+
154+
/**
155+
* Force reset the converter state. Use this if the converter gets stuck.
156+
*/
157+
fun forceReset() {
158+
Log.w(TAG, "Force resetting PdfConverter state")
159+
destroy()
160+
}
161+
162+
/**
163+
* Get current conversion state for debugging
164+
*/
165+
fun isCurrentlyConverting(): Boolean {
166+
return mIsCurrentlyConverting
167+
}
168+
169+
fun convert(
170+
context: Context?,
171+
htmlString: String?,
172+
file: File?,
173+
shouldEncode: Boolean,
174+
callback: ConversionCallback?,
175+
baseURL: String?
176+
) {
177+
if (context == null) throw Exception("context can't be null")
178+
if (htmlString == null) throw Exception("htmlString can't be null")
179+
if (file == null) throw Exception("file can't be null")
180+
181+
if (mIsCurrentlyConverting) {
182+
Log.w(TAG, "PDF conversion already in progress, ignoring new request")
183+
callback?.onFailure("Another PDF conversion is currently in progress")
184+
return
185+
}
186+
187+
Log.d(TAG, "Starting PDF conversion for file: ${file.absolutePath}")
188+
189+
try {
190+
mContext = context
191+
mHtmlString = htmlString
192+
mPdfFile = file
193+
mIsCurrentlyConverting = true
194+
mCallback = callback
195+
mBaseURL = baseURL
196+
197+
setupTimeout()
198+
runOnUiThread(this)
199+
} catch (e: Exception) {
200+
Log.e(TAG, "Error setting up PDF conversion", e)
201+
mIsCurrentlyConverting = false
202+
cancelTimeout()
203+
callback?.onFailure("Failed to setup PDF conversion: ${e.message}")
204+
}
205+
}
206+
207+
private fun getOutputFileDescriptor(): ParcelFileDescriptor? {
208+
return try {
209+
mPdfFile?.let { file ->
210+
file.createNewFile()
211+
ParcelFileDescriptor.open(
212+
file,
213+
ParcelFileDescriptor.MODE_TRUNCATE or ParcelFileDescriptor.MODE_READ_WRITE
214+
)
215+
}
216+
} catch (e: Exception) {
217+
Log.e(TAG, "Failed to open ParcelFileDescriptor", e)
218+
null
219+
}
220+
}
221+
222+
private fun getDefaultPrintAttrs(): PrintAttributes? {
223+
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
224+
null
225+
} else {
226+
PrintAttributes.Builder()
227+
.setMediaSize(PrintAttributes.MediaSize.NA_LETTER)
228+
.setResolution(
229+
PrintAttributes.Resolution("RESOLUTION_ID", "RESOLUTION_ID", 600, 600)
230+
)
231+
.setMinMargins(PrintAttributes.Margins.NO_MARGINS)
232+
.build()
233+
}
234+
}
235+
236+
private fun runOnUiThread(runnable: Runnable) {
237+
mContext?.let { context ->
238+
val handler = Handler(context.mainLooper)
239+
handler.post(runnable)
240+
}
241+
}
242+
243+
private fun setupTimeout() {
244+
cancelTimeout()
245+
mTimeoutHandler = Handler(Looper.getMainLooper())
246+
mTimeoutRunnable = Runnable {
247+
Log.w(TAG, "PDF conversion timed out after ${CONVERSION_TIMEOUT_MS}ms")
248+
mCallback?.onFailure("PDF conversion timed out")
249+
destroy()
250+
}
251+
mTimeoutHandler?.postDelayed(mTimeoutRunnable!!, CONVERSION_TIMEOUT_MS)
252+
}
253+
254+
private fun cancelTimeout() {
255+
mTimeoutRunnable?.let { runnable ->
256+
mTimeoutHandler?.removeCallbacks(runnable)
257+
}
258+
mTimeoutHandler = null
259+
mTimeoutRunnable = null
260+
}
261+
262+
private fun destroy() {
263+
try {
264+
cancelTimeout()
265+
mWebView?.let { webView ->
266+
try {
267+
webView.stopLoading()
268+
webView.destroy()
269+
} catch (e: Exception) {
270+
Log.w(TAG, "Error destroying WebView", e)
271+
}
272+
}
273+
mContext = null
274+
mHtmlString = null
275+
mPdfFile = null
276+
mPdfPrintAttrs = null
277+
mWebView = null
278+
mCallback = null
279+
mBaseURL = null
280+
mIsCurrentlyConverting = false
281+
} catch (e: Exception) {
282+
Log.e(TAG, "Error in destroy() method", e)
283+
mIsCurrentlyConverting = false
284+
}
285+
}
286+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package android.print;
2+
3+
import android.os.Bundle;
4+
import android.os.CancellationSignal;
5+
import android.os.ParcelFileDescriptor;
6+
import android.util.Log;
7+
import java.io.FileOutputStream;
8+
import java.io.InputStream;
9+
10+
public class PdfPrint {
11+
private static final String TAG = "PdfPrint";
12+
private final PrintAttributes printAttributes;
13+
14+
public PdfPrint(PrintAttributes attributes) {
15+
this.printAttributes = attributes;
16+
}
17+
18+
public interface PrintCallback {
19+
void onSuccess();
20+
void onFailure(String error);
21+
}
22+
23+
public void print(PrintDocumentAdapter adapter, FileOutputStream output, PrintCallback callback) {
24+
try {
25+
Log.d(TAG, "Creating pipe...");
26+
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
27+
ParcelFileDescriptor readFd = pipe[0];
28+
ParcelFileDescriptor writeFd = pipe[1];
29+
30+
Log.d(TAG, "Calling onLayout...");
31+
adapter.onLayout(null, printAttributes, null, new PrintDocumentAdapter.LayoutResultCallback() {
32+
@Override
33+
public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
34+
Log.d(TAG, "onLayoutFinished - Pages: " + info.getPageCount());
35+
Log.d(TAG, "Calling onWrite...");
36+
adapter.onWrite(new PageRange[]{PageRange.ALL_PAGES}, writeFd, new CancellationSignal(),
37+
new PrintDocumentAdapter.WriteResultCallback() {
38+
@Override
39+
public void onWriteFinished(PageRange[] pages) {
40+
Log.d(TAG, "onWriteFinished - Pages: " + pages.length);
41+
try {
42+
writeFd.close();
43+
Log.d(TAG, "Reading from pipe...");
44+
InputStream input = new ParcelFileDescriptor.AutoCloseInputStream(readFd);
45+
byte[] buffer = new byte[8192];
46+
int bytesRead;
47+
int totalBytes = 0;
48+
while ((bytesRead = input.read(buffer)) != -1) {
49+
output.write(buffer, 0, bytesRead);
50+
totalBytes += bytesRead;
51+
}
52+
Log.d(TAG, "Wrote " + totalBytes + " bytes");
53+
output.flush();
54+
output.close();
55+
input.close();
56+
Log.d(TAG, "PDF write successful");
57+
callback.onSuccess();
58+
} catch (Exception e) {
59+
Log.e(TAG, "Error in onWriteFinished", e);
60+
callback.onFailure(e.getMessage());
61+
}
62+
}
63+
64+
@Override
65+
public void onWriteFailed(CharSequence error) {
66+
Log.e(TAG, "onWriteFailed: " + error);
67+
try {
68+
writeFd.close();
69+
readFd.close();
70+
output.close();
71+
} catch (Exception ignored) {}
72+
callback.onFailure(error != null ? error.toString() : "Write failed");
73+
}
74+
});
75+
}
76+
77+
@Override
78+
public void onLayoutFailed(CharSequence error) {
79+
Log.e(TAG, "onLayoutFailed: " + error);
80+
try {
81+
writeFd.close();
82+
readFd.close();
83+
output.close();
84+
} catch (Exception ignored) {}
85+
callback.onFailure(error != null ? error.toString() : "Layout failed");
86+
}
87+
}, new Bundle());
88+
} catch (Exception e) {
89+
Log.e(TAG, "Exception in print", e);
90+
callback.onFailure(e.getMessage());
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)