Mercurial > cgi-bin > hgweb.cgi > ClipMan
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()) +}