@dimer47/capacitor-plugin-printer 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapacitorPluginPrinter.podspec +17 -0
- package/LICENSE +202 -0
- package/Package.swift +31 -0
- package/README.md +505 -0
- package/android/build.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrintAdapter.kt +73 -0
- package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrintContent.kt +78 -0
- package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrintIO.kt +136 -0
- package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrintManager.kt +219 -0
- package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrintOptions.kt +260 -0
- package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrintProxy.kt +44 -0
- package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrinterPlugin.kt +267 -0
- package/dist/esm/definitions.d.ts +307 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +13 -0
- package/dist/esm/web.js +47 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +61 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +64 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/PrinterPlugin/PrinterControllerHelper.swift +33 -0
- package/ios/Sources/PrinterPlugin/PrinterFont.swift +99 -0
- package/ios/Sources/PrinterPlugin/PrinterInfo.swift +64 -0
- package/ios/Sources/PrinterPlugin/PrinterItem.swift +104 -0
- package/ios/Sources/PrinterPlugin/PrinterLayout.swift +61 -0
- package/ios/Sources/PrinterPlugin/PrinterPaper.swift +71 -0
- package/ios/Sources/PrinterPlugin/PrinterPlugin.swift +327 -0
- package/ios/Sources/PrinterPlugin/PrinterRenderer.swift +135 -0
- package/ios/Sources/PrinterPlugin/PrinterUnit.swift +45 -0
- package/package.json +75 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
package com.nichedev.capacitor.printer
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.BitmapFactory
|
|
6
|
+
import java.io.BufferedInputStream
|
|
7
|
+
import java.io.InputStream
|
|
8
|
+
import java.net.URLConnection
|
|
9
|
+
|
|
10
|
+
class PrintContent private constructor(ctx: Context) {
|
|
11
|
+
|
|
12
|
+
enum class ContentType { PLAIN, HTML, IMAGE, PDF, UNSUPPORTED }
|
|
13
|
+
|
|
14
|
+
private val io = PrintIO(ctx)
|
|
15
|
+
|
|
16
|
+
companion object {
|
|
17
|
+
fun getContentType(path: String?, context: Context, forcedMimeType: String? = null): ContentType {
|
|
18
|
+
return PrintContent(context).getContentType(path, forcedMimeType)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fun open(path: String, context: Context): BufferedInputStream? {
|
|
22
|
+
return PrintContent(context).open(path)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fun decode(path: String, context: Context): Bitmap? {
|
|
26
|
+
return PrintContent(context).decode(path)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private fun getContentType(path: String?, forcedMimeType: String? = null): ContentType {
|
|
31
|
+
if (path == null || path.isEmpty() || path.startsWith("<")) {
|
|
32
|
+
return ContentType.HTML
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (path.matches(Regex("^[a-z0-9]+://.+"))) {
|
|
36
|
+
val mime: String? = forcedMimeType ?: if (path.startsWith("base64:")) {
|
|
37
|
+
try {
|
|
38
|
+
URLConnection.guessContentTypeFromStream(io.openBase64(path))
|
|
39
|
+
} catch (e: Exception) {
|
|
40
|
+
return ContentType.UNSUPPORTED
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
URLConnection.guessContentTypeFromName(path)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return when (mime) {
|
|
47
|
+
"image/bmp", "image/png", "image/jpeg", "image/jpeg2000",
|
|
48
|
+
"image/jp2", "image/gif", "image/x-icon",
|
|
49
|
+
"image/vnd.microsoft.icon", "image/heif" -> ContentType.IMAGE
|
|
50
|
+
"application/pdf" -> ContentType.PDF
|
|
51
|
+
else -> ContentType.UNSUPPORTED
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return ContentType.PLAIN
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private fun open(path: String): BufferedInputStream? {
|
|
59
|
+
val stream: InputStream? = when {
|
|
60
|
+
path.startsWith("res:") -> io.openResource(path)
|
|
61
|
+
path.startsWith("file:///") -> io.openFile(path)
|
|
62
|
+
path.startsWith("file://") -> io.openAsset(path)
|
|
63
|
+
path.startsWith("base64:") -> io.openBase64(path)
|
|
64
|
+
else -> null
|
|
65
|
+
}
|
|
66
|
+
return stream?.let { BufferedInputStream(it) }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private fun decode(path: String): Bitmap? {
|
|
70
|
+
return when {
|
|
71
|
+
path.startsWith("res:") -> io.decodeResource(path)
|
|
72
|
+
path.startsWith("file:///") -> io.decodeFile(path)
|
|
73
|
+
path.startsWith("file://") -> io.decodeAsset(path)
|
|
74
|
+
path.startsWith("base64:") -> io.decodeBase64(path)
|
|
75
|
+
else -> BitmapFactory.decodeFile(path)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
package com.nichedev.capacitor.printer
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.BitmapFactory
|
|
6
|
+
import android.util.Base64
|
|
7
|
+
import java.io.ByteArrayInputStream
|
|
8
|
+
import java.io.Closeable
|
|
9
|
+
import java.io.FileInputStream
|
|
10
|
+
import java.io.IOException
|
|
11
|
+
import java.io.InputStream
|
|
12
|
+
import java.io.OutputStream
|
|
13
|
+
|
|
14
|
+
class PrintIO(private val context: Context) {
|
|
15
|
+
|
|
16
|
+
companion object {
|
|
17
|
+
@Throws(IOException::class)
|
|
18
|
+
fun copy(input: InputStream, output: OutputStream) {
|
|
19
|
+
val buf = ByteArray(8192)
|
|
20
|
+
var bytesRead: Int
|
|
21
|
+
|
|
22
|
+
if (input.markSupported()) {
|
|
23
|
+
input.mark(Int.MAX_VALUE)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
while (input.read(buf).also { bytesRead = it } > 0) {
|
|
27
|
+
output.write(buf, 0, bytesRead)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (input.markSupported()) {
|
|
31
|
+
try { input.reset() } catch (_: IOException) { }
|
|
32
|
+
}
|
|
33
|
+
close(output)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fun close(stream: Closeable) {
|
|
37
|
+
try {
|
|
38
|
+
stream.close()
|
|
39
|
+
} catch (_: IOException) {
|
|
40
|
+
// ignore
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fun openFile(path: String): InputStream? {
|
|
46
|
+
if (path.length <= 7) return null
|
|
47
|
+
val absPath = path.substring(7)
|
|
48
|
+
return try {
|
|
49
|
+
FileInputStream(absPath)
|
|
50
|
+
} catch (e: Exception) {
|
|
51
|
+
null
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fun decodeFile(path: String): Bitmap? {
|
|
56
|
+
if (path.length <= 7) return null
|
|
57
|
+
val absPath = path.substring(7)
|
|
58
|
+
return BitmapFactory.decodeFile(absPath)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fun openAsset(path: String): InputStream? {
|
|
62
|
+
val resPath = path.replaceFirst("file:/", "public")
|
|
63
|
+
return try {
|
|
64
|
+
context.assets.open(resPath)
|
|
65
|
+
} catch (e: Exception) {
|
|
66
|
+
null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fun decodeAsset(path: String): Bitmap? {
|
|
71
|
+
val stream = openAsset(path) ?: return null
|
|
72
|
+
val bitmap = BitmapFactory.decodeStream(stream)
|
|
73
|
+
close(stream)
|
|
74
|
+
return bitmap
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fun openResource(path: String): InputStream? {
|
|
78
|
+
if (path.length <= 6) return null
|
|
79
|
+
val resPath = path.substring(6)
|
|
80
|
+
val resId = getResId(resPath)
|
|
81
|
+
if (resId == 0) return null
|
|
82
|
+
return try { context.resources.openRawResource(resId) } catch (_: Exception) { null }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fun decodeResource(path: String): Bitmap? {
|
|
86
|
+
if (path.length <= 6) return null
|
|
87
|
+
val resPath = path.substring(6)
|
|
88
|
+
val resId = getResId(resPath)
|
|
89
|
+
if (resId == 0) return null
|
|
90
|
+
return BitmapFactory.decodeResource(context.resources, resId)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
fun openBase64(path: String): InputStream? {
|
|
94
|
+
if (path.length <= 9) return null
|
|
95
|
+
val data = path.substring(9)
|
|
96
|
+
return try {
|
|
97
|
+
val bytes = Base64.decode(data, 0)
|
|
98
|
+
ByteArrayInputStream(bytes)
|
|
99
|
+
} catch (_: Exception) { null }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fun decodeBase64(path: String): Bitmap? {
|
|
103
|
+
if (path.length <= 9) return null
|
|
104
|
+
val data = path.substring(9)
|
|
105
|
+
return try {
|
|
106
|
+
val bytes = Base64.decode(data, 0)
|
|
107
|
+
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
108
|
+
} catch (_: Exception) { null }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private fun getResId(resPath: String): Int {
|
|
112
|
+
val res = context.resources
|
|
113
|
+
val pkgName = context.packageName
|
|
114
|
+
var dirName = "drawable"
|
|
115
|
+
var fileName = resPath
|
|
116
|
+
|
|
117
|
+
if (resPath.contains("/")) {
|
|
118
|
+
dirName = resPath.substring(0, resPath.lastIndexOf('/'))
|
|
119
|
+
fileName = resPath.substring(resPath.lastIndexOf('/') + 1)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
val dotIndex = fileName.lastIndexOf('.')
|
|
123
|
+
val resName = if (dotIndex > 0) fileName.substring(0, dotIndex) else fileName
|
|
124
|
+
var resId = res.getIdentifier(resName, dirName, pkgName)
|
|
125
|
+
|
|
126
|
+
if (resId == 0) {
|
|
127
|
+
resId = res.getIdentifier(resName, "mipmap", pkgName)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (resId == 0) {
|
|
131
|
+
resId = res.getIdentifier(resName, "drawable", pkgName)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return resId
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
package com.nichedev.capacitor.printer
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.print.PrintAttributes
|
|
6
|
+
import android.print.PrintDocumentAdapter
|
|
7
|
+
import android.print.PrintJob
|
|
8
|
+
import android.print.PrintJobInfo
|
|
9
|
+
import android.webkit.CookieManager
|
|
10
|
+
import android.webkit.WebSettings
|
|
11
|
+
import android.webkit.WebView
|
|
12
|
+
import android.webkit.WebViewClient
|
|
13
|
+
import androidx.print.PrintHelper
|
|
14
|
+
import org.json.JSONArray
|
|
15
|
+
import org.json.JSONObject
|
|
16
|
+
|
|
17
|
+
class PrintManager(private val context: Context) {
|
|
18
|
+
|
|
19
|
+
private var view: WebView? = null
|
|
20
|
+
|
|
21
|
+
fun canPrintItem(item: String?): Boolean {
|
|
22
|
+
var supported = PrintHelper.systemSupportsPrint()
|
|
23
|
+
|
|
24
|
+
if (item != null && item.isNotEmpty()) {
|
|
25
|
+
supported = PrintContent.getContentType(item, context) != PrintContent.ContentType.UNSUPPORTED
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return supported
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
companion object {
|
|
32
|
+
fun getPrintableTypes(): JSONArray {
|
|
33
|
+
return JSONArray().apply {
|
|
34
|
+
put("com.adobe.pdf")
|
|
35
|
+
put("com.microsoft.bmp")
|
|
36
|
+
put("public.jpeg")
|
|
37
|
+
put("public.jpeg-2000")
|
|
38
|
+
put("public.png")
|
|
39
|
+
put("public.heif")
|
|
40
|
+
put("com.compuserve.gif")
|
|
41
|
+
put("com.microsoft.ico")
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fun print(
|
|
47
|
+
content: String?,
|
|
48
|
+
settings: JSONObject,
|
|
49
|
+
webView: WebView,
|
|
50
|
+
callback: (Boolean) -> Unit,
|
|
51
|
+
forcedMimeType: String? = null
|
|
52
|
+
) {
|
|
53
|
+
when (PrintContent.getContentType(content, context, forcedMimeType)) {
|
|
54
|
+
PrintContent.ContentType.IMAGE -> {
|
|
55
|
+
if (content == null) { callback(false); return }
|
|
56
|
+
printImage(content, settings, callback)
|
|
57
|
+
}
|
|
58
|
+
PrintContent.ContentType.PDF -> {
|
|
59
|
+
if (content == null) { callback(false); return }
|
|
60
|
+
printPdf(content, settings, callback)
|
|
61
|
+
}
|
|
62
|
+
PrintContent.ContentType.HTML -> {
|
|
63
|
+
if (content.isNullOrEmpty()) {
|
|
64
|
+
printWebView(webView, settings, callback)
|
|
65
|
+
} else {
|
|
66
|
+
printHtml(content, settings, callback)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
PrintContent.ContentType.UNSUPPORTED,
|
|
70
|
+
PrintContent.ContentType.PLAIN -> printText(content, settings, callback)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private fun printHtml(content: String, settings: JSONObject, callback: (Boolean) -> Unit) {
|
|
75
|
+
printContent(content, "text/html", settings, callback)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private fun printText(content: String?, settings: JSONObject, callback: (Boolean) -> Unit) {
|
|
79
|
+
printContent(content ?: "", "text/plain", settings, callback)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private fun printContent(
|
|
83
|
+
content: String,
|
|
84
|
+
mimeType: String,
|
|
85
|
+
settings: JSONObject,
|
|
86
|
+
callback: (Boolean) -> Unit
|
|
87
|
+
) {
|
|
88
|
+
val activity = context as? Activity ?: run {
|
|
89
|
+
callback(false)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
activity.runOnUiThread {
|
|
94
|
+
val webView = createWebView(settings)
|
|
95
|
+
view = webView
|
|
96
|
+
|
|
97
|
+
webView.webViewClient = object : WebViewClient() {
|
|
98
|
+
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean = false
|
|
99
|
+
|
|
100
|
+
override fun onPageFinished(wv: WebView, url: String) {
|
|
101
|
+
val currentView = this@PrintManager.view ?: run {
|
|
102
|
+
callback(false)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
printWebView(currentView, settings) { result ->
|
|
106
|
+
this@PrintManager.view?.destroy()
|
|
107
|
+
this@PrintManager.view = null
|
|
108
|
+
callback(result)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
webView.loadDataWithBaseURL(
|
|
114
|
+
"file:///android_asset/public/",
|
|
115
|
+
content,
|
|
116
|
+
mimeType,
|
|
117
|
+
"UTF-8",
|
|
118
|
+
null
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private fun printWebView(view: WebView, settings: JSONObject, callback: (Boolean) -> Unit) {
|
|
124
|
+
val options = PrintOptions(settings)
|
|
125
|
+
val jobName = options.getJobName()
|
|
126
|
+
|
|
127
|
+
val activity = context as? Activity ?: run {
|
|
128
|
+
callback(false)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
activity.runOnUiThread {
|
|
133
|
+
val adapter: PrintDocumentAdapter = view.createPrintDocumentAdapter(jobName)
|
|
134
|
+
val proxy = PrintProxy(adapter) {
|
|
135
|
+
callback(isPrintJobCompleted(jobName))
|
|
136
|
+
}
|
|
137
|
+
printAdapter(proxy, options)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private fun printPdf(path: String, settings: JSONObject, callback: (Boolean) -> Unit) {
|
|
142
|
+
val stream = PrintContent.open(path, context) ?: run {
|
|
143
|
+
callback(false)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
val options = PrintOptions(settings)
|
|
148
|
+
val jobName = options.getJobName()
|
|
149
|
+
val pageCount = options.getPageCount()
|
|
150
|
+
val adapter = PrintAdapter(jobName, pageCount, stream) {
|
|
151
|
+
callback(isPrintJobCompleted(jobName))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
printAdapter(adapter, options)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private fun printAdapter(adapter: PrintDocumentAdapter, options: PrintOptions) {
|
|
158
|
+
val jobName = options.getJobName()
|
|
159
|
+
val attrs = options.toPrintAttributes()
|
|
160
|
+
getPrintService().print(jobName, adapter, attrs)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private fun printImage(path: String, settings: JSONObject, callback: (Boolean) -> Unit) {
|
|
164
|
+
val bitmap = PrintContent.decode(path, context) ?: run {
|
|
165
|
+
callback(false)
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
val options = PrintOptions(settings)
|
|
170
|
+
val printer = PrintHelper(context)
|
|
171
|
+
val jobName = options.getJobName()
|
|
172
|
+
|
|
173
|
+
options.decoratePrintHelper(printer)
|
|
174
|
+
|
|
175
|
+
printer.printBitmap(jobName, bitmap) {
|
|
176
|
+
callback(isPrintJobCompleted(jobName))
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private fun createWebView(settings: JSONObject): WebView {
|
|
181
|
+
val jsEnabled = settings.optBoolean("javascript", false)
|
|
182
|
+
val webView = WebView(context)
|
|
183
|
+
val spec = webView.settings
|
|
184
|
+
val font = settings.optJSONObject("font")
|
|
185
|
+
|
|
186
|
+
spec.setSupportZoom(true)
|
|
187
|
+
spec.useWideViewPort = true
|
|
188
|
+
spec.javaScriptEnabled = jsEnabled
|
|
189
|
+
|
|
190
|
+
if (font != null && font.has("size")) {
|
|
191
|
+
spec.defaultFixedFontSize = font.optInt("size", 16)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
spec.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
|
195
|
+
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)
|
|
196
|
+
|
|
197
|
+
return webView
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private fun findPrintJobByName(jobName: String): PrintJob? {
|
|
201
|
+
for (job in getPrintService().printJobs) {
|
|
202
|
+
if (job.info.label == jobName) {
|
|
203
|
+
return job
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private fun isPrintJobCompleted(jobName: String): Boolean {
|
|
210
|
+
val job = findPrintJobByName(jobName) ?: return true
|
|
211
|
+
return job.info.state != PrintJobInfo.STATE_FAILED &&
|
|
212
|
+
job.info.state != PrintJobInfo.STATE_CANCELED
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private fun getPrintService(): android.print.PrintManager {
|
|
216
|
+
return context.getSystemService(Context.PRINT_SERVICE) as? android.print.PrintManager
|
|
217
|
+
?: throw IllegalStateException("Print service not available")
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
package com.nichedev.capacitor.printer
|
|
2
|
+
|
|
3
|
+
import android.print.PrintAttributes
|
|
4
|
+
import android.print.PrintDocumentInfo
|
|
5
|
+
import androidx.print.PrintHelper
|
|
6
|
+
import org.json.JSONObject
|
|
7
|
+
import kotlin.math.roundToInt
|
|
8
|
+
|
|
9
|
+
class PrintOptions(private val spec: JSONObject) {
|
|
10
|
+
|
|
11
|
+
companion object {
|
|
12
|
+
/** Named paper sizes mapped to PrintAttributes.MediaSize constants. */
|
|
13
|
+
private val NAMED_SIZES: Map<String, PrintAttributes.MediaSize> = mapOf(
|
|
14
|
+
// ISO A series
|
|
15
|
+
"A0" to PrintAttributes.MediaSize.ISO_A0,
|
|
16
|
+
"A1" to PrintAttributes.MediaSize.ISO_A1,
|
|
17
|
+
"A2" to PrintAttributes.MediaSize.ISO_A2,
|
|
18
|
+
"A3" to PrintAttributes.MediaSize.ISO_A3,
|
|
19
|
+
"A4" to PrintAttributes.MediaSize.ISO_A4,
|
|
20
|
+
"A5" to PrintAttributes.MediaSize.ISO_A5,
|
|
21
|
+
"A6" to PrintAttributes.MediaSize.ISO_A6,
|
|
22
|
+
"A7" to PrintAttributes.MediaSize.ISO_A7,
|
|
23
|
+
"A8" to PrintAttributes.MediaSize.ISO_A8,
|
|
24
|
+
"A9" to PrintAttributes.MediaSize.ISO_A9,
|
|
25
|
+
"A10" to PrintAttributes.MediaSize.ISO_A10,
|
|
26
|
+
// ISO B series
|
|
27
|
+
"B0" to PrintAttributes.MediaSize.ISO_B0,
|
|
28
|
+
"B1" to PrintAttributes.MediaSize.ISO_B1,
|
|
29
|
+
"B2" to PrintAttributes.MediaSize.ISO_B2,
|
|
30
|
+
"B3" to PrintAttributes.MediaSize.ISO_B3,
|
|
31
|
+
"B4" to PrintAttributes.MediaSize.ISO_B4,
|
|
32
|
+
"B5" to PrintAttributes.MediaSize.ISO_B5,
|
|
33
|
+
"B6" to PrintAttributes.MediaSize.ISO_B6,
|
|
34
|
+
"B7" to PrintAttributes.MediaSize.ISO_B7,
|
|
35
|
+
"B8" to PrintAttributes.MediaSize.ISO_B8,
|
|
36
|
+
"B9" to PrintAttributes.MediaSize.ISO_B9,
|
|
37
|
+
"B10" to PrintAttributes.MediaSize.ISO_B10,
|
|
38
|
+
// ISO C series
|
|
39
|
+
"C0" to PrintAttributes.MediaSize.ISO_C0,
|
|
40
|
+
"C1" to PrintAttributes.MediaSize.ISO_C1,
|
|
41
|
+
"C2" to PrintAttributes.MediaSize.ISO_C2,
|
|
42
|
+
"C3" to PrintAttributes.MediaSize.ISO_C3,
|
|
43
|
+
"C4" to PrintAttributes.MediaSize.ISO_C4,
|
|
44
|
+
"C5" to PrintAttributes.MediaSize.ISO_C5,
|
|
45
|
+
"C6" to PrintAttributes.MediaSize.ISO_C6,
|
|
46
|
+
"C7" to PrintAttributes.MediaSize.ISO_C7,
|
|
47
|
+
"C8" to PrintAttributes.MediaSize.ISO_C8,
|
|
48
|
+
"C9" to PrintAttributes.MediaSize.ISO_C9,
|
|
49
|
+
"C10" to PrintAttributes.MediaSize.ISO_C10,
|
|
50
|
+
// North America
|
|
51
|
+
"LETTER" to PrintAttributes.MediaSize.NA_LETTER,
|
|
52
|
+
"LEGAL" to PrintAttributes.MediaSize.NA_LEGAL,
|
|
53
|
+
"TABLOID" to PrintAttributes.MediaSize.NA_TABLOID,
|
|
54
|
+
"LEDGER" to PrintAttributes.MediaSize.NA_LEDGER,
|
|
55
|
+
"JUNIOR_LEGAL" to PrintAttributes.MediaSize.NA_JUNIOR_LEGAL,
|
|
56
|
+
"GOVT_LETTER" to PrintAttributes.MediaSize.NA_GOVT_LETTER,
|
|
57
|
+
"INDEX_3X5" to PrintAttributes.MediaSize.NA_INDEX_3X5,
|
|
58
|
+
"INDEX_4X6" to PrintAttributes.MediaSize.NA_INDEX_4X6,
|
|
59
|
+
"4X6" to PrintAttributes.MediaSize.NA_INDEX_4X6,
|
|
60
|
+
"INDEX_5X8" to PrintAttributes.MediaSize.NA_INDEX_5X8,
|
|
61
|
+
"MONARCH" to PrintAttributes.MediaSize.NA_MONARCH,
|
|
62
|
+
"QUARTO" to PrintAttributes.MediaSize.NA_QUARTO,
|
|
63
|
+
"FOOLSCAP" to PrintAttributes.MediaSize.NA_FOOLSCAP,
|
|
64
|
+
// JIS
|
|
65
|
+
"JIS_B0" to PrintAttributes.MediaSize.JIS_B0,
|
|
66
|
+
"JIS_B1" to PrintAttributes.MediaSize.JIS_B1,
|
|
67
|
+
"JIS_B2" to PrintAttributes.MediaSize.JIS_B2,
|
|
68
|
+
"JIS_B3" to PrintAttributes.MediaSize.JIS_B3,
|
|
69
|
+
"JIS_B4" to PrintAttributes.MediaSize.JIS_B4,
|
|
70
|
+
"JIS_B5" to PrintAttributes.MediaSize.JIS_B5,
|
|
71
|
+
"JIS_B6" to PrintAttributes.MediaSize.JIS_B6,
|
|
72
|
+
"JIS_B7" to PrintAttributes.MediaSize.JIS_B7,
|
|
73
|
+
"JIS_B8" to PrintAttributes.MediaSize.JIS_B8,
|
|
74
|
+
"JIS_B9" to PrintAttributes.MediaSize.JIS_B9,
|
|
75
|
+
"JIS_B10" to PrintAttributes.MediaSize.JIS_B10,
|
|
76
|
+
"JIS_EXEC" to PrintAttributes.MediaSize.JIS_EXEC,
|
|
77
|
+
// Japanese
|
|
78
|
+
"JPN_CHOU2" to PrintAttributes.MediaSize.JPN_CHOU2,
|
|
79
|
+
"JPN_CHOU3" to PrintAttributes.MediaSize.JPN_CHOU3,
|
|
80
|
+
"JPN_CHOU4" to PrintAttributes.MediaSize.JPN_CHOU4,
|
|
81
|
+
"JPN_HAGAKI" to PrintAttributes.MediaSize.JPN_HAGAKI,
|
|
82
|
+
"JPN_OUFUKU" to PrintAttributes.MediaSize.JPN_OUFUKU,
|
|
83
|
+
"JPN_KAHU" to PrintAttributes.MediaSize.JPN_KAHU,
|
|
84
|
+
"JPN_KAKU2" to PrintAttributes.MediaSize.JPN_KAKU2,
|
|
85
|
+
"JPN_YOU4" to PrintAttributes.MediaSize.JPN_YOU4,
|
|
86
|
+
// Chinese
|
|
87
|
+
"ROC_8K" to PrintAttributes.MediaSize.ROC_8K,
|
|
88
|
+
"ROC_16K" to PrintAttributes.MediaSize.ROC_16K,
|
|
89
|
+
"PRC_1" to PrintAttributes.MediaSize.PRC_1,
|
|
90
|
+
"PRC_2" to PrintAttributes.MediaSize.PRC_2,
|
|
91
|
+
"PRC_3" to PrintAttributes.MediaSize.PRC_3,
|
|
92
|
+
"PRC_4" to PrintAttributes.MediaSize.PRC_4,
|
|
93
|
+
"PRC_5" to PrintAttributes.MediaSize.PRC_5,
|
|
94
|
+
"PRC_6" to PrintAttributes.MediaSize.PRC_6,
|
|
95
|
+
"PRC_7" to PrintAttributes.MediaSize.PRC_7,
|
|
96
|
+
"PRC_8" to PrintAttributes.MediaSize.PRC_8,
|
|
97
|
+
"PRC_9" to PrintAttributes.MediaSize.PRC_9,
|
|
98
|
+
"PRC_10" to PrintAttributes.MediaSize.PRC_10,
|
|
99
|
+
"PRC_16K" to PrintAttributes.MediaSize.PRC_16K,
|
|
100
|
+
"OM_PA_KAI" to PrintAttributes.MediaSize.OM_PA_KAI,
|
|
101
|
+
"OM_DAI_PA_KAI" to PrintAttributes.MediaSize.OM_DAI_PA_KAI,
|
|
102
|
+
"OM_JUURO_KU_KAI" to PrintAttributes.MediaSize.OM_JUURO_KU_KAI,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
/** Convert a unit string (e.g., '10mm', '1in', '2cm') to mils (thousandths of inch). */
|
|
106
|
+
fun toMils(value: Any?): Int {
|
|
107
|
+
if (value == null) return 0
|
|
108
|
+
if (value is Number) return (value.toDouble() * 1000.0 / 72.0).roundToInt()
|
|
109
|
+
|
|
110
|
+
val str = value.toString()
|
|
111
|
+
if (str.isEmpty()) return 0
|
|
112
|
+
|
|
113
|
+
return when {
|
|
114
|
+
str.endsWith("mm") -> {
|
|
115
|
+
val v = str.dropLast(2).toDoubleOrNull() ?: 0.0
|
|
116
|
+
(v * 39.3701).roundToInt()
|
|
117
|
+
}
|
|
118
|
+
str.endsWith("cm") -> {
|
|
119
|
+
val v = str.dropLast(2).toDoubleOrNull() ?: 0.0
|
|
120
|
+
(v * 393.701).roundToInt()
|
|
121
|
+
}
|
|
122
|
+
str.endsWith("in") -> {
|
|
123
|
+
val v = str.dropLast(2).toDoubleOrNull() ?: 0.0
|
|
124
|
+
(v * 1000.0).roundToInt()
|
|
125
|
+
}
|
|
126
|
+
str.endsWith("pt") -> {
|
|
127
|
+
val v = str.dropLast(2).toDoubleOrNull() ?: 0.0
|
|
128
|
+
(v * 1000.0 / 72.0).roundToInt()
|
|
129
|
+
}
|
|
130
|
+
else -> {
|
|
131
|
+
val v = str.toDoubleOrNull() ?: 0.0
|
|
132
|
+
(v * 1000.0 / 72.0).roundToInt()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fun getJobName(): String {
|
|
139
|
+
val jobName = spec.optString("name", "")
|
|
140
|
+
return if (jobName.isEmpty()) {
|
|
141
|
+
"Printer Plugin Job #${System.currentTimeMillis()}"
|
|
142
|
+
} else {
|
|
143
|
+
jobName
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
fun getPageCount(): Int {
|
|
148
|
+
val count = spec.optInt("pageCount", PrintDocumentInfo.PAGE_COUNT_UNKNOWN)
|
|
149
|
+
return if (count <= 0) PrintDocumentInfo.PAGE_COUNT_UNKNOWN else count
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fun toPrintAttributes(): PrintAttributes {
|
|
153
|
+
val builder = PrintAttributes.Builder()
|
|
154
|
+
val margin = spec.opt("margin")
|
|
155
|
+
val paper = spec.optJSONObject("paper")
|
|
156
|
+
val orientation = spec.optString("orientation", "")
|
|
157
|
+
|
|
158
|
+
// Resolve media size from paper option or orientation
|
|
159
|
+
val mediaSize = resolveMediaSize(paper, orientation)
|
|
160
|
+
if (mediaSize != null) {
|
|
161
|
+
builder.setMediaSize(mediaSize)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Color mode
|
|
165
|
+
if (spec.has("monochrome")) {
|
|
166
|
+
if (spec.optBoolean("monochrome")) {
|
|
167
|
+
builder.setColorMode(PrintAttributes.COLOR_MODE_MONOCHROME)
|
|
168
|
+
} else {
|
|
169
|
+
builder.setColorMode(PrintAttributes.COLOR_MODE_COLOR)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Margins: false → NO_MARGINS, object → custom margins
|
|
174
|
+
if (margin is Boolean && !margin) {
|
|
175
|
+
builder.setMinMargins(PrintAttributes.Margins.NO_MARGINS)
|
|
176
|
+
} else if (margin is JSONObject) {
|
|
177
|
+
val left = toMils(margin.opt("left"))
|
|
178
|
+
val top = toMils(margin.opt("top"))
|
|
179
|
+
val right = toMils(margin.opt("right"))
|
|
180
|
+
val bottom = toMils(margin.opt("bottom"))
|
|
181
|
+
builder.setMinMargins(PrintAttributes.Margins(left, top, right, bottom))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Duplex
|
|
185
|
+
when (spec.optString("duplex")) {
|
|
186
|
+
"long" -> builder.setDuplexMode(PrintAttributes.DUPLEX_MODE_LONG_EDGE)
|
|
187
|
+
"short" -> builder.setDuplexMode(PrintAttributes.DUPLEX_MODE_SHORT_EDGE)
|
|
188
|
+
"none" -> builder.setDuplexMode(PrintAttributes.DUPLEX_MODE_NONE)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return builder.build()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fun decoratePrintHelper(printer: PrintHelper) {
|
|
195
|
+
when (spec.optString("orientation")) {
|
|
196
|
+
"landscape" -> printer.orientation = PrintHelper.ORIENTATION_LANDSCAPE
|
|
197
|
+
"portrait" -> printer.orientation = PrintHelper.ORIENTATION_PORTRAIT
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (spec.has("monochrome")) {
|
|
201
|
+
if (spec.optBoolean("monochrome")) {
|
|
202
|
+
printer.colorMode = PrintHelper.COLOR_MODE_MONOCHROME
|
|
203
|
+
} else {
|
|
204
|
+
printer.colorMode = PrintHelper.COLOR_MODE_COLOR
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (spec.optBoolean("autoFit", true)) {
|
|
209
|
+
printer.scaleMode = PrintHelper.SCALE_MODE_FIT
|
|
210
|
+
} else {
|
|
211
|
+
printer.scaleMode = PrintHelper.SCALE_MODE_FILL
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private fun resolveMediaSize(
|
|
216
|
+
paper: JSONObject?,
|
|
217
|
+
orientation: String
|
|
218
|
+
): PrintAttributes.MediaSize? {
|
|
219
|
+
var mediaSize: PrintAttributes.MediaSize? = null
|
|
220
|
+
|
|
221
|
+
if (paper != null) {
|
|
222
|
+
val name = paper.optString("name", "").uppercase()
|
|
223
|
+
|
|
224
|
+
if (name.isNotEmpty()) {
|
|
225
|
+
mediaSize = NAMED_SIZES[name]
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Custom width/height in paper option (values with units → mils)
|
|
229
|
+
if (mediaSize == null) {
|
|
230
|
+
val widthMils = toMils(paper.opt("width"))
|
|
231
|
+
val heightMils = toMils(paper.opt("height"))
|
|
232
|
+
if (widthMils > 0 && heightMils > 0) {
|
|
233
|
+
mediaSize = PrintAttributes.MediaSize(
|
|
234
|
+
"custom_${widthMils}x${heightMils}",
|
|
235
|
+
"Custom ${widthMils}x${heightMils}",
|
|
236
|
+
widthMils,
|
|
237
|
+
heightMils
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Apply orientation: if landscape, swap to landscape variant
|
|
244
|
+
if (mediaSize != null) {
|
|
245
|
+
mediaSize = when (orientation) {
|
|
246
|
+
"landscape" -> if (mediaSize.isPortrait) mediaSize.asLandscape() else mediaSize
|
|
247
|
+
"portrait" -> if (!mediaSize.isPortrait) mediaSize.asPortrait() else mediaSize
|
|
248
|
+
else -> mediaSize
|
|
249
|
+
}
|
|
250
|
+
return mediaSize
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Fallback: orientation only (no paper specified)
|
|
254
|
+
return when (orientation) {
|
|
255
|
+
"landscape" -> PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE
|
|
256
|
+
"portrait" -> PrintAttributes.MediaSize.UNKNOWN_PORTRAIT
|
|
257
|
+
else -> null
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|