@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.
Files changed (36) hide show
  1. package/CapacitorPluginPrinter.podspec +17 -0
  2. package/LICENSE +202 -0
  3. package/Package.swift +31 -0
  4. package/README.md +505 -0
  5. package/android/build.gradle +68 -0
  6. package/android/src/main/AndroidManifest.xml +3 -0
  7. package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrintAdapter.kt +73 -0
  8. package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrintContent.kt +78 -0
  9. package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrintIO.kt +136 -0
  10. package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrintManager.kt +219 -0
  11. package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrintOptions.kt +260 -0
  12. package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrintProxy.kt +44 -0
  13. package/android/src/main/kotlin/com/nichedev/capacitor/printer/PrinterPlugin.kt +267 -0
  14. package/dist/esm/definitions.d.ts +307 -0
  15. package/dist/esm/definitions.js +2 -0
  16. package/dist/esm/definitions.js.map +1 -0
  17. package/dist/esm/index.d.ts +4 -0
  18. package/dist/esm/index.js +7 -0
  19. package/dist/esm/index.js.map +1 -0
  20. package/dist/esm/web.d.ts +13 -0
  21. package/dist/esm/web.js +47 -0
  22. package/dist/esm/web.js.map +1 -0
  23. package/dist/plugin.cjs.js +61 -0
  24. package/dist/plugin.cjs.js.map +1 -0
  25. package/dist/plugin.js +64 -0
  26. package/dist/plugin.js.map +1 -0
  27. package/ios/Sources/PrinterPlugin/PrinterControllerHelper.swift +33 -0
  28. package/ios/Sources/PrinterPlugin/PrinterFont.swift +99 -0
  29. package/ios/Sources/PrinterPlugin/PrinterInfo.swift +64 -0
  30. package/ios/Sources/PrinterPlugin/PrinterItem.swift +104 -0
  31. package/ios/Sources/PrinterPlugin/PrinterLayout.swift +61 -0
  32. package/ios/Sources/PrinterPlugin/PrinterPaper.swift +71 -0
  33. package/ios/Sources/PrinterPlugin/PrinterPlugin.swift +327 -0
  34. package/ios/Sources/PrinterPlugin/PrinterRenderer.swift +135 -0
  35. package/ios/Sources/PrinterPlugin/PrinterUnit.swift +45 -0
  36. 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
+ }