view src/name/blackcap/clipman/RtfToHtml.kt @ 18:96cc73ae2904

Make it more fail-safe.
author David Barts <n5jrn@me.com>
date Tue, 21 Jan 2020 16:39:02 -0800
parents be282c48010a
children 8aa2dfac27eb
line wrap: on
line source

/*
 * 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())
}