Skip to content

Commit fc5bdbb

Browse files
committed
Added new codec: baudot
1 parent 5c56832 commit fc5bdbb

5 files changed

Lines changed: 424 additions & 44 deletions

File tree

README.md

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,38 @@ This library extends the native `codecs` library and provides some new encodings
1313

1414
**Codec** | **Conversions** | **Comment**
1515
:---: | :---: | ---
16-
`affine` | Affine <-> text | aka Affine Cipher
17-
`ascii85` | Ascii85 <-> text | Python 3 only
18-
`atbash` | Atbash <-> text | aka Atbash Cipher
19-
`bacon` | Bacon <-> text | aka Baconian Cipher
20-
`barbie-N` | Barbie <-> text | aka Barbie Typewriter (N belongs to [1, 4])
21-
`baseXX` | BaseXX <-> text | see [base encodings](https://python-codext.readthedocs.io/en/latest/base.html)
22-
`braille` | braille <-> text | Python 3 only
23-
`dna-N` | DNA-N <-> text | implements the 8 rules of DNA sequences (N belongs to [1,8])
24-
`leetspeak` | leetspeak <-> text | based on minimalistic elite speaking rules
16+
`affine` | text <-> affine ciphertext | aka Affine Cipher
17+
`ascii85` | text <-> ascii85 encoded text | Python 3 only
18+
`atbash` | text <-> Atbash ciphertext | aka Atbash Cipher
19+
`bacon` | text <-> Bacon ciphertext | aka Baconian Cipher
20+
`barbie-N` | text <-> barbie ciphertext | aka Barbie Typewriter (N belongs to [1, 4])
21+
`baseXX` | text <-> baseXX | see [base encodings](https://python-codext.readthedocs.io/en/latest/base.html)
22+
`baudot` | text <-> Baudot code bits | supports CCITT-1, CCITT-2, EU/FR, ITA1, ITA2, MTK-2 (Python3 only), UK, ...
23+
`braille` | text <-> braille symbols | Python 3 only
24+
`dna-N` | text <-> DNA-N sequence | implements the 8 rules of DNA sequences (N belongs to [1,8])
25+
`leetspeak` | text <-> leetspeak encoded text | based on minimalistic elite speaking rules
2526
`markdown` | markdown --> HTML | unidirectional
26-
`morse` | morse <-> text | uses whitespace as a separator
27-
`octal` | Octal <-> text | dummy octal conversion (converts to 3-digits groups)
28-
`ordinal` | Ordinal <-> text | dummy character ordinals conversion (converts to 3-digits groups)
29-
`radio` | Radio <-> text | aka NATO or radio phonetic alphabet
30-
`resistor` | Resistor <-> text | aka resistor color codes
31-
`rot-N` | ROT(N) <-> text | aka Caesar cipher (N belongs to [1,25])
32-
`shift` | shift(N) <-> text | shift ordinals with N (belongs to [1,255])
33-
`sms` | Phone keystrokes <-> text | also called T9 code ; uses "`-`" as a separator for encoding, "`-`" or "`_`" or whitespace for decoding
34-
`url` | URL <-> text | aka URL encoding
35-
`xor-N` | XOR(N) <-> text | XOR with a single byte (N belongs to [1,255])
36-
`whitespace` | Whitespaces <-> text | replaces bits with whitespaces and tabs
27+
`morse` | text <-> morse encoded text | uses whitespace as a separator
28+
`octal` | text <-> octal digits | dummy octal conversion (converts to 3-digits groups)
29+
`ordinal` | text <-> ordinal digits | dummy character ordinals conversion (converts to 3-digits groups)
30+
`radio` | text <-> radio words | aka NATO or radio phonetic alphabet
31+
`resistor` | text <-> resistor colors | aka resistor color codes
32+
`rot-N` | text <-> rot(N) ciphertext | aka Caesar cipher (N belongs to [1,25])
33+
`shift` | text <-> shift(N) ciphertext | shift ordinals with N (belongs to [1,255])
34+
`sms` | text <-> phone keystrokes | also called T9 code ; uses "`-`" as a separator for encoding, "`-`" or "`_`" or whitespace for decoding
35+
`url` | text <-> URL encoded text | aka URL encoding
36+
`xor-N` | text <-> XOR(N) ciphertext | XOR with a single byte (N belongs to [1,255])
37+
`whitespace` | text <-> whitespaces and tabs | replaces bits with whitespaces and tabs
3738

3839
A few variants are also implemented.
3940

4041
**Codec** | **Conversions** | **Comment**
4142
:---: | :---: | ---
42-
`octal-spaced` | Octal (whitespace-separated) <-> text | dummy octal conversion
43-
`ordinal-spaced` | Ordinal (whitespace-separated) <-> text | dummy character ordinals conversion
44-
`whitespace_after_before` | Whitespaces[letter]whitespaces <-> text | encodes characters as new characters with whitespaces before and after according to an equation described in the codec name (e.g. "`whitespace+2*after-3*before`")
43+
`baudot-spaced` | text <-> Baudot code groups of bits | groups of 5 bits are whitespace-separated
44+
`baudot-tape` | text <-> Baudot code tape | outputs a string that looks like a perforated tape
45+
`octal-spaced` | text <-> octal digits (whitespace-separated) | dummy octal conversion
46+
`ordinal-spaced` | text <-> ordinal digits (whitespace-separated) | dummy character ordinals conversion
47+
`whitespace_after_before` | text <-> lines of whitespaces[letter]whitespaces | encodes characters as new characters with whitespaces before and after according to an equation described in the codec name (e.g. "`whitespace+2*after-3*before`")
4548

4649

4750
## Setup

codext/others/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# -*- coding: UTF-8 -*-
2+
from .baudot import *
23
from .dna import *
34
from .dummy import *
45
from .markdown import *
56
from .octal import *
67
from .ordinal import *
78
from .url import *
9+

codext/others/baudot.py

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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

Comments
 (0)