diff src/name/blackcap/clipman/RtfToHtml.kt @ 0:be282c48010a

Incomplete; checking it in as a backup.
author David Barts <n5jrn@me.com>
date Tue, 14 Jan 2020 14:07:19 -0800
parents
children 8aa2dfac27eb
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/name/blackcap/clipman/RtfToHtml.kt	Tue Jan 14 14:07:19 2020 -0800
@@ -0,0 +1,111 @@
+/*
+ * Because Java (and by implication Kotlin) sucks at processing RTF data,
+ * we deal with such data by invoking an external program to convert it
+ * to HTML.
+ */
+package name.blackcap.clipman
+
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.UnsupportedEncodingException
+
+private val RTF_CHARSET_NAME = "UTF-8"
+private val LANG = "en_US." + RTF_CHARSET_NAME
+private val UNRTF = System.getenv("UNRTF")
+
+private class Consumer(val source: InputStream): Thread() {
+    val target = ByteArrayOutputStream()
+
+    override fun run() {
+        source.use { it.copyTo(target) }
+    }
+
+    val output: String
+        @Synchronized get() {
+            if (isAlive()) {
+                throw IllegalThreadStateException("consumer not finished!")
+            } else {
+                return target.toString(RTF_CHARSET_NAME)
+            }
+        }
+}
+
+/**
+ * Convert an InputStream of RTF bytes to a String containing an HTML
+ * document.
+ * @param rtfStream stream containing the RTF document
+ * @return a Pair. On success, the first element contains the HTML and
+ *         the second is null. On failure, the first is null and the
+ *         second contains an error message.
+ */
+public fun rtfToHtml(rtfStream: InputStream): Pair<String?, String?> {
+    if (OS.type == OS.MAC && UNRTF == null) {
+        return _rtfToHtml(rtfStream, ProcessBuilder("textutil", "-format",
+            "rtf", "-convert", "html", "-stdin", "-stdout"))
+    } else {
+        return _rtfToHtml(rtfStream, ProcessBuilder(
+            if (UNRTF == null) { "unrtf" } else { UNRTF },
+            "--html", "--nopict"))
+    }
+}
+
+private fun _rtfToHtml(rtfStream: InputStream, pb: ProcessBuilder): Pair<String?, String?> {
+    var job: Process? = null
+    try {
+        /* set the Posix locale to force UTF-8 I/O */
+        pb.environment().run {
+            put("LANG", LANG)
+            put("LC_ALL", LANG)
+        }
+
+        /* start the process */
+        job = pb.start()
+
+        /* start consuming its output and error streams */
+        val outputConsumer = Consumer(job.inputStream).apply { start() }
+        val errorConsumer = Consumer(job.errorStream).apply { start() }
+
+        /* feed it input */
+        job.outputStream.use { rtfStream.copyTo(it) }
+
+        /* wait for it to exit */
+        val exitStatus = job.waitFor()
+
+        /* after it exits, wait for our data consumers to exit */
+        outputConsumer.join();
+        errorConsumer.join();
+
+        /* if it barfed, return an error, else return the HTML */
+        if (exitStatus != 0) {
+            val errors = errorConsumer.output
+            if (errors.isEmpty()) {
+                return Pair(null, "converter exited with status " + exitStatus)
+            } else {
+                return Pair(null, errors)
+            }
+        }
+        return Pair(outputConsumer.output, null)
+    } catch (e: IOException) {
+        return barfed(e)
+    } catch (e: InterruptedException) {
+        if (job != null && job.isAlive()) {
+            job.destroy()
+            job.waitFor()
+        }
+        return barfed(e)
+    }
+}
+
+private fun barfed(e: Exception): Pair<String?, String?> {
+    val sb = StringBuilder(e::class.simpleName)
+    val message = e.message ?: ""
+    if (!message.isEmpty()) {
+        sb.append(": ")
+        sb.append(e.message)
+    }
+    return Pair(null, sb.toString())
+}