Skip to content

Commit 754324f

Browse files
committed
Added codecs: fancy, upsidedown
1 parent 38191c0 commit 754324f

6 files changed

Lines changed: 266 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
434434
#### [Languages](https://python-codext.readthedocs.io/en/latest/enc/languages.html)
435435

436436
- [X] `braille`: well-known braille language (Python 3 only)
437+
- [X] `fancy`: Unicode styled alphabets (mathematical bold, italic, script, fraktur, double-struck, ..., circled and parenthesized letters), also `fullwidth` and `smallcaps`
437438
- [X] `ipsum`: aka lorem ipsum
438439
- [X] `galactic`: aka Standard Galactic Alphabet or Minecraft enchantment language (as written by most online translators)
439440
- [X] `leetspeak`: based on minimalistic elite speaking rules
@@ -444,6 +445,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
444445
- [X] `southpark-icase`: case insensitive variant of `southpark`
445446
- [X] `tap`: converts text to tap/knock code, commonly used by prisoners (as written by cryptii)
446447
- [X] `tomtom`: similar to `morse`, using slashes and backslashes
448+
- [X] `upsidedown`: upside-down (flip) text, with turned Unicode characters in reverse order
447449

448450
#### [Others](https://python-codext.readthedocs.io/en/latest/enc/others.html)
449451

‎docs/pages/enc/languages.md‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,33 @@ It supports letters, digits and some special characters.
2121

2222
-----
2323

24+
### Fancy alphabets
25+
26+
Unicode has styled versions of the Latin letters and of the digits, often used to write "fancy" text (e.g. in social networks) or to obfuscate text (e.g. to evade filters): the [mathematical alphanumeric symbols](https://www.unicode.org/charts/PDF/U1D400.pdf) (some letters being in the Letterlike Symbols block, e.g. `ℬ` or `ℎ`), the circled and parenthesized letters and digits, the full-width forms of ASCII and the small capitals. Encoding writes the letters and digits of the text in the given style, keeping the other characters, while decoding writes back the characters of this style ; `fancy` without a style decodes all of them at once (e.g. for text mixing several styles).
27+
28+
**Codec** | **Conversions** | **Aliases** | **Comment**
29+
:---: | :---: | --- | ---
30+
`fancy` | text <-> styled text | `fancy-bold`, `fancy-italic`, `fancy-bold-italic`, `fancy-script`, `fancy-bold-script`, `fancy-fraktur`, `fancy-bold-fraktur`, `fancy-double-struck`, `fancy-sans-serif`, `fancy-sans-serif-bold`, `fancy-sans-serif-italic`, `fancy-sans-serif-bold-italic`, `fancy-monospace`, `fancy-circled`, `fancy-parenthesized` | parametrized with the style (required for encoding) ; digits only in the bold, double-struck, sans-serif, sans-serif bold, monospace, circled and parenthesized (except 0) styles
31+
`fullwidth` | text <-> full-width text | `full-width`, `fancy-fullwidth` | printable ASCII characters, the space being written as an ideographic space
32+
`smallcaps` | text <-> text with small capitals | `small-caps`, `fancy-smallcaps` | lowercase letters only (uppercase letters kept), except `x` that has no small capital
33+
34+
!!! note "NFKC normalization"
35+
36+
The NFKC normalization (`unicodedata.normalize("NFKC", ...)`) also undoes the mathematical, circled and full-width styles, but not the parenthesized one (giving e.g. `(a)`) nor the small capitals. Unlike this codec, it changes other characters as well (e.g. ligatures and superscripts).
37+
38+
```python
39+
>>> codext.encode("Hello World", "fancy-script")
40+
'ℋℯ𝓁𝓁ℴ 𝒲ℴ𝓇𝓁𝒹'
41+
>>> codext.encode("Hello World", "fancy-double-struck")
42+
'ℍ𝕖𝕝𝕝𝕠 𝕎𝕠𝕣𝕝𝕕'
43+
>>> codext.encode("Hello World", "smallcaps")
44+
'Hᴇʟʟᴏ Wᴏʀʟᴅ'
45+
>>> codext.decode("𝐇𝖊𝚕ⓛ⒪ wᴏʀʟᴅ", "fancy")
46+
'Hello world'
47+
```
48+
49+
-----
50+
2451
### Galactic
2552

2653
This implements the Standard Galactic Alphabet, i.e. the language of [Minecraft's enchanting table](https://www.thegamer.com/minecraft-enchantment-table-language-guide/), using resembling Unicode characters as most online translators (e.g. LingoJam) do: one character for most letters, but `!¡` for *p*, `ℸ ̣ ` (with a combining dot below) for *t*, ` ̇/` (with a combining dot above) for *x* and `||` for *y*. Other ASCII characters are kept, except `!` and `|`.
@@ -199,3 +226,20 @@ This codec is similar to morse. It converts text into slashes and backslashes.
199226
>>> codext.decode("\\\\/\\ /\\\\ /\\\\\\ \\/\\ | /\\\\\\ \\/\\ | / | \\\\/\\ /\\ \\/\\ \\\\/\\", "tomtom")
200227
'THIS IS A TEST'
201228
```
229+
230+
-----
231+
232+
### Upside-down
233+
234+
[Upside-down text](https://en.wikipedia.org/wiki/Transformation_of_text#Upside-down_text) (aka flip text) is written with Unicode characters that look like the letters, digits and punctuation turned by 180 degrees (e.g. `ɐ` for `a`, `ꓭ` for `B` or `¿` for `?`), in reverse order, so that the text reads normally when turned upside down. As turning the text twice gives it back, encoding and decoding are the same operation.
235+
236+
**Codec** | **Conversions** | **Aliases** | **Comment**
237+
:---: | :---: | --- | ---
238+
`upsidedown` | text <-> upside-down text | `upside-down`, `flip-text` | other characters kept as is ; decoding also accepts other turned characters of the online generators (e.g. `∀` for `A` or `∩` for `U`)
239+
240+
```python
241+
>>> codext.encode("Hello World", "upsidedown")
242+
'pꞁɹoM oꞁꞁǝH'
243+
>>> codext.decode("pꞁɹoM oꞁꞁǝH", "upside-down")
244+
'Hello World'
245+
```

‎src/codext/languages/__init__.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: UTF-8 -*-
22
from .braille import *
3+
from .fancy import *
34
from .galactic import *
45
from .ipsum import *
56
from .leetspeak import *
@@ -9,4 +10,5 @@
910
from .southpark import *
1011
from .tap import *
1112
from .tomtom import *
13+
from .upsidedown import *
1214

‎src/codext/languages/fancy.py‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Fancy Codecs - Unicode styled alphabets content encoding.
3+
4+
Unicode has styled versions of the Latin letters and of the digits, often used to write "fancy" text (e.g. in social
5+
networks) or to obfuscate text (e.g. to evade filters): the mathematical alphanumeric symbols (bold, italic, bold
6+
italic, script, bold script, fraktur, bold fraktur, double-struck, sans-serif, sans-serif bold, sans-serif italic,
7+
sans-serif bold italic and monospace, some letters being in the Letterlike Symbols block, e.g. "ℬ" or "ℎ"), the
8+
circled and parenthesized letters and digits, the full-width forms of ASCII and the small capitals (for the lowercase
9+
letters, except "x" that has no small capital). Encoding writes the letters and digits of the text in the given style
10+
(or, for full-width forms, all the printable ASCII characters, and the space as an ideographic space), keeping the
11+
other characters, while decoding writes back the characters of this style ; "fancy" without a style decodes all of
12+
them at once (e.g. for text mixing several styles).
13+
14+
The NFKC normalization (unicodedata.normalize("NFKC", ...)) also undoes the mathematical, circled and full-width styles,
15+
but not the parenthesized one (giving e.g. "(a)") nor the small capitals ; unlike this codec, it changes other
16+
characters as well (e.g. ligatures and superscripts).
17+
18+
These codecs:
19+
- en/decode strings from str to str
20+
- en/decode strings from bytes to bytes
21+
- decode file content to str (read)
22+
- encode file content from str to bytes (write)
23+
24+
Reference: https://www.unicode.org/charts/PDF/U1D400.pdf
25+
"""
26+
import unicodedata
27+
from string import ascii_lowercase, ascii_uppercase, digits
28+
29+
from ..__common__ import *
30+
31+
32+
__examples1__ = {
33+
'enc(fancy-bold|fancy_bold)': {'Hello 2026!': "\U0001d407\U0001d41e\U0001d425\U0001d425\U0001d428 \U0001d7d0"
34+
"\U0001d7ce\U0001d7d0\U0001d7d4!"},
35+
'enc(fancy-script)': {'Bohe': "ℬℴ\U0001d4bdℯ"},
36+
'dec(fancy)': {'\U0001d407\U0001d58a\U0001d695ⓛ⒪ wᴏʀʟᴅ': "Hello world"},
37+
'enc-dec(fancy-%s)': ["", "Hello World 2026", "caf\xe9 & €, 日本!"],
38+
}
39+
__examples2__ = {
40+
'enc(fullwidth|full-width|full_width)': {'Hello, World!': "Hello,\u3000Wo"
41+
"rld!"},
42+
'enc-dec(fullwidth)': ["", "@random{256}"],
43+
}
44+
__examples3__ = {
45+
'enc(smallcaps|small-caps|small_caps)': {'Hello World': "Hᴇʟʟᴏ Wᴏʀʟᴅ"},
46+
'enc-dec(smallcaps)': ["", "The quick brown fox jumps over the lazy dog", "@random{256}"],
47+
}
48+
49+
50+
_DIGITS = ["ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE"]
51+
# names of the letters of some styles that are in the Letterlike Symbols block (e.g. "SCRIPT CAPITAL B")
52+
_LETTERLIKE = {'SCRIPT': "SCRIPT", 'FRAKTUR': "BLACK-LETTER", 'DOUBLE-STRUCK': "DOUBLE-STRUCK"}
53+
54+
55+
def _lookup(*names):
56+
""" First character with one of the given names (the characters missing from the Unicode database of the version of
57+
Python being skipped). """
58+
for name in names:
59+
try:
60+
return unicodedata.lookup(name)
61+
except KeyError:
62+
pass
63+
64+
65+
def __alphabet(name, chars):
66+
return {c: f for c in chars if (f := _lookup(*name(c)))}
67+
68+
69+
def __math(style):
70+
case = lambda c: "CAPITAL" if c.isupper() else "SMALL"
71+
return __alphabet(lambda c: [f"MATHEMATICAL {style} DIGIT {_DIGITS[int(c)]}"] if c.isdigit() else
72+
[f"MATHEMATICAL {style} {case(c)} {c.upper()}"] +
73+
([f"{_LETTERLIKE[style]} {case(c)} {c.upper()}"] if style in _LETTERLIKE else []) +
74+
["PLANCK CONSTANT"] * (style == "ITALIC" and c == "h"),
75+
ascii_uppercase + ascii_lowercase + digits)
76+
77+
78+
def __enclosed(kind):
79+
return __alphabet(lambda c: [f"{kind} DIGIT {_DIGITS[int(c)]}"] if c.isdigit() else
80+
[f"{kind} LATIN {'CAPITAL' if c.isupper() else 'SMALL'} LETTER {c.upper()}"],
81+
ascii_uppercase + ascii_lowercase + digits)
82+
83+
84+
ALPHABETS = {s.lower().replace(" ", "-"): __math(s) for s in ["BOLD", "ITALIC", "BOLD ITALIC", "SCRIPT", "BOLD SCRIPT",
85+
"FRAKTUR", "BOLD FRAKTUR", "DOUBLE-STRUCK", "SANS-SERIF", "SANS-SERIF BOLD", "SANS-SERIF ITALIC",
86+
"SANS-SERIF BOLD ITALIC", "MONOSPACE"]}
87+
ALPHABETS.update({
88+
'circled': __enclosed("CIRCLED"),
89+
'parenthesized': __enclosed("PARENTHESIZED"),
90+
'fullwidth': {chr(i): chr(i + 0xfee0) for i in range(0x21, 0x7f)} | {" ": "\u3000"},
91+
'smallcaps': __alphabet(lambda c: [f"LATIN LETTER SMALL CAPITAL {c.upper()}"], ascii_lowercase),
92+
})
93+
DECODE = {k: {f: c for c, f in a.items()} for k, a in ALPHABETS.items()}
94+
DECODE_ALL = {f: c for d in DECODE.values() for f, c in d.items()}
95+
__examples1__["enc-dec(fancy-%s)" % "|fancy-".join(ALPHABETS)] = __examples1__.pop("enc-dec(fancy-%s)")
96+
97+
98+
def fancy_encode(style=None):
99+
def encode(text, errors="strict"):
100+
if not style:
101+
raise LookupError("Bad parameter for encoding 'fancy': a style is required, among " + ", ".join(ALPHABETS))
102+
alphabet = ALPHABETS[style.replace("_", "-")]
103+
return "".join(alphabet.get(c, c) for c in ensure_str(text)), len(text)
104+
return encode
105+
106+
107+
def fancy_decode(style=None):
108+
def decode(text, errors="strict"):
109+
table = DECODE[style.replace("_", "-")] if style else DECODE_ALL
110+
return "".join(table.get(c, c) for c in ensure_str(text)), len(text)
111+
return decode
112+
113+
114+
add("fancy", fancy_encode, fancy_decode, r"^fancy(?:[-_](" + "|".join(s.replace("-", "[-_]") for s in ALPHABETS) +
115+
r"))?$", examples=__examples1__, guess=None)
116+
add("fullwidth", fancy_encode("fullwidth"), fancy_decode("fullwidth"), r"^full[-_]?width$", examples=__examples2__,
117+
guess=None)
118+
add("smallcaps", fancy_encode("smallcaps"), fancy_decode("smallcaps"), r"^small[-_]?caps$", examples=__examples3__,
119+
guess=None)

‎src/codext/languages/upsidedown.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Upside-down Codec - upside-down (flip) text content encoding.
3+
4+
Upside-down text (aka flip text) is written with Unicode characters that look like the letters, digits and punctuation
5+
turned by 180 degrees (mostly from the IPA, Lisu and Letterlike Symbols blocks, e.g. "ɐ" for "a", "ꓭ" for "B" or "¿"
6+
for "?"), in reverse order, so that the text reads normally when turned upside down. Some characters are their own
7+
turned versions (e.g. "o", "s", "x", "H" or "8") or the turned version of each other (e.g. "b" and "q", "n" and "u",
8+
"M" and "W", "6" and "9" or the brackets), and the table pairs every character with its turned version, hence turning
9+
the text twice gives it back: encoding and decoding are the same operation. Decoding also accepts other characters
10+
used by the online generators (e.g. "∀" for "A", "˥" for "L" or "∩" for "U"). The other characters are kept as is.
11+
12+
This codec:
13+
- en/decodes strings from str to str
14+
- en/decodes strings from bytes to bytes
15+
- decodes file content to str (read)
16+
- encodes file content from str to bytes (write)
17+
18+
Reference: https://en.wikipedia.org/wiki/Transformation_of_text#Upside-down_text
19+
"""
20+
from ..__common__ import *
21+
22+
23+
__examples__ = {
24+
'enc(upsidedown|upside-down|upside_down|flip-text|fliptext)': {
25+
'Hello, World!': "\xa1pꞁɹoM 'oꞁꞁǝH", 'hello world': "pꞁɹoʍ oꞁꞁǝɥ",
26+
},
27+
'dec(upsidedown)': {"\xa1pꞁɹoM '∀": "A, World!"},
28+
'enc-dec(upsidedown)': ["", "The quick brown fox jumps over the lazy dog: 0123456789 (&) [?] {!} <_> \"'.,;",
29+
"THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG", "caf\xe9 €"],
30+
}
31+
32+
33+
# characters with their turned versions (based on the table of Wikipedia, with single characters, the M/W pair and the
34+
# Lisu U, and the digits of the online generators), each pair being used in both directions
35+
_PAIRS = [
36+
("a", "ɐ"), ("b", "q"), ("c", "ɔ"), ("d", "p"), ("e", "ǝ"), ("f", "ɟ"), ("g", "ᵷ"), ("h", "ɥ"), ("i", "ᴉ"),
37+
("j", "ɾ"), ("k", "ʞ"), ("l", "ꞁ"), ("m", "ɯ"), ("n", "u"), ("r", "ɹ"), ("t", "ʇ"), ("v", "ʌ"), ("w", "ʍ"),
38+
("y", "ʎ"), ("A", "Ɐ"), ("B", "ꓭ"), ("C", "Ɔ"), ("D", "ꓷ"), ("E", "Ǝ"), ("F", "Ⅎ"), ("G", "⅁"), ("J", "ꓩ"),
39+
("K", "Ʞ"), ("L", "Ꞁ"), ("M", "W"), ("P", "Ԁ"), ("Q", "Ꝺ"), ("R", "ꓤ"), ("T", "Ʇ"), ("U", "ꓵ"), ("V", "Ʌ"),
40+
("Y", "⅄"), ("1", "Ɩ"), ("2", "↊"), ("3", "↋"), ("4", "ㄣ"), ("5", "ϛ"), ("6", "9"), ("7", "ㄥ"), ("&", "⅋"),
41+
("_", "‾"), ("?", "¿"), ("!", "¡"), ('"', "„"), ("'", ","), (".", "˙"), (";", "؛"), ("(", ")"), ("[", "]"),
42+
("{", "}"), ("<", ">"),
43+
]
44+
# other turned versions used by the online generators, only decoded
45+
_VARIANTS = {
46+
"ı": "i", "ן": "l", "ƃ": "g", "ɓ": "g", "∀": "A", "ꓯ": "A", "𐐒": "B", "ꓛ": "C", "ꓱ": "E", "ꓞ": "F", "ꓨ": "G",
47+
"ſ": "J", "ꓘ": "K", "˥": "L", "⅂": "L", "ꓶ": "L", "ꟽ": "M", "ꓒ": "P", "ꓕ": "T", "⊥": "T", "∩": "U", "Ո": "U",
48+
"Λ": "V", "ꓥ": "V", "𐤵": "W", "ᄅ": "2", "Ɛ": "3", "߈": "4", "𝘓": "7", "\U0001fbf1": "1", "\U0001fbf5": "5",
49+
}
50+
UPSIDE_DOWN = {**{a: b for a, b in _PAIRS}, **{b: a for a, b in _PAIRS}, **_VARIANTS}
51+
52+
53+
def upsidedown(text, errors="strict"):
54+
return "".join(UPSIDE_DOWN.get(c, c) for c in reversed(ensure_str(text))), len(text)
55+
56+
57+
add("upsidedown", upsidedown, upsidedown, r"^(?:upside[-_]?down|flip[-_]?text)$", examples=__examples__,
58+
guess=None)

‎tests/test_manual.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,47 @@ def test_codec_hash_functions(self):
965965
self.assertIsNotNone(codecs.encode(STR, h := f"crypt-{m}"))
966966
self.assertRaises(NotImplementedError, codecs.decode, STR, h)
967967

968+
def test_codec_fancy_upsidedown(self):
969+
import unicodedata
970+
from string import ascii_letters, digits
971+
from codext.languages.fancy import ALPHABETS
972+
from codext.languages.upsidedown import _PAIRS, UPSIDE_DOWN, _VARIANTS
973+
# complete alphabets (with the letters of the Letterlike Symbols block, e.g. "\u212c" for the script B), each
974+
# styled character being decoded back
975+
math = [s for s in ALPHABETS if s not in ("circled", "parenthesized", "fullwidth", "smallcaps")]
976+
for style in math + ["circled", "parenthesized"]:
977+
self.assertTrue(all(c in ALPHABETS[style] for c in ascii_letters))
978+
self.assertEqual(ALPHABETS["script"]["B"], "\u212c")
979+
self.assertEqual(ALPHABETS["italic"]["h"], "\u210e")
980+
self.assertEqual(ALPHABETS["double-struck"]["R"], "\u211d")
981+
self.assertEqual(sorted(s for s in math if "0" in ALPHABETS[s]),
982+
["bold", "double-struck", "monospace", "sans-serif", "sans-serif-bold"])
983+
self.assertNotIn("x", ALPHABETS["smallcaps"])
984+
text = "The quick brown fox jumps over the lazy dog 0123456789"
985+
# the NFKC normalization also undoes the mathematical, circled and full-width styles, but not the parenthesized
986+
# one, the small capitals and the upside-down text
987+
for style in ALPHABETS:
988+
enc = codecs.encode(text, f"fancy-{style}")
989+
self.assertEqual(codecs.decode(enc, f"fancy-{style}"), text)
990+
self.assertEqual(codecs.decode(enc, "fancy"), text)
991+
self.assertEqual(unicodedata.normalize("NFKC", enc) == text, style not in ("parenthesized", "smallcaps"))
992+
self.assertNotEqual(unicodedata.normalize("NFKC", codecs.encode(text, "upsidedown")), text)
993+
# a style is required for encoding, while decoding without a style handles mixed styles
994+
self.assertRaises(LookupError, codecs.encode, text, "fancy")
995+
mixed = "".join(codecs.encode(c, f"fancy-{s}") for c, s in zip("Hello", ALPHABETS))
996+
self.assertEqual(codecs.decode(mixed, "fancy"), "Hello")
997+
# small capitals for the lowercase letters only (there is no small capital x)
998+
self.assertEqual(codecs.encode("Mixed CASE x", "smallcaps"), "M\u026ax\u1d07\u1d05 CASE x")
999+
# upside-down: pairs of turned characters, used in both directions (turning twice gives the text back), and
1000+
# the other turned versions of the online generators being decoded
1001+
self.assertEqual(len({c for p in _PAIRS for c in p}), 2 * len(_PAIRS))
1002+
for c in UPSIDE_DOWN:
1003+
if c not in _VARIANTS:
1004+
self.assertEqual(UPSIDE_DOWN[UPSIDE_DOWN[c]], c)
1005+
self.assertEqual(codecs.decode("\u2229\u02e5\ua7fd\u2200", "upsidedown"), "AMLU")
1006+
self.assertEqual(codecs.decode(codecs.encode(text, "upsidedown"), "upsidedown"), text)
1007+
self.assertEqual(codecs.encode(b"Hi!", "upside-down"), "\u00a1\u1d09H".encode("utf-8"))
1008+
9681009
def test_codec_markdown(self):
9691010
HTM = "<h1>Test title</h1>\n\n<p>Test paragraph</p>\n"
9701011
MD = "# Test title\n\nTest paragraph"

0 commit comments

Comments
 (0)