|
| 1 | +# -*- coding: UTF-8 -*- |
| 2 | +"""Baudot Codec - baudot content conversion to HTML. |
| 3 | +
|
| 4 | +This codec: |
| 5 | +- en/decodes strings from str to str |
| 6 | +- en/decodes strings from bytes to bytes |
| 7 | +- decodes file content to str (read) |
| 8 | +- encodes file content from str to bytes (write) |
| 9 | +""" |
| 10 | +from ..__common__ import * |
| 11 | + |
| 12 | + |
| 13 | +PATTERN = r"^baudot%s([-_](?:ccitt1|ccitt2|eu|fr|ita1|ita2|ita2[-_](?:us|meteo)" + (r"|mtk2" if PY3 else r"") + \ |
| 14 | + r"|murray|uk|us_tty)(?:[-_](?:lsb|msb))?)?$" |
| 15 | +# reserved character |
| 16 | +RES_CHR = "\xff" |
| 17 | + |
| 18 | +# sources: |
| 19 | +# - http://rabbit.eng.miami.edu/info/baudot.html |
| 20 | +# - https://en.wikipedia.org/wiki/Baudot_code |
| 21 | +# - https://fr.qwe.wiki/wiki/Baudot_code |
| 22 | +# all alphabets consider MSB by default |
| 23 | +# CCITT-1 original Baudot code (source: http://rabbit.eng.miami.edu/info/baudot.html) |
| 24 | +CCITT1 = [ |
| 25 | + "00001", "00010", |
| 26 | + "\x00\xff\xff\xffA-JKEXGM/ZHLYSBRUTCQIWFNOVDP", |
| 27 | + "\x00\xff\xff\xff1.6(2\xff7)\xff:\xff=3\xff8-4\xff9/\xff?\xff£5'0+", |
| 28 | +] |
| 29 | +# CCITT-2 revised Baudot code (source: http://rabbit.eng.miami.edu/info/baudot.html) |
| 30 | +CCITT2 = [ |
| 31 | + "11111", "11011", |
| 32 | + "\x00E\nA SIU\rDRJNFCKTZLWHYPQOBG\xffMXV\xff", |
| 33 | + "\x003\n- \x0787\r$4',!:(5\")2#6019?&\xff./;\xff", |
| 34 | +] |
| 35 | +# Original Baudot (French/European ; sources: https://fr.qwe.wiki/wiki/Baudot_code |
| 36 | +# https://en.wikipedia.org/wiki/Baudot_code) |
| 37 | +BAUDOT = EU = FR = [ |
| 38 | + "10000", "01000", |
| 39 | + "\x00AEÉYUIO\xffJGHBCFD \nXZSTWV\x7fKMLRQNP", |
| 40 | + "\x0012&34°5 67h89f0\xff.,:;!?'\x7f()=-/\u2116%", |
| 41 | +] |
| 42 | +# International Telegraphic Alphabet 1 (sources: https://fr.qwe.wiki/wiki/Baudot_code |
| 43 | +# https://en.wikipedia.org/wiki/Baudot_code) |
| 44 | +ITA1 = [ |
| 45 | + "10000", "01000", |
| 46 | + "\x00AE\rYUIO\xffJGHBCFD \xffXZSTWV\x7fKMLRQNP", |
| 47 | + "\x0012\r34\xff5 67+89\xff0\xff\n,:.\xff?'\x7f()=-/\xff%", |
| 48 | +] |
| 49 | +# International Telegraphic Alphabet 2 (sources: https://fr.qwe.wiki/wiki/Baudot_code |
| 50 | +# https://en.wikipedia.org/wiki/Baudot_code) |
| 51 | +ITA2 = [ |
| 52 | + "11111", "11011", |
| 53 | + "\x00E\nA SIU\rDRJNFCKTZLWHYPQOBG\xffMXV\xff", |
| 54 | + "\x003\n- '87\r\x054\x07,!:(5+)2$6019?&\xff./=\xff", |
| 55 | +] |
| 56 | +# International Telegraphic Alphabet 2 - US TTY (sources: https://fr.qwe.wiki/wiki/Baudot_code |
| 57 | +# https://en.wikipedia.org/wiki/Baudot_code) |
| 58 | +ITA2_US = US_TTY = [ |
| 59 | + "11111", "11011", |
| 60 | + "\x00E\nA SIU\rDRJNFCKTZLWHYPQOBG\xffMXV\xff", |
| 61 | + "\x003\n- \x0787\r$4',!:(5\")2#6019?&\xff./;\xff", |
| 62 | +] |
| 63 | +# International Telegraphic Alphabet 2 - Meteo version (source: https://en.wikipedia.org/wiki/Baudot_code) |
| 64 | +ITA2_METEO = [ |
| 65 | + "11111", "11011", |
| 66 | + "\x00E\nA SIU\rDRJNFCKTZLWHYPQOBG\xffMXV\xff", |
| 67 | + "-3\n\u2191 \x0787\r\u21974\u2199\u29b7\u2192\u25ef\u21905+\u21962\u21936019\u2295\u2198\xff./\u29b6\xff", |
| 68 | +] |
| 69 | +# Russian MTK-2 alphabet (source: https://fr.qwe.wiki/wiki/Baudot_code) |
| 70 | +MTK2 = [ |
| 71 | + "11111", "11011", |
| 72 | + "\x00Е\n\xff СИУ\r\xffРЙНФЦКТЗЛВХЫПЯОБГ\xffМЬЖ\xff", |
| 73 | + "\x003\n- '87\r\xff4Ю,Э:(5+)2Щ6019?Ш\xff./=\xff", |
| 74 | +] |
| 75 | +# Murray code ; NB: not all fractions are supported (source: https://en.wikipedia.org/wiki/Baudot_code) |
| 76 | +MURRAY = [ |
| 77 | + "00100", "11011", |
| 78 | + " E\xffA\xffSIU\nDRJNFCKTZLWHYPQOBF\xffMXV\x7f", |
| 79 | + "\x003\xff\xff\xff'87\n²4\xff-\u215f(\xff5./2\xff6019?\xff\xff,£)*", |
| 80 | +] |
| 81 | +# English Baudot ; NB: not all fractions are supported (sources: https://fr.qwe.wiki/wiki/Baudot_code |
| 82 | +# https://en.wikipedia.org/wiki/Baudot_code) |
| 83 | +UK = [ |
| 84 | + "10000", "01000", |
| 85 | + "\x00AE/YUIO\xffJGHBCFD -XZSTWV\x7fKMLRQNP", |
| 86 | + "\x0012\u215f34\xff5 67\xb989\xff0\xff.\xff:\xff²?'\x7f()=-/£+", |
| 87 | +] |
| 88 | + |
| 89 | + |
| 90 | +class BaudotDecodeError(ValueError): |
| 91 | + pass |
| 92 | + |
| 93 | + |
| 94 | +class BaudotEncodeError(ValueError): |
| 95 | + pass |
| 96 | + |
| 97 | + |
| 98 | +def _bits_from_tape(tape, trans={'*': "1", ' ': "0"}): |
| 99 | + """ Converts a tape-like string with the given translation for ones and zeros to a series of bits. """ |
| 100 | + bits = "" |
| 101 | + trans_rev = {v: k for k, v in trans.items()} |
| 102 | + for i, line in enumerate(tape.splitlines()): |
| 103 | + if i == 0: |
| 104 | + if line != trans_rev['1'] * 3 + "." + trans_rev['1'] * 2: |
| 105 | + raise ValueError("Bad tape header '{}'".format(line)) |
| 106 | + else: |
| 107 | + line = line[:3] + line[4:] |
| 108 | + if len(line) != 5: |
| 109 | + raise ValueError("Bad tape line '{}'".format(line)) |
| 110 | + bits += "".join(trans.get(c, "") for c in line) |
| 111 | + return bits |
| 112 | + |
| 113 | + |
| 114 | +def _bits_to_tape(bits, trans={'1': "*", '0': " "}): |
| 115 | + """ Converts a series of bits to a tape-like string with the given translation for ones and zeros. """ |
| 116 | + tape = [trans['1'] * 3 + "." + trans['1'] * 2] |
| 117 | + for i in range(0, len(bits), 5): |
| 118 | + group = "".join(trans[b] for b in bits[i:i+5]) |
| 119 | + tape.append(group[:3] + "." + group[3:]) |
| 120 | + return "\n".join(tape) |
| 121 | + |
| 122 | + |
| 123 | +def _check_alphabet(alphabet): |
| 124 | + """ Checks the length of letters and figures (must be 32 chars). """ |
| 125 | + for chars in alphabet: |
| 126 | + l = len(chars) |
| 127 | + if l != 32: |
| 128 | + raise ValueError("Bad length of alphabet (%d instead of 32)" % l) |
| 129 | + |
| 130 | + |
| 131 | +def _handle_alphabet(alphabet): |
| 132 | + """ Gets the given alphabet name and transforms it to its dictionary with letters and figures. """ |
| 133 | + alphabet = (alphabet or "baudot").lower().replace("-", "_").strip("_") |
| 134 | + if "_lsb" in alphabet: |
| 135 | + alphabet = alphabet.replace("_lsb", "") |
| 136 | + func = lambda x: x[::-1] |
| 137 | + else: |
| 138 | + alphabet = alphabet.replace("_msb", "") |
| 139 | + func = lambda x: x |
| 140 | + _ = globals()[alphabet.upper()] |
| 141 | + st, a = _[:2], _[2:] |
| 142 | + _check_alphabet(a) |
| 143 | + alphabet = {n: {ch: bin(i)[2:].zfill(5) for i, ch in enumerate(alph) if ch != RES_CHR} for n, alph in \ |
| 144 | + zip(["letters", "figures"], a)} |
| 145 | + return alphabet, {'letters': st[0], 'figures': st[1]}, func |
| 146 | + |
| 147 | + |
| 148 | +def baudot_encode(alphabet=None, spaced=False, tape=False): |
| 149 | + ename = "baudot" + ("-spaced" if spaced else "-tape" if tape else "") |
| 150 | + alphabet, states, func = _handle_alphabet(alphabet) |
| 151 | + def encode(text, errors="strict"): |
| 152 | + s, state, seen_states = "", None, [] |
| 153 | + for i, c in enumerate(text): |
| 154 | + # if the state is undefined yet, find the relevant alphabet |
| 155 | + if state is None: |
| 156 | + bits= None |
| 157 | + for st in states.keys(): |
| 158 | + try: |
| 159 | + bits = func(alphabet[st][c]) |
| 160 | + state = st |
| 161 | + if st not in seen_states: |
| 162 | + seen_states.append(st) |
| 163 | + break |
| 164 | + except KeyError: |
| 165 | + pass |
| 166 | + if bits is None: |
| 167 | + handle_error(ename, errors, BaudotEncodeError, "?", 5)(c, i) |
| 168 | + s += bits |
| 169 | + # otherwise, handle state change (when the current alphabet does not contain the character to encode but the |
| 170 | + # other alphabet does |
| 171 | + else: |
| 172 | + try: |
| 173 | + s += func(alphabet[state][c]) |
| 174 | + continue |
| 175 | + except KeyError: |
| 176 | + state = list(set(states.keys()) - {state})[0] |
| 177 | + try: |
| 178 | + char = func(alphabet[state][c]) |
| 179 | + s += func(states[state]) + char |
| 180 | + if state not in seen_states: |
| 181 | + seen_states.append(state) |
| 182 | + except KeyError as e: |
| 183 | + handle_error(ename, errors, BaudotEncodeError, "?", 5)(c, i) |
| 184 | + state = list(set(states.keys()) - {state})[0] # reset the state |
| 185 | + # by default, if no state is specified, the encoded string is handled as letters ; so if figures are used only, |
| 186 | + # it is necessary to include the groups of bits for figures at the beginning of the encoded string |
| 187 | + s = (states['figures'] if seen_states == ["figures"] else "") + s |
| 188 | + if spaced: |
| 189 | + s = " ".join(s[i:i+5] for i in range(0, len(s), 5)) |
| 190 | + elif tape: |
| 191 | + s = _bits_to_tape(s) |
| 192 | + return s, len(s) |
| 193 | + return encode |
| 194 | + |
| 195 | + |
| 196 | +def baudot_decode(alphabet=None, spaced=False, tape=False): |
| 197 | + ename = "baudot" + ("-spaced" if spaced else "-tape" if tape else "") |
| 198 | + alphabet, states, func = _handle_alphabet(alphabet) |
| 199 | + alphabet = {st: {v: k for k, v in alph.items()} for st, alph in alphabet.items()} |
| 200 | + states = {v: k for k, v in states.items()} |
| 201 | + def decode(text, errors="strict"): |
| 202 | + s = "" |
| 203 | + if spaced: |
| 204 | + text = text.replace(" ", "") |
| 205 | + elif tape: |
| 206 | + text = _bits_from_tape(text) |
| 207 | + # infer the starting state by searching for the first encountered groups of bits indicating a valid state ; |
| 208 | + # by default, we assume letters |
| 209 | + state = "letters" |
| 210 | + for i in range(0, len(text), 5): |
| 211 | + bits = func(text[i:i+5]) |
| 212 | + # the following code handles a possible ambiguity ; e.g. when letters have a group of bits matching |
| 213 | + # a state change |
| 214 | + if bits in states.keys(): |
| 215 | + error = False |
| 216 | + # so, when we see the bits of a state, we parse previous groups in order to determine if they are valid |
| 217 | + # groups in the corresponding state, that is, if no error occurs ; if an error occurs, then it is a |
| 218 | + # valid state change and not simply a character, and we can set it as the starting state |
| 219 | + for j in range(i-5, 0, -5): |
| 220 | + try: |
| 221 | + alphabet[states[bits]][text[j:j+5]] |
| 222 | + except KeyError: |
| 223 | + error = True |
| 224 | + break |
| 225 | + if error: |
| 226 | + state = list(set(states.values()) - {states[bits]})[0] |
| 227 | + break |
| 228 | + # now parse the input text |
| 229 | + for i in range(0, len(text), 5): |
| 230 | + bits = func(text[i:i+5]) |
| 231 | + try: |
| 232 | + s += alphabet[state][bits] |
| 233 | + except KeyError: |
| 234 | + if bits in states.keys() and states[bits] != state: |
| 235 | + state = states[bits] |
| 236 | + else: |
| 237 | + handle_error(ename, errors, BaudotDecodeError, decode=True, item="group")(bits, i//5) |
| 238 | + return s, len(s) |
| 239 | + return decode |
| 240 | + |
| 241 | + |
| 242 | +add("baudot", baudot_encode, baudot_decode, PATTERN % r"") |
| 243 | + |
| 244 | + |
| 245 | +baudot_spaced_encode = lambda a: baudot_encode(a, spaced=True) |
| 246 | +baudot_spaced_decode = lambda a: baudot_decode(a, spaced=True) |
| 247 | +add("baudot-spaced", baudot_spaced_encode, baudot_spaced_decode, PATTERN % r"[-_]spaced") |
| 248 | + |
| 249 | + |
| 250 | +baudot_tape_encode = lambda a: baudot_encode(a, tape=True) |
| 251 | +baudot_tape_decode = lambda a: baudot_decode(a, tape=True) |
| 252 | +add("baudot-tape", baudot_tape_encode, baudot_tape_decode, PATTERN % r"[-_]tape") |
| 253 | + |
0 commit comments