comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:be282c48010a
1 /*
2 * Because Java (and by implication Kotlin) sucks at processing RTF data,
3 * we deal with such data by invoking an external program to convert it
4 * to HTML.
5 */
6 package name.blackcap.clipman
7
8 import java.io.BufferedInputStream
9 import java.io.BufferedOutputStream
10 import java.io.ByteArrayOutputStream
11 import java.io.IOException
12 import java.io.InputStream
13 import java.io.OutputStream
14 import java.io.UnsupportedEncodingException
15
16 private val RTF_CHARSET_NAME = "UTF-8"
17 private val LANG = "en_US." + RTF_CHARSET_NAME
18 private val UNRTF = System.getenv("UNRTF")
19
20 private class Consumer(val source: InputStream): Thread() {
21 val target = ByteArrayOutputStream()
22
23 override fun run() {
24 source.use { it.copyTo(target) }
25 }
26
27 val output: String
28 @Synchronized get() {
29 if (isAlive()) {
30 throw IllegalThreadStateException("consumer not finished!")
31 } else {
32 return target.toString(RTF_CHARSET_NAME)
33 }
34 }
35 }
36
37 /**
38 * Convert an InputStream of RTF bytes to a String containing an HTML
39 * document.
40 * @param rtfStream stream containing the RTF document
41 * @return a Pair. On success, the first element contains the HTML and
42 * the second is null. On failure, the first is null and the
43 * second contains an error message.
44 */
45 public fun rtfToHtml(rtfStream: InputStream): Pair<String?, String?> {
46 if (OS.type == OS.MAC && UNRTF == null) {
47 return _rtfToHtml(rtfStream, ProcessBuilder("textutil", "-format",
48 "rtf", "-convert", "html", "-stdin", "-stdout"))
49 } else {
50 return _rtfToHtml(rtfStream, ProcessBuilder(
51 if (UNRTF == null) { "unrtf" } else { UNRTF },
52 "--html", "--nopict"))
53 }
54 }
55
56 private fun _rtfToHtml(rtfStream: InputStream, pb: ProcessBuilder): Pair<String?, String?> {
57 var job: Process? = null
58 try {
59 /* set the Posix locale to force UTF-8 I/O */
60 pb.environment().run {
61 put("LANG", LANG)
62 put("LC_ALL", LANG)
63 }
64
65 /* start the process */
66 job = pb.start()
67
68 /* start consuming its output and error streams */
69 val outputConsumer = Consumer(job.inputStream).apply { start() }
70 val errorConsumer = Consumer(job.errorStream).apply { start() }
71
72 /* feed it input */
73 job.outputStream.use { rtfStream.copyTo(it) }
74
75 /* wait for it to exit */
76 val exitStatus = job.waitFor()
77
78 /* after it exits, wait for our data consumers to exit */
79 outputConsumer.join();
80 errorConsumer.join();
81
82 /* if it barfed, return an error, else return the HTML */
83 if (exitStatus != 0) {
84 val errors = errorConsumer.output
85 if (errors.isEmpty()) {
86 return Pair(null, "converter exited with status " + exitStatus)
87 } else {
88 return Pair(null, errors)
89 }
90 }
91 return Pair(outputConsumer.output, null)
92 } catch (e: IOException) {
93 return barfed(e)
94 } catch (e: InterruptedException) {
95 if (job != null && job.isAlive()) {
96 job.destroy()
97 job.waitFor()
98 }
99 return barfed(e)
100 }
101 }
102
103 private fun barfed(e: Exception): Pair<String?, String?> {
104 val sb = StringBuilder(e::class.simpleName)
105 val message = e.message ?: ""
106 if (!message.isEmpty()) {
107 sb.append(": ")
108 sb.append(e.message)
109 }
110 return Pair(null, sb.toString())
111 }